EDIT: I believe the issue is that app.use(serveStatic(PROD_INDEX_PATH));
prevents the app.use("/*",
middleware from running. I'm still working on a solution and any help would be appreciated!
My Shopify app breaks after deploying to Heroku but ONLY when clicking the provided "install" link.
For example, when I click "Install App", this is the URL that is generated: https://shopify-app.herokuapp.com/?hmac=d61597ca3ea6ca74b8bd6ea8f8bcc812b4382fad6f15434c0158bc4c3ade519a&host=dXBsaWZ0ZWQtY29tbWVyY2UtZGV2Lm15c2hvcGlmeS5jb20vYWRtaW4&shop=dev.myshopify.com×tamp=1657326911
Which skips that auth process and takes me to "This page does not exist."
So the flow is: "Click Install App" => "This page does not exist"
However, if I manually click the auth link: https://shopify-app.herokuapp.com/api/auth?shop=dev.myshopify.com
, the app successfully completes the auth process and works without any issues.
I'm using the standard server code scaffolded from the Shopify CLI.
Commenting out this section in production allows the app to partially function:
if (isProd) {
const compression = await import("compression").then(
({ default: fn }) => fn
);
const serveStatic = await import("serve-static").then(
({ default: fn }) => fn
);
app.use(compression());
app.use(serveStatic(PROD_INDEX_PATH));
}
Once the code is commented out, the generated install link is of the format: https://shopify-app.herokuapp.com/api/auth?shop=dev.myshopify.com. However, functionality throughout the app breaks upon clicking the install button.
The new flow is: "Click Install App" => "Begin Auth flow" => "Accept requested scopes" => "App breaks"
So essentially, the code snippet if(isProd)
breaks the installation link while removing it breaks the rest of the app's functionality.
More specifically, the app.use(serveStatic(PROD_INDEX_PATH));
snippet.
Really scratching my head on this one. Any ideas what's going on?
This is my server:
const USE_ONLINE_TOKENS = true;
const TOP_LEVEL_OAUTH_COOKIE = "shopify_top_level_oauth";
// @ts-ignore
const PORT = parseInt(process.env.BACKEND_PORT || process.env.PORT, 10);
const isTest = process.env.NODE_ENV === "test" || !!process.env.VITE_TEST_BUILD;
const versionFilePath = "./version.txt";
let templateVersion = "unknown";
if (fs.existsSync(versionFilePath)) {
templateVersion = fs.readFileSync(versionFilePath, "utf8").trim();
}
// TODO: There should be provided by env vars
const DEV_INDEX_PATH = `${process.cwd()}/frontend/`;
const PROD_INDEX_PATH = `${process.cwd()}/frontend/dist/`;
const DB_PATH = `${process.cwd()}/database.sqlite`;
Shopify.Context.initialize({
// @ts-ignore
API_KEY: process.env.SHOPIFY_API_KEY,
// @ts-ignore
API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
// @ts-ignore
SCOPES: process.env.SCOPES.split(","),
// @ts-ignore
HOST_NAME: process.env.HOST.replace(/https?:\/\//, ""),
// @ts-ignore
HOST_SCHEME: process.env.HOST.split("://")[0],
API_VERSION: ApiVersion.April22,
IS_EMBEDDED_APP: true,
// This should be replaced with your preferred storage strategy
// SESSION_STORAGE: new Shopify.Session.SQLiteSessionStorage(DB_PATH),
SESSION_STORAGE: new Shopify.Session.CustomSessionStorage(
storeCallback,
// @ts-ignore
loadCallback,
deleteCallback
),
USER_AGENT_PREFIX: `Node App Template/${templateVersion}`,
});
const ACTIVE_SHOPIFY_SHOPS = {};
Shopify.Webhooks.Registry.addHandler("APP_UNINSTALLED", {
path: "/api/webhooks",
webhookHandler: async (topic, shop, body) => {
// @ts-ignore
delete ACTIVE_SHOPIFY_SHOPS[shop],
//Delete unsubscribed shop and clean undefined entries
console.log("APP UNINSTALLED");
await pool.query(
`DELETE FROM shop WHERE shop_url=$1 OR shop_url='undefined' OR shop_url='' OR shop_url IS NULL`,
[shop]
);
},
});
setupGDPRWebHooks("/api/webhooks");
// export for test use only
export async function createServer(
root = process.cwd(),
isProd = process.env.NODE_ENV === "production",
billingSettings = BILLING_SETTINGS
) {
const app = express();
app.set("top-level-oauth-cookie", TOP_LEVEL_OAUTH_COOKIE);
app.set("active-shopify-shops", ACTIVE_SHOPIFY_SHOPS);
app.set("use-online-tokens", USE_ONLINE_TOKENS);
app.use(cookieParser(Shopify.Context.API_SECRET_KEY));
applyAuthMiddleware(app, {
billing: billingSettings,
});
app.post("/api/webhooks", async (req, res) => {
try {
await Shopify.Webhooks.Registry.process(req, res);
console.log(`Webhook processed, returned status code 200`);
} catch (error) {
console.log(`Failed to process webhook: ${error}`);
if (!res.headersSent) {
res.status(500).send(error.message);
}
}
});
app.use(bodyParser.json());
// All endpoints after this point will require an active session
app.use(
"/api/*",
verifyRequest(app, {
// @ts-ignore
billing: billingSettings,
})
);
//app.use("/api/test", test);
app.use("/api/sort-options", sortOptions);
app.use("/api/sort-logic", sortLogic);
app.get("/api/products-count", async (req, res) => {
const session = await Shopify.Utils.loadCurrentSession(req, res, true);
const { Product } = await import(
`@shopify/shopify-api/dist/rest-resources/${Shopify.Context.API_VERSION}/index.js`
);
const countData = await Product.count({ session });
res.status(200).send(countData);
});
app.post("/api/graphql", async (req, res) => {
try {
const response = await Shopify.Utils.graphqlProxy(req, res);
res.status(200).send(response.body);
} catch (error) {
res.status(500).send(error.message);
}
});
app.use(express.json());
app.use((req, res, next) => {
const shop = req.query.shop;
if (Shopify.Context.IS_EMBEDDED_APP && shop) {
res.setHeader(
"Content-Security-Policy",
`frame-ancestors https://${shop} https://admin.shopify.com;`
);
} else {
res.setHeader("Content-Security-Policy", `frame-ancestors 'none';`);
}
next();
});
if (isProd) {
const compression = await import("compression").then(
({ default: fn }) => fn
);
const serveStatic = await import("serve-static").then(
({ default: fn }) => fn
);
app.use(compression());
app.use(serveStatic(PROD_INDEX_PATH));
console.log(`Serving static files from ${PROD_INDEX_PATH}`);
}
app.use("/*", async (req, res, next) => {
const shop = req.query.shop;
console.log("THIS IS THE CATCHALL ROUTE")
// //CHECK TO MAKE SURE SCOPE EXISTS AND ISN'T UNDEFINED, OFFLINE SHOPS?
const shopValue = await pool.query(`SELECT * FROM shop WHERE shop_url=$1`, [
shop,
]);
if (shopValue?.rows[0]?.scope) {
ACTIVE_SHOPIFY_SHOPS[shop] = shopValue.rows[0].scope;
} else {
ACTIVE_SHOPIFY_SHOPS[shop] = undefined;
}
// Detect whether we need to reinstall the app, any request from Shopify will
// include a shop in the query parameters.
// @ts-ignore
if (app.get("active-shopify-shops")[shop] === undefined) {
res.redirect(`/api/auth?shop=${shop}`);
} else {
// res.set('X-Shopify-App-Nothing-To-See-Here', '1');
const fs = await import("fs");
console.log(`Serving static files from ${DEV_INDEX_PATH}`);
const fallbackFile = join(
isProd ? PROD_INDEX_PATH : DEV_INDEX_PATH,
"index.html"
);
res
.status(200)
.set("Content-Type", "text/html")
.send(fs.readFileSync(fallbackFile));
}
});
return { app };
}
CodePudding user response:
The solution is changing: app.use(serveStatic(PROD_INDEX_PATH));
to app.use(serveStatic(PROD_INDEX_PATH, { index: false }));
It was a bug in the CLI which was resolved here