TheSecurityDev / simple-koa-shopify-auth

An unofficial, simplified version of the @Shopify/koa-shopify-auth middleware library.
MIT License
25 stars 6 forks source link

App bridge error after redirect in pages #10

Closed coreway1 closed 2 years ago

coreway1 commented 2 years ago

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;
TheSecurityDev commented 2 years ago

Where are you getting the error? Did you read the error message and try to figure it out? It says "History.create" is not a function". So just search for that text in your page code (at the line: const history = History.create(app);). It's probably an issue with your code here.

Also, I see in your createShopifyAuth function you are passing in parameters that aren't needed. The only parameters you need are: accessMode, afterAuth.

I would recommend using Typescript and ESLint if you aren't already, to catch issues like this.