Questions and discussions about using the Shopify CLI and Shopify-built libraries.
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 (
<ApolloProvider client={client}>
<Component {...props} />
</ApolloProvider>
);
}
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 (
<AppProvider i18n={translations}>
<Provider
config={{
apiKey: API_KEY,
host:Cookies.get('host'),
forceRedirect: true,
}}
>
<MyProvider Component={Component} {...pageProps} />
</Provider>
</AppProvider>
);
}
}
// 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<br><br>Upto 40 Products: $69<br><br>Upto 100 Products: $99 <br><br>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
Hi @Arjun98
Did you just comment out these auth lines? Can you put them back?
// 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); // }
Henry | Social Care @ Shopify
- Was my reply helpful? Click Like to let me know!
- Was your question answered? Mark it as an Accepted Solution
- To learn more visit the Shopify Help Center or the Shopify Blog
Hi Henry
When i uncomment the code that you mentioned, it shows the following error.
initially the app will direct to the billing page and when we click on the Approve then it is not getting redirected to my app dashboard page.
It shows that site can't reach or Undefined.
The url appeared - https://undefined/admin/oauth/authorize?client_id=7db4f02ae21a1d663629a17110658006&scope=write_products%2Cwrite_script_tags&redirect_uri=https%3A%2F%2F12c5-103-160-233-158.ngrok.io%2Fauth%2Fcallback&state=986183926163707&grant_options%5B%5D=per-user
The issue will be after calling the billing function it will redirect to the confirmation url and when we click on the approve page and it come back in the server then the shop will be undefined there.
Hi @Arjun98
The issue could come from `Cookies.get('host')` is undefined from the Provider below.
```
<Provider config={{ apiKey: API_KEY, host:Cookies.get('host'), forceRedirect: true, }} >
```
There is an issue with the `host` param isn't available in query params during installation flow. In this case, can you temporarily construct the host param manually if it's not available? For example:
```
const shop = ... // get your shop here. Ex: test.myshopify.com
const host = store.get("host") || Cookies.get('host') || btoa(shop + '/admin')
```
Henry | Social Care @ Shopify
- Was my reply helpful? Click Like to let me know!
- Was your question answered? Mark it as an Accepted Solution
- To learn more visit the Shopify Help Center or the Shopify Blog
Hi Henry,
We may be running into a similar issue... Here's the call sequence for initiating recurring charge:
Request:
{"recurring_application_charge": {"name": "Standard", "price": 0, "return_url": "https://zumigostore4.myshopify.com/admin/apps/deriskify", "test": "true", "capped_amount": 500, "terms": "Name Address Phone verification\n USA Phones: USD 0.20;\n Canada Phones: USD 0.30;"}}
Response:
{"recurring_application_charge": {"id": 24549851288, "name": "Standard", "api_client_id": 6068747, "price": "0.00", "status": "pending", "return_url": "https://zumigostore4.myshopify.com/admin/apps/deriskify", "billing_on": null, "created_at": "2022-02-08T13:57:49-08:00", "updated_at": "2022-02-08T13:57:49-08:00", "test": true, "activated_on": null, "cancelled_on": null, "trial_days": 0, "capped_amount": "500.00", "trial_ends_on": null, "balance_used": 0.0, "balance_remaining": 500.0, "risk_level": 0.0, "decorated_return_url": "https://zumigostore4.myshopify.com/admin/apps/deriskify?charge_id=24549851288", "confirmation_url": "https://zumigostore4.myshopify.com/admin/charges/6068747/24549851288/RecurringApplicationCharge/conf..."}}
When the browser visits the confirmation_url, it gets a 404:
This was working fine until a few days back. Any ideas why this is failing now?
Thanks
This is also happening on my plugin now.
It has been working fine for months and now all the sudden we are getting 404 on recurring charge confirmation redirect with no changes on our end.
So it seems shopify changed something in their api and are returning a bad confirmation_url.
What they are sending in now (sections in bold should NOT be in the url):
'https://-.myshopify.com/admin/charges/278XXX7/2229XXX6490/RecurringApplicationCharge/confirm_recurring_application_charge?signature=BAh7BzoHaWRsKwhKgCwxBQA6EmF1dG9fYWN0aXZhdGVU--4caf3ecc1dd65f4f79d6e863e39fd521bXXX8341'
which results in the 'No page at this address'
What it should be (according to their own documentation, which is how I noticed what was going wrong, https://shopify.dev/api/admin-rest/2022-01/resources/recurringapplicationcharge#[post]/admin/api/202...😞
'https://-.myshopify.com/admin/charges/2229XXX6490/confirm_recurring_application_charge?signature=BAh7BzoHaWRsKwhKgCwxBQA6EmF1dG9fYWN0aXZhdGVU--4caf3ecc1dd65f4f79d6e863e39fd521bXXX8341'
Once I removed the unknown id and RecurringApplicationCharge/ from the url everything went back to working as normal. So now I have to parse this out of the confirmation_url until they fix this (or update documentation to show the changes and how to actually use it now) because this is breaking production.
Hope this helps anyone having this issue.
@bdozonos We have seen the same format returned in the confirmation_url for the past few weeks that we have logs for (API version 2021-07). The sample in the API documentation seems not to match what they return though, hope Shopify folks update the docs to minimize any confusion
Quick update - Our issue resolved itself with no changes on our end... I'd like to think it is some sort of a glitch on the Shopify end-point, the right thing would have been to return a 5XX if that was indeed an error, a 404 threw us off a bit, and we had to scramble.
@harishman1
oh that's interesting. I'm still getting 404 but that's good to know it eventually sorted itself out.
We're also running into this issue. It's causing us to be rejected from the app store when submitting V2 of our app.
Did you ever find the solution to this? I'm running into the same thing.