i have created a Shopify public app using node with shopify CLI , and for payment i enabled the recurring charge API after that the major issue occurred.
So after running the code locally with the ngrok tunneling when i click on the provided Url i was successfully able to redirect to the initial oAuth page and after oAuth it will redirect to the Billing Approval page and after approval i was able to redirect to the App Dashboard.
All these Works fine when i run it locally, and the issue begins when i deployed My app in Heroku and and try to re-install it in my store.
These Screenshots are the correct work flow of my application when i run it locally with ngrok tunneling.
So after deployed in heroku i uninstalled my app in the development store to check the working in the production and after whitelisting the Url
i go to the App → Myapp → test in development store → Install app.
Here the issue comes.
After clicking the Install App it doesn’t redirect to the initial oAuth it shows like Invalid Url
it was working fine initially but the issue happens after implemented the code for recurring Billing in the server.js file
The code portion will be mentioned below
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 session from "koa-session";
import * as handlers from "./handlers/index";
import store from "store-js";
dotenv.config();
Shopify.Context.initialize({
API_KEY: process.env.SHOPIFY_API_KEY,
API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
SCOPES: process.env.SHOPIFY_API_SCOPES.toString().split(","),
HOST_NAME: process.env.HOST.toString().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(),
});
const port = parseInt(process.env.PORT, 10) || 8081;
const dev = process.env.NODE_ENV !== "production";
const app = next({
dev,
});
const handle = app.getRequestHandler();
// 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();
server.use(session({ secure: true, sameSite: "none" }, server));
server.keys = [Shopify.Context.API_SECRET_KEY];
server.use(
createShopifyAuth({
async afterAuth(ctx) {
// Access token and shop available in ctx.state.shopify
const { shop, accessToken, scope } = ctx.state.shopify;
const host = ctx.query.host;
ACTIVE_SHOPIFY_SHOPS[shop] = scope;
ctx.cookies.set("shopOrigin", shop, {
httpOnly: false,
secure: true,
sameSite: "none",
});
ctx.cookies.set("host", host, {
httpOnly: false,
secure: true,
sameSite: "none",
});
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
server.context.client = await handlers.createClient(shop, accessToken);
await handlers.getSubscriptionUrl(ctx);
},
})
);
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("/webhooks/customers/data_request", (ctx) => {
console.log("received webhook: ", ctx.state.webhook.topic);
customerDataRequest(ctx.state.webhook);
ctx.res.statusCode = 200;
});
router.post("/webhooks/customers/redact", (ctx) => {
console.log("received webhook: ", ctx.state.webhook.topic);
customerRedact(ctx.state.webhook);
ctx.res.statusCode = 200;
});
router.post("/webhooks/shops/redact", (ctx) => {
console.log("received webhook: ", ctx.state.webhook.topic);
shopRedact(ctx.state.webhook);
ctx.res.statusCode = 200;
});
router.post(
"/graphql",
verifyRequest({ returnHeader: true }),
async (ctx, next) => {
await Shopify.Utils.graphqlProxy(ctx.req, ctx.res);
}
);
router.get("(/_next/static/.*)", handleRequest); // Static content is clear
router.get("/_next/webpack-hmr", handleRequest); // Webpack content is clear
router.get("(/ARViewerScript.*)", handleRequest);
router.get("(.*)", async (ctx) => {
const shop = ctx.query.shop;
ctx.res.setHeader(
"Content-Security-Policy",
`frame-ancestors https://${shop} https://admin.shopify.com`
);
// 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);
// }
await handleRequest(ctx);
});
server.use(router.allowedMethods());
server.use(router.routes());
server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
});
App.js
import ApolloClient from "apollo-boost";
import { ApolloProvider } from "@apollo/client";
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";
import Cookies from "js-cookie";
import store from "store-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",
},
});
const Component = props.Component;
return (
);
}
class MyApp extends App {
render() {
const { Component, pageProps, host } = this.props;
let hostValue = store.get("host")
let hostVal = Cookies.get('host')
console.log("host1",hostValue)
console.log("host2",hostVal)
return (
);
}
}
// MyApp.getInitialProps = async ({ ctx }) => {
// return {
// host: ctx.Cookies.get("host")
// };
// };
export default MyApp;
get-subscriptionurl.js
import "isomorphic-fetch";
import { gql } from "apollo-boost";
export function RECURRING_CREATE(url) {
return gql`
mutation {
appSubscriptionCreate(
name: "Super Duper Plan"
returnUrl: "${url}"
test: true
lineItems: [
{
plan: {
appUsagePricingDetails: {
cappedAmount: { amount: 199, currencyCode: USD }
terms: "Basic Plan Upto 10 Products supports: $39
Upto 40 Products: $69
Upto 100 Products: $99
Upto 100 Products: $199"
}
}
}
{
plan: {
appRecurringPricingDetails: {
price: { amount: 39, currencyCode: USD }
}
}
}
]
) {
userErrors {
field
message
}
confirmationUrl
appSubscription {
id
}
}
}`;
}
export const getSubscriptionUrl = async (ctx) => {
const { client } = ctx;
const confirmationUrl = await client
.mutate({
mutation: RECURRING_CREATE(process.env.HOST),
})
.then((response) => response.data.appSubscriptionCreate.confirmationUrl);
return ctx.redirect(confirmationUrl);
};
If anyone can covey there thoughts on this and share there suggestions on this it will be awesome and very helpful.
Thanks