App reviews, troubleshooting, and recommendations
I have been trying to get this to work for months and i can't find anything that makes any sense.
I have set up my node app to use CustomSessionStorage with the session being stored to FireBase Firestore DB.
I can see the session being stored in the DB but the app needs re-authenticating every 24 hours or so. I read that setting accessMode: 'offline' would remove the issue of any session expiry but it hasn't made a difference.
Any advice or if i am missing something would be greatly appreciated.
Session in DB
{
"id":"offline_jaques-of-london.myshopify.com",
"shop":"jaques-of-london.myshopify.com",
"state":"105619287389410",
"isOnline":false,
"accessToken":"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",
"scope":"read_products,write_discounts,write_script_tags"
}
server.js
import "@babel/polyfill";
import dotenv from "dotenv";
import "isomorphic-fetch";
import createShopifyAuth, { verifyRequest } from "@shopify/koa-shopify-auth";
import Shopify, { ApiVersion } from "@shopify/shopify-api";
import Koa from "koa";
import next from "next";
import Router from "koa-router";
import gql from "graphql-tag";
import db from './firebase';
import moment from 'moment';
import FirebaseSessionHandler from "./firebase-session";
dotenv.config();
const port = parseInt(process.env.PORT, 10) || 8081;
const dev = process.env.NODE_ENV !== "production";
const app = next({
dev,
});
const handle = app.getRequestHandler();
const sessionHandler = new FirebaseSessionHandler();
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,
SESSION_STORAGE: new Shopify.Session.CustomSessionStorage(
sessionHandler.storeCallback,
sessionHandler.loadCallback,
sessionHandler.deleteCallback
),
});
const createDiscountCode = require('./createDiscountCode');
const QUERY_SCRIPT_TAGS = gql`
query {
scriptTags(first: 5) {
edges {
node {
id
src
displayScope
}
}
}
}
`;
// Storing the currently active shops in memory will force them to re-login when your server restarts. You should
// persist this object in your app.
const ACTIVE_SHOPIFY_SHOPS = {};
app.prepare().then(async () => {
const server = new Koa();
const router = new Router();
let setAccessToken;
let setShop;
server.keys = [Shopify.Context.API_SECRET_KEY];
server.use(
createShopifyAuth({
accessMode: 'offline',
async afterAuth(ctx) {
// Access token and shop available in ctx.state.shopify
const { shop, accessToken, scope } = ctx.state.shopify;
setAccessToken = accessToken;
setShop = shop;
const host = ctx.query.host;
ACTIVE_SHOPIFY_SHOPS[shop] = scope;
const response = await Shopify.Webhooks.Registry.register({
shop,
accessToken,
path: "/webhooks",
topic: "APP_UNINSTALLED",
webhookHandler: async (topic, shop, body) =>
delete ACTIVE_SHOPIFY_SHOPS[shop],
});
if (!response.success) {
console.log(
`Failed to register APP_UNINSTALLED webhook: ${response.result}`
);
}
// Redirect to app with shop parameter upon auth
ctx.redirect(`/?shop=${shop}&host=${host}`);
},
})
);
const handleRequest = async (ctx) => {
await handle(ctx.req, ctx.res);
ctx.respond = false;
ctx.res.statusCode = 200;
};
router.post("/webhooks", async (ctx) => {
try {
await Shopify.Webhooks.Registry.process(ctx.req, ctx.res);
console.log(`Webhook processed, returned status code 200`);
} catch (error) {
console.log(`Failed to process webhook: ${error}`);
}
});
router.post(
"/graphql",
verifyRequest({ returnHeader: true }),
async (ctx, next) => {
await Shopify.Utils.graphqlProxy(ctx.req, ctx.res);
}
);
//Get the winning ratios
router.get("/echo/prizes", async (ctx, next) => {
try {
const res = await db.collection("shopify").doc('config').get();
const json = await res.data();
ctx.body = {
status: 'success',
data: json
}
} catch (error) {
console.log(`Failed to process webhook: ${error}`);
}
});
//Check the code exists and the status
router.get("/echo/check/:code", async (ctx, next) => {
//console.log(ctx.params);
try {
const res = await db.collection("codes").doc(`${ctx.params.code}`).get();
const json = await res.data();
ctx.body = {
status: 'success',
params: ctx.params,
data: json
}
} catch (error) {
console.log(`Failed to process webhook: ${error}`);
}
});
//Create the code
router.get("/echo/create/:code/:prize", async (ctx, next) => {
let codeCode = ctx.params.code;
let codePrize = parseInt(ctx.params.prize);
let startDate = moment().utc().format();
let endDate = moment().add(48, 'hours').utc().format();
try {
const codeData = await createDiscountCode(setAccessToken, setShop, codeCode, codePrize, startDate, endDate);
ctx.body = {
status: 'success',
data: codeData,
end: moment().add(48, 'hours').format('YYYY-MM-DD HH:mm:ss')
}
//Add code to prizes
const data = {
prize: codePrize,
claimed: moment().format("DD/MM/YYYY HH:mm:ss"),
campaign: `${codeCode.slice(0, 2)}`,
};
const addPrize = await db.collection('prizes').doc(`${codeCode}`).set(data);
//Update code status
db.collection("codes").doc(`${codeCode}`).update({
'redeemed': true,
'used': moment().format("DD/MM/YYYY HH:mm:ss")
});
} catch (error) {
console.log(`Failed to process webhook: ${error}`);
}
});
//Email
router.get("/echo/email/:email/:code/:prize", async (ctx, next) => {
try {
let sendEmail = ctx.params.email;
let sendCode = ctx.params.code;
let sendPrize = ctx.params.prize;
const response = await fetch('https://hooks.zapier.com/hooks/catch/411924/bua3yph/', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: `${sendEmail}`,
latest_code: `${sendCode}`,
code_prize: `${sendPrize}`
})
}).then(function (response) {
return response.json();
});
ctx.body = {
status: 'success',
data: response
}
} catch (error) {
console.log(`Failed to process webhook: ${error}`);
}
});
router.get("(/_next/static/.*)", handleRequest); // Static content is clear
router.get("/_next/webpack-hmr", handleRequest); // Webpack content is clear
router.get("(.*)", async (ctx) => {
const shop = ctx.query.shop;
// 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 {
await handleRequest(ctx);
}
});
server.use(router.allowedMethods());
server.use(router.routes());
server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
});
firebase-session.js
import db from "./firebase";
class FirebaseSessionHandler {
async storeCallback(session) {
try {
let doc = db.collection('session').doc(session.id);
await doc.set({
session: JSON.stringify(session)
})
return true
} catch (err) {
throw new Error(err)
}
};
async loadCallback(id) {
try {
return await db.collection('session').doc(id).get().then(item => {
return JSON.parse(item.data().session);
});
} catch (err) {
throw new Error(err)
}
};
async deleteCallback(id) {
try {
await db.collection('session').doc(id).delete();
return true
} catch (err) {
throw new Error(err)
}
};
}
export default FirebaseSessionHandler
_app.js
import ApolloClient from "apollo-boost";
import { ApolloProvider } from "react-apollo";
import App from "next/app";
import { AppProvider } from "@shopify/polaris";
import { Provider, useAppBridge } from "@shopify/app-bridge-react";
import { authenticatedFetch } from "@shopify/app-bridge-utils";
import { Redirect } from "@shopify/app-bridge/actions";
import "@shopify/polaris/dist/styles.css";
import translations from "@shopify/polaris/locales/en.json";
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",
},
});
const Component = props.Component;
return (
<ApolloProvider client={client}>
<Component {...props} />
</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,
}}
>
<MyProvider Component={Component} {...pageProps} />
</Provider>
</AppProvider>
);
}
}
MyApp.getInitialProps = async ({ ctx }) => {
return {
host: ctx.query.host,
};
};
export default MyApp;
Solved! Go to the solution
This is an accepted solution.
Did you see this comment in the template file? This is the root of your problem -- your underlying CustomSessionStorage needs to connect to a database, and you need an independent accounting of whether or not an app has gone through the oAuth process and generated a Session / access token -- not the locally-scoped ACTIVE_SHOPIFY_SHOPS (which, as you've noted, will be wiped out on server restart)
// Storing the currently active shops in memory will force them to re-login when your server restarts.
// You should persist this object in your app.
const ACTIVE_SHOPIFY_SHOPS = {};
This is an accepted solution.
Did you see this comment in the template file? This is the root of your problem -- your underlying CustomSessionStorage needs to connect to a database, and you need an independent accounting of whether or not an app has gone through the oAuth process and generated a Session / access token -- not the locally-scoped ACTIVE_SHOPIFY_SHOPS (which, as you've noted, will be wiped out on server restart)
// Storing the currently active shops in memory will force them to re-login when your server restarts.
// You should persist this object in your app.
const ACTIVE_SHOPIFY_SHOPS = {};
Thank you for your help with this, that makes sense and is somewhat obvious now you have pointed it out. I appreciate your time.
@Brandammo could you share any hints or code for how you persisted active shopify shops in the db and checked for them?
I'm able to persist in db and check, but I'm running my check in the same app.use("/*") where the active-shopify-shops object was being set before, and it always runs twice quickly and fails the second time due to req.query being undefined... I've been searching 2 days for a solution
Hi, I have the same problem. I implemented the storeCallback, loadCallback and deleteCallback...but I can't figure out where and how to set the
Starting a B2B store is a big undertaking that requires careful planning and execution. W...
By JasonH Sep 23, 2024By investing 30 minutes of your time, you can unlock the potential for increased sales,...
By Jacqui Sep 11, 2024We appreciate the diverse ways you participate in and engage with the Shopify Communi...
By JasonH Sep 9, 2024