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;