An unofficial, simplified version of the @Shopify/koa-shopify-auth middleware library.

Overview

simple-koa-shopify-auth

https://www.npmjs.com/package/simple-koa-shopify-auth

NOTE: This package is not maintained by or affiliated with Shopify.

Description:

A better, simplified version of the (no longer supported) @Shopify/koa-shopify-auth middleware library. It removes the use of cookies for sessions (which greatly smooths the auth process by requiring fewer redirects in some cases), replaces a deprecated API call, and supports v2 of the official @shopify/shopify-api package.

Installation:

npm i simple-koa-shopify-auth

Requirements:

This package assumes you have @shopify/shopify-api v2 already installed. If you are on v1.x.x you will need to upgrade to the latest version with npm i @shopify/shopify-api@latest.

WARNING:

@shopify/shopify-api v2 has some breaking changes from v1. Please check the changelog to see all the changes, and update your code accordingly.

Usage:

The usage is very similar to @Shopify/koa-shopify-auth (which you should check for more examples), but there are a few differences, so it isn't a drop-in replacement.

Import the middleware functions (ES6 syntax):

import { createShopifyAuth, verifyRequest } from "simple-koa-shopify-auth";

Importing differs slightly from the official library in that the createShopifyAuth function is not a default import here, and has been renamed.

Using verifyRequest for verifying session token on routes:

For requests, create the middleware like this:

// For requests from the frontend, we want to return headers, so we can check if we need to reauth on the client side
const verifyApiRequest = verifyRequest({ returnHeader: true });
const verifyPageRequest = verifyRequest();

The verifyRequest middleware function only accepts the following parameters (default values shown):

NOTE: These parameters differ from the ones in the official library.

{
  accessMode: "online",  // The access mode of the token to check
  authRoute: "/auth",  // Where to redirect if the session is invalid
  returnHeader: false,  // If true, set headers instead of redirecting if session is invalid
}

For more help on how to use the middleware functions, check the examples from the official library.

Registering middleware for handling auth routes:

The createShopifyAuth middleware function only accepts the following parameters (default values shown):

NOTE: These parameters differ from the ones in the official library.

{
  accessMode: "online",  // What kind of token we want to fetch
  authPath: "/auth",  // The path to handle the request on
  async afterAuth(ctx) { }  // Callback function after auth is completed (the token is available at ctx.state.shopify)
}

This is a simple example that you can use to help understand how to implement it.

const server = new Koa();

// Installation route (get offline, permanent access token)
server.use(
  createShopifyAuth({
    accessMode: "offline",
    authPath: "/install/auth",
    async afterAuth(ctx) {
      const { shop, accessToken } = ctx.state.shopify;
      if (!accessToken) {
        // This can happen if the browser interferes with the auth flow
        ctx.response.status = 500;
        ctx.response.body = "Failed to get access token! Please try again.";
        return;
      }
      // Redirect to user auth endpoint, to get user's online token
      ctx.redirect(`/auth?shop=${shop}`);
    },
  })
);

// User auth route (get online session token)
server.use(
  createShopifyAuth({
    accessMode: "online",
    authPath: "/auth",
    async afterAuth(ctx) {
      const { shop } = ctx.state.shopify;
      // Check if the app is installed
      // NOTE: You can replace with your own function to check if the shop is installed, or you can just remove it, but this is an extra check that can help prevent auth issues
      if (isShopActive(shop)) {
        // Redirect to app
        ctx.redirect(`/?shop=${shop}`);
      } else {
        // Redirect to installation endpoint to get permanent access token
        ctx.redirect(`/install/auth/?shop=${shop}`);
      }
    },
  })
);
Comments
  • The host is undefined

    The host is undefined

    Since moving to simple-koa-shopify-auth the host is undefined in _app.js which is create an infinite loop in the console (hangs the tab)

    MyApp.getInitialProps = async ({ ctx }) => {
      let host = ctx.query.host;
      console.log(`→ ctx.query.host is ${host}`); //undefined
      return {
        host,
      };
    };
    
    opened by redochka 12
  • Session creation is not working

    Session creation is not working

    Discussion started at: https://github.com/Shopify/koa-shopify-auth/issues/134

    When using simple-koa-shopify-auth the session is not created, as if my CustomSessionStorage is never called. It was working fine before switching to simple-koa-shopify-auth

    app.prepare().then(async () => {
      const redisStoreAccessToken = await RedisStoreAccessToken();
    
      // Create a new instance of the custom storage class
      const sessionStorage = await RedisStoreSession();
    
    
      Shopify.Context.initialize({
        API_KEY        : process.env.SHOPIFY_API_KEY,
        API_SECRET_KEY : process.env.SHOPIFY_API_SECRET,
        SCOPES         : process.env.SCOPES.split(","),
        HOST_NAME      : process.env.HOST.replace(/https:\/\//, ""),
        API_VERSION    : ApiVersion.October20,
        IS_EMBEDDED_APP: true,
        // This should be replaced with your preferred storage strategy
        // SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
        SESSION_STORAGE: new Shopify.Session.CustomSessionStorage(
          sessionStorage.storeCallback,
          sessionStorage.loadCallback,
          sessionStorage.deleteCallback
        )
      });
    
      const server = new Koa();
      const router = new Router();
      server.keys  = [Shopify.Context.API_SECRET_KEY];
      server.use(
        createShopifyAuth({
          async afterAuth(ctx) {
            console.log("inside afterAuth");
    
            // Access token and shop available in ctx.state.shopify
            const { shop, accessToken, scope } = ctx.state.shopify;
            const host                         = ctx.query.host;
    
            await redisStoreAccessToken.storeCallback({
              id: shop,
              shop,
              scope,
              accessToken
            });
    
            registerUninstallWebhooks(shop, accessToken, async (topic, shop, body) => redisStoreAccessToken.deleteCallback(shop)).then();
    
            // Redirect to app with shop parameter upon auth
            ctx.redirect(`/?shop=${shop}&host=${host}`);
          }
        })
      );
    
      router.post("/my-rest", verifyRequest({ returnHeader: true }), async (ctx) => {
        try {
          ctx.body = "response from myRest endpoint!";
        } catch (error) {
          ctx.body = "✘ something wrong in myRest endpoint!";
        }
      });
      
    });
    

    package.json

    "simple-koa-shopify-auth": "^1.0.4",
    "@shopify/shopify-api": "^2.0.0",
    

    It seems that the loadCallback is not called

    Any clue?

    opened by redochka 9
  • Add the ability to specify the host of the redirect url instead of ctx.host

    Add the ability to specify the host of the redirect url instead of ctx.host

    I'm having the same issue as this person here on the original koa-shopify-auth and so am suggesting the exact same thing: https://github.com/Shopify/koa-shopify-auth/issues/11

    Overview The developer should be able to define the host of the redirect URL to be https://{Host} instead of taking the host from context (ctx).

    https://github.com/TheSecurityDev/simple-koa-shopify-auth/blob/e113d012a114b9ec3230f687b5ed51cd33a0490f/src/top-level-oauth-redirect.ts#L36

    When running createShopifyAuth() there could be an extra optional parameter, host, which if set the above function will use the provided host instead of ctx.host, which can be wrong when using proxies.

    opened by regexj 7
  • "Internal Server Error" message when first time installed an app, but app works fine after reload page.

    When I install an app using this package for auth, it shows message "Internal Server Error" MicrosoftTeams-image

    But after this message, when reloaded the page, the app works fine. This problem is consistent accross any store when installed an app for the first time. I pulled up the logs from the server and this is what I found. image

    Not sure if this is an issue with this library, but I am not accessing any id property on my createShopifyAuth call.

    opened by FourLineCode 4
  • Latest version (2.1.4) doesn't use the host param when calling createApp in top-level-oauth-redirect.ts

    Latest version (2.1.4) doesn't use the host param when calling createApp in top-level-oauth-redirect.ts

    Noticed that this was solved with https://github.com/TheSecurityDev/simple-koa-shopify-auth/commit/f7aff2a9e5eec31564503e571b89ef89e97a02b9

    Can you please say when you going to create a new release? Shopify required us to adapt to the new admin.shopify.com host scheme and it looks like this issue is a blocker for us atm.

    Thank you very much!

    opened by eyal0s 3
  • Small fix to session.isActive() inside verify-request.ts

    Small fix to session.isActive() inside verify-request.ts

    First of all big thanks for creating this repo 👍 It's a shame Shopify is deprecating the original.

    While trying out this repo I encountered an error, "session.isActive() is not a function". I use Mongodb as my custom session storage and realized isActive() does not save into the session object (in my case I'm using JSON.stringify() to save my session object, as per shopify-node-api's documentation, which doesn't serialize methods) and so will be undefined when called after loading the session.

    The method is being called here.

    // original
    if (session) {
      // Verify session is valid
      if (session.isActive())
    ...
    

    I replaced this method call with the actual business logic from Shopify's own code used for checking if the session is active, which removes the responsibility from the session object.

    // fix
    if (session) {
      // Verify session is valid
      if (
        Shopify.Context.SCOPES.equals(session.scope) && 
        session.accessToken &&
        (!session.expires || session.expires >= new Date())
        ) {
        ...
    
    opened by limeforadime 3
  • Reauth on Client Side

    Reauth on Client Side

    In the docs there is this line: For requests from the frontend, we want to return headers, so we can check if we need to reauth on the client side

    Do you have an example of what reauthing on the client side would look like?

    All our reauths happen server side: ctx.redirect(/online/auth/?shop=${shop}). However, I'd love to figure out how reauth client side as well.

    EDIT:

    For a little more clarity, I recognize that authenticatedFetch from the node boilerplate should assist with redirects. However, I'm having trouble getting it to work with the Axios interceptor example from the docs. I'm able to successfully make API calls using authAxios. However, I'm having trouble when the online access token expires (no reauth). The main goal is to force a redirect client side if the online access token is expired.

    _App.js

    function userLoggedInFetch(app) {
     const fetchFunction = authenticatedFetch(app);
    
     return async (uri, options) => {
       const response = await fetchFunction(uri, options);
    
       if (
         response.headers.get("X-Shopify-API-Request-Failure-Reauthorize") === "1"
       ) {
         const authUrlHeader = response.headers.get(
           "X-Shopify-API-Request-Failure-Reauthorize-Url"
         );
    
         const redirect = Redirect.create(app);
         redirect.dispatch(Redirect.Action.APP, authUrlHeader || `/auth`);
         return null;
       }
    
       return response;
     };
    }
    
    function MyProvider(props) {
     const app = useAppBridge();
    
     const client = new ApolloClient({
       fetch: userLoggedInFetch(app),
       fetchOptions: {
         credentials: "include",
       },
     });
    
     // Create axios instance for authenticated request
     const authAxios = axios.create();
     // intercept all requests on this axios instance
     authAxios.interceptors.request.use(function (config) {
       return getSessionToken(app).then((token) => {
         let decoded = jwt_decode(token);
    
         // append your request headers with an authenticated token
         config.headers["Authorization"] = `Bearer ${token}`;
    
         config.params["shopId"] = decoded.dest.replace(/(^\w+:|^)\/\//, "");
    
         return config;
       });
     });
    
     const Component = props.Component;
    
     return (
       <ApolloProvider client={client}>
         <Component {...props} authAxios={authAxios} app={app} />
       </ApolloProvider>
     );
    }
    
    class MyApp extends App {
     render() {
       const { Component, pageProps, host } = this.props;
       return (
         <AppProvider i18n={translations}>
           <Provider
             config={{
               apiKey: API_KEY,
               host: host,
               forceRedirect: true,
             }}
           >
             <RoutePropagator />
             <MyProvider Component={Component} {...pageProps} />
           </Provider>
         </AppProvider>
       );
     }
    }
    
    
    opened by PurplePineapple123 2
  • App bridge error after redirect in pages

    App bridge error after redirect in pages

    i have setup simple koa shopify auth first time its working properly after redirect in side page getting below error

    TypeError: History.create is not a function

    Here is my server.js code

    require('isomorphic-fetch');
    
    const dotenv = require('dotenv');
    const next = require('next');
    
    // Koa-related
    const http = require('http');
    const Koa = require('koa');
    const cors = require('@koa/cors');
    const socket = require('socket.io');
    const bodyParser = require('koa-bodyparser');
    const vhost = require('koa-virtual-host');
    
    
    const { createShopifyAuth, verifyRequest } = require("simple-koa-shopify-auth");
    
    const { default: Shopify, ApiVersion } = require('@shopify/shopify-api');
    
    
    const session = require('koa-session');
    const { default: graphQLProxy } = require('@shopify/koa-shopify-graphql-proxy');
    const Router = require('koa-router');
    
    
    // Server-related
    const registerShopifyWebhooks = require('./server/registerShopifyWebhooks');
    const getShopInfo = require('./server/getShopInfo');
    const { configureURLForwarderApp } = require('./routes/urlForwarderApp');
    
    // Mongoose-related
    const mongoose = require('mongoose');
    const Shop = require('./models/shop');
    
    // Routes
    const combinedRouters = require('./routes');
    const jobRouter = require('./routes/jobs');
    
    // Twilio-related
    const twilio = require('twilio');
    
    // Mixpanel
    const Mixpanel = require('mixpanel');
    const { eventNames } = require('./constants/mixpanel');
    const { createDefaultSegments } = require('./modules/segments');
    const { createDefaultTemplates } = require('./modules/templates');
    const { createDefaultAutomations } = require('./modules/automations');
    
    // Server Files
    const { registerJobs } = require('./server/agenda');
    const { EventsQueueConsumer } = require('./server/jobs/eventsQueueConsumer');
    const getShopOrderCount = require('./server/getShopOrderCount');
    
    
    const jwt_decode = require("jwt-decode");
    
    // Access env variables
    dotenv.config();
    
    // Constants
    const {
      APP_VERSION_UPDATE_DATE_RAW,
      MONGODB_URI,
      PROD_SHOPIFY_API_KEY,
      PROD_SHOPIFY_API_SECRET_KEY,
      STAGING_SHOPIFY_API_KEY,
      STAGING_SHOPIFY_API_SECRET_KEY,
      TWILIO_ACCOUNT_SID,
      TWILIO_AUTH_TOKEN,
      TWILIO_PHONE_NUMBER,
      TWILIO_TOLL_FREE_PHONE_NUMBER,
      URL_FORWARDER_HOST,
      QA_MIXPANEL_TOKEN,
      PROD_MIXPANEL_TOKEN,
      ENABLE_JOBS,
      ENABLE_QUEUE_CONSUMERS,
    } = process.env;
    
    // Server initialization
    const port = parseInt(process.env.PORT, 10) || 3000;
    const dev = process.env.NODE_ENV !== 'production';
    const app = next({ dev });
    const handle = app.getRequestHandler();
    
    
    const SHOPIFY_API_KEY = dev ? STAGING_SHOPIFY_API_KEY : PROD_SHOPIFY_API_KEY;
    const SHOPIFY_API_SECRET_KEY = dev
      ? STAGING_SHOPIFY_API_SECRET_KEY
      : PROD_SHOPIFY_API_SECRET_KEY;
    const APP_VERSION_UPDATE_DATE = new Date(APP_VERSION_UPDATE_DATE_RAW);
    const MIXPANEL_TOKEN = dev ? QA_MIXPANEL_TOKEN : PROD_MIXPANEL_TOKEN;
    
    
    
    // Mongo DB set up
    mongoose.connect(MONGODB_URI, { useNewUrlParser: true });
    mongoose.set('useFindAndModify', false);
    const db = mongoose.connection;
    db.on('error', console.error.bind(console, 'MongoDB connection error:'));
    
    
    // initializes the library
    Shopify.Context.initialize({
      API_KEY: process.env.PROD_SHOPIFY_API_KEY,
      API_SECRET_KEY: process.env.PROD_SHOPIFY_API_SECRET_KEY,
      SCOPES: [
        'read_orders',
        'write_orders',
        'read_products',
        'write_products',
        'read_customers',
        'write_customers',
        'write_draft_orders',
        'read_draft_orders',
        'read_script_tags',
        'write_script_tags',
      ],
      HOST_NAME: process.env.PROD_SERVER_URL.replace(/https:\/\//, ''),
      API_VERSION: ApiVersion.April22,
      IS_EMBEDDED_APP: true,
      SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
    });
    
    const ACTIVE_SHOPIFY_SHOPS = {};
    
    app.prepare().then(() => {
      const router = new Router();
      const server = new Koa();
    
      //const {shop, accessToken} = ctx.state.shopify;
    
      server.use(session({ secure: true, sameSite: 'none' }, server));
      const httpServer = http.createServer(server.callback());
      const io = socket(httpServer);
      server.context.io = io;
    
      // Start queue consumers if required
      if (ENABLE_QUEUE_CONSUMERS == 'true') {
        EventsQueueConsumer.run().catch((err) => {
          console.log(`Error running Events Queue Consumer: ${err}`);
        });
      }
    
      // Bind mixpanel to server context
      const mixpanel = Mixpanel.init(MIXPANEL_TOKEN);
      server.context.mixpanel = mixpanel;
    
      // Twilio config
      const twilioClient = new twilio(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);
      const twilioSend = (body, to, from, mediaUrl = []) => {
        return twilioClient.messages
          .create({
            body,
            to,
            from: from || TWILIO_PHONE_NUMBER,
            mediaUrl,
          })
          .then((twilioMessagesRes) => {
            return { delivered: true };
          })
          .catch((twilioMessagesErr) => {
            console.log('Error sending twilio message from API: twilioMessagesErr');
            return { delivered: false, error: twilioMessagesErr };
          });
      };
      server.context.twilioSend = twilioSend;
    
      io.on('connection', function (socket) {
        socket.on('registerShop', () => {
          let ctx = server.createContext(
            socket.request,
            new http.OutgoingMessage()
          );
          //const { shop } = ctx.session;
    
          const shop = ctx.query.shop;
    
          //const {shop, accessToken} = ctx.state.shopify;
    
    
          Shop.findOne({ shopifyDomain: shop }).then((userShop) => {
            if (userShop) {
              socket.join(`shop:${userShop._id}`);
    
              // Initialize mixpanel user
              ctx.mixpanel.people.set(userShop._id, {
                $first_name: userShop.shopifyDomain,
                $last_name: '',
                shopifyDomain: userShop.shopifyDomain,
              });
            }
          });
        });
      });
    
      server.keys = [SHOPIFY_API_SECRET_KEY];
    
      if (!URL_FORWARDER_HOST) {
        console.warn('URL_FORWARDER_HOST is not set and will not function.');
      } else {
        server.use(vhost(URL_FORWARDER_HOST, configureURLForwarderApp()));
      }
    
      // Initiate agenda for jobs
      if (ENABLE_JOBS === 'true') {
        (async function () {
          await registerJobs();
        })();
      }
    
      server.use(cors());
    
    
      console.log('step1', SHOPIFY_API_KEY, 'step2', SHOPIFY_API_SECRET_KEY);
    
       server.use(
        createShopifyAuth({
            apiKey: SHOPIFY_API_KEY,
            secret: SHOPIFY_API_SECRET_KEY,
            accessMode: 'offline',
            scopes: [
              'read_orders',
              'write_orders',
              'read_products',
              'write_products',
              'read_customers',
              'write_customers',
              'write_draft_orders',
              'read_draft_orders',
              'read_script_tags',
              'write_script_tags',
            ],
            async afterAuth(ctx) {
    
            const {shop, accessToken} = ctx.state.shopify;
    
    
    
    
           ACTIVE_SHOPIFY_SHOPS[shop] = true;
    
    
            // Register Shopify webhooks
            await registerShopifyWebhooks(accessToken, shop);
    
            // Gather base shop info on shop
            const [shopInfo, orderCount] = await Promise.all([
              getShopInfo(accessToken, shop),
              getShopOrderCount(accessToken, shop),
            ]);
    
            // If user doesn't already exist, hasn't approved a subscription,
            // or does not have the latest app version, force them to approve
            // the app payment/usage pricing screen
            let userShop = await Shop.findOne({ shopifyDomain: shop });
            const shouldUpdateApp =
              !userShop ||
              !userShop.appLastUpdatedAt ||
              new Date(userShop.appLastUpdatedAt) < APP_VERSION_UPDATE_DATE;
    
            const existingShopName =
              userShop && userShop.shopName
                ? userShop.shopName
                : shopInfo && shopInfo.name;
    
            // Load 25 cents into new accounts
            if (!userShop) {
              // Track new install
              userShop = await Shop.findOneAndUpdate(
                { shopifyDomain: shop },
                {
                  accessToken: accessToken,
                  appLastUpdatedAt: new Date(),
                  shopSmsNumber: TWILIO_PHONE_NUMBER,
                  tollFreeNumber: TWILIO_TOLL_FREE_PHONE_NUMBER,
                  smsNumberIsPrivate: false,
                  loadedFunds: 0.5,
                  shopName: existingShopName,
                  quarterlyOrderCount: orderCount,
                  guidedToursCompletion: {
                    dashboard: null,
                    conversations: null,
                    templates: null,
                    subscribers: null,
                    segments: null,
                    campaigns: null,
                    analytics: null,
                    automations: null,
                  },
                },
                { new: true, upsert: true, setDefaultsOnInsert: true }
              ).exec();
              ctx.mixpanel.track(eventNames.INSTALLED_APP, {
                distinct_id: userShop._id,
                shopifyDomain: shop,
                accessToken,
                shopSmsNumber: TWILIO_PHONE_NUMBER,
                tollFreeNumber: TWILIO_TOLL_FREE_PHONE_NUMBER,
              });
    
              // Create default templates and segments
              createDefaultSegments(userShop);
              createDefaultTemplates(userShop);
            } else if (shouldUpdateApp) {
              const existingShopSmsNumber =
                userShop.shopSmsNumber || TWILIO_PHONE_NUMBER;
              const existingTollFreeSmsNumber =
                userShop.tollFreeNumber || TWILIO_TOLL_FREE_PHONE_NUMBER;
              const existingNumberIsPrivate = !!userShop.smsNumberIsPrivate;
              userShop = await Shop.findOneAndUpdate(
                { shopifyDomain: shop },
                {
                  accessToken: accessToken,
                  appLastUpdatedAt: new Date(),
                  shopSmsNumber: existingShopSmsNumber,
                  tollFreeNumber: existingTollFreeSmsNumber,
                  smsNumberIsPrivate: existingNumberIsPrivate,
                  shopName: existingShopName,
                  quarterlyOrderCount: orderCount,
                },
                { new: true, upsert: true, setDefaultsOnInsert: true }
              ).exec();
            } else {
              userShop = await Shop.findOneAndUpdate(
                { shopifyDomain: shop },
                {
                  accessToken: accessToken,
                  shopName: existingShopName,
                  quarterlyOrderCount: orderCount,
                },
                { new: true, upsert: true, setDefaultsOnInsert: true }
              ).exec();
            }
    
            ctx.mixpanel &&
              ctx.mixpanel.people.set(shop._id, {
                quarterlyOrders: orderCount,
              });
    
            // Redirect user to app home page
            //ctx.redirect('/');
            ctx.redirect(`/?shop=${shop}`);
          },
        })
      );
      // Milind Changes
      server.use(async (ctx, next) => {
        const shop = ctx.query.shop;
        console.log('Milind', 'step1', shop, `frame-ancestors https://${shop} https://admin.shopify.com;`);
        ctx.set('Content-Security-Policy', `frame-ancestors https://${shop} https://admin.shopify.com;`);
        await next();
      });
    
      server.use(graphQLProxy({ version: ApiVersion.April22 }));
      server.use(bodyParser());
    
      server.use(jobRouter.routes());
    
    
    
    
      router.get('/healthcheck', async (ctx) => {
    
    
        var decoded = jwt_decode(ctx.req.headers.authorization.replace('Bearer ', ''));
    
    
    
        //const { shop } = decoded.dest.replace('https://', '');
    
    
        ctx.res.statusCode = 200;
        ctx.body = { decoded };
        return { decoded };
      });
    
      router.get('/', async (ctx) => {
         const shop = ctx.query.shop;
    
        //const {shop, accessToken} = ctx.state.shopify;
    
        const userShop = await Shop.findOne({ shopifyDomain: shop });
      
        // If this shop hasn't been seen yet, go through OAuth to create a session
        if (ACTIVE_SHOPIFY_SHOPS[shop] === undefined) {
          ctx.redirect(`/auth?shop=${shop}`);
        } else {
          // Load app skeleton. Don't include sensitive information here!
         // ctx.body = '🎉';
    
    
          //ctx.body = { userShop };
        
    
              if (!userShop.email || !userShop.country || !userShop.adminPhoneNumber) {
                
                //ctx.body = '🎉';
                app.render(ctx.req, ctx.res, '/welcome', ctx.query);
              } else if (
                !userShop.onboardingVersionCompleted ||
                userShop.onboardingVersionCompleted < 1.0
              ) {
                app.render(ctx.req, ctx.res, '/onboarding/get-started', ctx.query);
              } else {
                await handle(ctx.req, ctx.res);
              }
    
    
        }
      });
    
    
      router.get('*', async (ctx) => {
        //router.get('*', async (ctx) => {
        if (ctx.url.includes('/?')) {
          if (ctx.url.substring(0, 2) != '/?') {
            ctx.url = ctx.url.replace('/?', '?'); // Remove trailing slash before params
            ctx.url = ctx.url.replace(/\/\s*$/, ''); // Remove trailing slash at the end
          }
        }
    
        await handle(ctx.req, ctx.res);
        ctx.respond = false;
        ctx.res.statusCode = 200;
      });
    
      server.use(combinedRouters());
    
      server.use(router.allowedMethods());
      server.use(router.routes());
    
      httpServer.listen(port, () => {
        console.log(`> Ready on http://localhost:${port}`);
      });
    });
    

    Page code

    import {
      Banner,
      Button,
      Card,
      FormLayout,
      Layout,
      Page,
      Select,
      Spinner,
      TextField,
    } from '@shopify/polaris';
    import { Context } from '@shopify/app-bridge-react';
    import { History } from '@shopify/app-bridge/actions';
    import countryList from 'country-list';
    import {
      getAreaCodeForCountry,
      isValidEmail,
      isValidPhoneNumber,
    } from '../modules/utils';
    import mixpanel from '../modules/mixpanel';
    import { eventNames, eventPages } from '../constants/mixpanel';
    
    import createApp from "@shopify/app-bridge";
    import { authenticatedFetch } from "@shopify/app-bridge-utils";
    import { Redirect } from "@shopify/app-bridge/actions";
    
    
    import userLoggedInFetch from '../utils/userLoggedInFetch';
    
    
    class Welcome extends React.Component {
      static contextType = Context;
    
      state = {
        emailAddress: '',
        country: '',
        adminPhoneNumber: '',
        formError: false,
        screenWidth: 0,
        loading: false,
      };
    
      componentDidMount() {
        console.log("Milind", this.context);
        const app = this.context;
        const history = History.create(app);
        history.dispatch(History.Action.PUSH, `/welcome`);
    
        this.setState({
          app: app,
        });
    
        //this.getShopifyStoreInfo();
        this.updateWindowDimensions();
        window.addEventListener('resize', this.updateWindowDimensions);
      }
    
      componentWillUnmount() {
        window.removeEventListener('resize', this.updateWindowDimensions);
      }
    
      updateWindowDimensions = () => {
        this.setState({ screenWidth: window.innerWidth });
      };
    
      getShopifyStoreInfo = () => {
    
        fetch(`${SERVER_URL}/get-shopify-store-info`)
          .then(response => {
            if (response.headers.get("X-Shopify-API-Request-Failure-Reauthorize") === "1") {
              const authUrlHeader = response.headers.get("X-Shopify-API-Request-Failure-Reauthorize-Url");
        
              const redirect = Redirect.create(app);
              redirect.dispatch(Redirect.Action.APP, authUrlHeader || `/auth`);
              return null;
              }else{
        
                return response.json();
        
        
              }
    
          })
          .then((data) => {
            this.setState({
              emailAddress: data.email || '',
              adminPhoneNumber: data.phone || '',
              country: data.country || '',
            });
          });
      };
    
      handleEmailAddressChange = (emailAddress) => {
        this.setState({ emailAddress });
      };
    
      handleCountryChange = (country) => {
        this.setState({ country });
      };
    
      handleAdminPhoneNumber = (adminPhoneNumber) => {
        this.setState({ adminPhoneNumber });
      };
    
      saveAdminInformation = () => {
        if (
          !isValidEmail(this.state.emailAddress) ||
          !this.state.country ||
          !isValidPhoneNumber(this.state.adminPhoneNumber, this.state.country)
        ) {
          this.setState({ formError: true });
          return;
        }
    
        this.setState({ loading: true });
    
        const fetch = userLoggedInFetch(this.state.app);
    
        // Save admin information
        fetch(`${SERVER_URL}/save-admin-information`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            email: this.state.emailAddress,
            country: this.state.country,
            adminPhoneNumber: this.state.adminPhoneNumber,
          }),
        })
        .then((response) => {
          return response.json();
        })
          .then((data) => {
    
    
            if (data.email && data.country && data.adminPhoneNumber) {
              mixpanel.track(eventNames.CONFIRMED_ADMIN_INFORMATION, {
                page: eventPages.ONBOARDING,
                email: data.email,
                country: data.country,
                adminPhoneNumber: data.adminPhoneNumber,
                shopifyDomain: data.shopifyDomain,
              });
              this.setState({ loading: false });
              // Redirect to next page
             // window.location.assign(`/`);
    
             const redirect = Redirect.create(this.state.app);
             redirect.dispatch(Redirect.Action.APP, `/?shop=`+data.shopifyDomain);
    
    
    
            } else {
              this.setState({ formError: true, loading: false });
              return;
            }
          });
      };
    
      render() {
        return (
          <Page>
             {this.state.formError && (
              <Banner title="Error - missing fields" status="critical">
                <p>
                  Please make sure all fields are completed correctly before
                  proceeding.
                </p>
              </Banner>
            )}
    <Layout>
              <Layout.Section>
                <div
                  style={{
                    display: 'flex',
                    flexDirection: 'column',
                    alignItems: 'center',
                  }}
                >
                  <div
                    style={{
                      textAlign: 'center',
                      margin: '50px 0px',
                    }}
                  >
                    <div
                      style={{
                        fontWeight: '600',
                        fontSize: '24px',
                        marginBottom: '16px',
                        lineHeight: '30px',
                      }}
                    >
                      <div style={{ fontSize: '48px' }}>👋</div>
                      <br />
              
                    </div>
                    <div
                      style={{
                        fontSize: '16px',
                      }}
                    >
                   
           
                    </div>
                  </div>
                  <div
                    style={{
                      display: 'flex',
                      alignItems: 'center',
                    }}
                  >
                    {this.state.screenWidth > 1098 && (
                      <div
                        style={{
                          width: '250px',
                          marginRight: '50px',
                          lineHeight: '24px',
                          fontWeight: '600',
                        }}
                      >
                        <span style={{ fontSize: '36px' }}>💌</span>
                        <br />
                    
                      </div>
                    )}
                    <div
                      style={{
                        minWidth: '400px',
                        maxWidth: '400px',
                      }}
                    >
                      <Card sectioned>
                        <FormLayout onSubmit={() => {}}>
                          <TextField
                            label="Email address"
                            labelHidden
                            placeholder="Personal email address"
                            inputMode="email"
                            type="email"
                            onChange={this.handleEmailAddressChange}
                            value={this.state.emailAddress}
                          />
                          <Select
                            label="Country"
                            labelHidden
                            options={countryList.getNames().map((c) => {
                              return { label: c, value: c };
                            })}
                            onChange={this.handleCountryChange}
                            value={this.state.country}
                          />
                          <TextField
                            label="Phone number"
                            labelHidden
                            placeholder="Personal mobile number"
                            inputMode="tel"
                            type="tel"
                            prefix={
                              this.state.country
                                ? `+${getAreaCodeForCountry(this.state.country)}`
                                : ''
                            }
                            onChange={this.handleAdminPhoneNumber}
                            value={this.state.adminPhoneNumber}
                          />
                          {this.state.loading ? (
                            <div style={{ textAlign: 'center' }}>
                              <Spinner
                                accessibilityLabel="Send text spinner"
                                size="small"
                                color="teal"
                              />
                            </div>
                          ) : (
                            <Button
                              fullWidth
                              primary
                              onClick={this.saveAdminInformation}
                            >
                              Confirm
                            </Button>
                          )}
                        </FormLayout>
                      </Card>
                    </div>
                    {this.state.screenWidth > 1098 && (
                      <div
                        style={{
                          width: '250px',
                          marginLeft: '50px',
                          textAlign: 'left',
                          lineHeight: '24px',
                          fontWeight: '600',
                        }}
                      >
                    
                        <br />
         
                        <br />
                        <br />
                        <br />
    
                        <br />
                
                      </div>
                    )}
                  </div>
                  {this.state.screenWidth <= 1098 && (
                    <div
                      style={{
                        width: '400px',
                        textAlign: 'center',
                        lineHeight: '24px',
                        fontWeight: '600',
                        margin: '50px 0px',
                      }}
                    >
    
                 
                      <br />
           
                      <br />
             
                      <br />
                      <br />
    
                      <br />
              
                    </div>
                  )}
                </div>
              </Layout.Section>
            </Layout>
          </Page>
        );
      }
    }
    
    export default Welcome;
    
    opened by coreway1 1
  • Supporting latest version of `@shopify/shopify-api` (version 6)

    Supporting latest version of `@shopify/shopify-api` (version 6)

    Are there plans to update simple-koa-shopify-auth to support the latest version of @shopify/shopify-api (version 6)? There have been quite a few breaking changes as part of this upgrade: https://github.com/Shopify/shopify-api-js/blob/main/docs/migrating-to-v6.md.

    opened by brendenpalmer 1
  • Getting Context Initialize error

    Getting Context Initialize error

    Hi I was using koa auth for shopify, now since its depreciated, tried this library, first i was running koa auth on node 10.18.0 , on that , installing it gave me a error on redis, so upgraded node to 16.x and npm run build and npm start works fine, but when accessing the app, it crashes with Error: Context has not been properly initialized. Please call the .initialize() method to setup your app context object. I am very new to node js , pls help, below is my code in server.js

    /** require('isomorphic-fetch'); const dotenv = require('dotenv'); const Koa = require('koa'); const next = require('next'); //const {default: createShopifyAuth} = require('@shopify/koa-shopify-auth'); //const {verifyRequest} = require('@shopify/koa-shopify-auth'); const session = require('koa-session');

    dotenv.config();

    const port = parseInt(process.env.PORT, 10) || 3000; const dev = process.env.NODE_ENV !== 'production'; const app = next({dev}); const handle = app.getRequestHandler();

    const {SHOPIFY_API_SECRET_KEY, SHOPIFY_API_KEY} = process.env; const { createShopifyAuth } = require("simple-koa-shopify-auth"); const { verifyRequest } = require("simple-koa-shopify-auth"); const verifyApiRequest = verifyRequest({ returnHeader: true }); const verifyPageRequest = verifyRequest(); app.prepare().then(() => { const server = new Koa();

    server.use(session({ secure: true, sameSite: 'none' }, server));
    server.keys = [SHOPIFY_API_SECRET_KEY];
    
    server.use(
            createShopifyAuth({
                apiKey: SHOPIFY_API_KEY,
                secret: SHOPIFY_API_SECRET_KEY,
                scopes: ['read_orders,write_orders,read_shipping,read_products,write_products'],
                 accessMode: 'offline',
                   authPath: "/auth",  // Where to redirect if the session is invalid
    
                async afterAuth(ctx) {
                    const {shop, accessToken} = ctx.state.shopify;
              //      ctx.cookies.set('shopOrigin', shop, {httpOnly: false});
              //       ctx.cookies.set('shopOrigin', shop, {httpOnly: false,secure: true,sameSite: 'none'});
    
                    const axios = require('axios');
                    const https = require('https');
                    // return this promise
                    const agent = new https.Agent({
                        rejectUnauthorized: false
                    });
                    await axios.get('https://qikink.com/erp2/index.php/login/shopify?json=1&shop=' + shop + "&accesstoken=" + accessToken, {httpsAgent: agent}).then((response) => {
                       
                      //  console.log('got response' + res);
                      
                        if (response['data'].error) {
                            console.log("Below is the error");
                         console.log(response['data'].error);
                           return   ctx.redirect("/");
                            // ctx.redirect("/");
                           // return  ctx.redirect('https://qikink.com/erp2/index.php/login/shopify?json=0&shop=' + shop + "&accesstoken=" + accessToken);
                        } else {
                                  console.log("No Error");
                            console.log(response);
                            return   ctx.redirect("/");
                        }
    
                    });
    
                    console.log("why coming here");
    
    
    
                },
            }),
            );
    
    server.use(verifyRequest());
    server.use(async (ctx) => {
        await handle(ctx.req, ctx.res);
        ctx.respond = false;
        ctx.res.statusCode = 200;
        return;
    });
    
    server.listen(port, () => {
        console.log(`> Ready on http://localhost:${port}`);
    });
    

    });

    *//

    opened by samuel845 4
  • "The app couldn't be loaded" error

    I am following the example mentioned in the readme, but I get this when trying to install or access the app

    Screenshot 2022-03-09 at 22 38 54

    I can see that the online auth path is hit, and it redirects with

    ctx.redirect(/install/auth/?shop=${shop});

    From web server logs, I can observe that this is then hit : "GET /auth/callback?code=aeccdc[...]"

    Any clue to what that above error indicates? Do I need to set some specific allowed redirect urls in the Shopify app setup web ui?

    thanks for any hints

    opened by hrstrand 4
Releases(v2.1.10)
Owner
David
- Security researcher / bug bounty hunter - Shopify app developer - Languages: Javascript/Typescript, Python, GDScript, Rust, React
David
Firebase adepter auth process with custom token example in Next Auth

Firebase adepter auth process with custom token example in Next Auth Example of a firebase adapter that works with firebase authentication. A firebase

Low Front 10 Oct 14, 2022
Forked from hayes0724/shopify-packer Modern development tool for Shopify using Webpack 5. Easy to extend and customize, zero build config, compatible with Slate and existing websites.

Shopify Packer Modern development tool for Shopify using Webpack 5. Easy to extend and customize, zero build config, comes with starter themes and com

Web & Mobile | eCommerce | Full-Stack Developer 4 Nov 24, 2022
Shopify Landing (Open source landing page shopify application)

SHOPIFY Open source landing page shopify application Configuration and Setup Key Features Technologies used ?? Screenshots Author License Configuratio

Gilbert Hutapea 8 May 10, 2023
Workshop contruyendo una API Rest con Node.js + Koa.js

Workshop contruyendo una API Rest con Node.js + Koa.js Tabla de Contenido Acerca de Introducción Explicación del caso de uso Ciclo de vida de las soli

Jhony Rivero 6 Apr 8, 2022
Differences between Node + Koa and Deno + Oak

Node + Koa VS Deno + Oak Differences between Node + Koa and Deno + Oak About This is a project that aims to observe the differences between a simple R

Ronald Guilherme P. dos Santos 3 Jun 28, 2022
Run REST APIs in Node.js applications frameworks (Express, Koa, Hapi and Fastify) on top of any Serverless Cloud.

?? Serverless Adapter Install | Usage | Support | Architecture | Credits Run REST APIs and other web applications using your existing Node.js applicat

Vinicius Lourenço 45 Jan 1, 2023
Zork - Unofficial TypeScript Version

Zork - Unofficial TypeScript Version Zork was directly inspired by the first text adventure game, Colossal Cave Adventure, written in 1975. Zork disti

Lucas Silva 5 Jul 31, 2022
Motionia is a lightweight simplified on demand animation library

The Ultimate & smart JS animation library! Simple fast , flexible & easy to integrate. No need to write page long CSS Keyframes that consumes a lot of

Abhi 324 Dec 30, 2022
Simplified JavaScript Jargon

Simplified JavaScript Jargon (short SJSJ) is a community-driven attempt at explaining the loads of buzzwords making the current JavaScript ecosystem i

Kitty Giraudel 2.3k Dec 30, 2022
AWS Lambda and API Gateway, simplified for Javascript

alanajs AWS Lambda and API Gateway, simplified for JavaScript About alanajs Make setting up Lambda microservices easier than ever. alanajs is a free,

OSLabs Beta 64 Aug 1, 2022
Data-driven development, simplified.

CONTRACTUAL Table of Contents 0. Brief 1. Features 2. Installation 2.1 Download Installer 2.2 Fork and Clone Repo 3. How to Use 3.1 Contract Builder 3

OSLabs Beta 50 Nov 8, 2022
A simplified list crud that adds, removes and edits items

To-do list This project is as simplified todo list crud. Built With HTML CSS JavaScript Webpack Live version See live version Getting Started To get a

Fernando Saldaña 7 Apr 2, 2022
⚡ It is a simplified database module with multiple functions that you can use simultaneously with sqlite, yaml, firebase and json.

Prisma Database Developed with ?? by Roxza ⚡ An easy, open source database ?? Installation npm i prisma.db --save yarn add prisma.db ?? Importing impo

Roxza 21 Jan 3, 2023
🐬 A simplified implementation of TypeScript's type system written in TypeScript's type system

?? HypeScript Introduction This is a simplified implementation of TypeScript's type system that's written in TypeScript's type annotations. This means

Ronen Amiel 1.8k Dec 20, 2022
A simplified jQuery date and time picker

jSunPicker A simplified jQuery date and time picker Why another Date picker? There are numerous date, time pickers out there. However, each of those l

null 1 May 31, 2022
An overly simplified Angular tool for interacting with Smart Contracts, demod at ng-conf 2022.

AngularOperator This is a sample repo and tool created for ng-conf 2022. It contains a normal Angular app designed to interact with the blockchain. It

Stephen Fluin 8 Oct 7, 2022
REST API simplified.

Deepkit REST REST API simplified npm i \ @deepkit-rest/http-extension \ @deepkit-rest/rest-core \ @deepkit-rest/rest-crud Overview DeepKit REST

DeepKit REST 8 Dec 30, 2022
Library for readable and manageable Next.js middleware

?? Next Compose Middleware This is a library for building Next.js complex middleware declaratively. You can create highly readable and manageable midd

Ibuki Kaji 14 Dec 19, 2022
Solid component and library for LiveKit (unofficial)

This package provides Solid components that makes it easier to use LiveKit in a Solid app. Inspired completely by https://github.com/livekit/livekit-r

Prince Neil Cedrick Castro 6 Mar 8, 2022