There’s no page at this address | After enabling the Recurring charge API in Nodejs app

Arjun98
Shopify Partner
13 0 1

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.

localrun.PNGoAuth page.PNGPayment page.PNG

 

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.

 

invalidUrl.PNG

 

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

Replies 10 (10)

Henry_Tao
Shopify Staff
91 28 15

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

Arjun98
Shopify Partner
13 0 1

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.

Siteunreached.PNG

 

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.

 

if (ACTIVE_SHOPIFY_SHOPS[shop] === undefined) {
      ctx.redirect(`/auth?shop=${shop}`);
    } else {
      await handleRequest(ctx);
    }
 

 

Henry_Tao
Shopify Staff
91 28 15

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

harishman1
Shopify Partner
5 0 3

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:

 

https://zumigostore4.myshopify.com/admin/charges/6068747/24549851288/RecurringApplicationCharge/conf...

 

harishman1_0-1644357668281.png

 

This was working fine until a few days back. Any ideas why this is failing now?

 

Thanks

 

bdozonos
Shopify Partner
10 1 2

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.

bdozonos
Shopify Partner
10 1 2

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.

harishman1
Shopify Partner
5 0 3

@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.

bdozonos
Shopify Partner
10 1 2

@harishman1 
oh that's interesting. I'm still getting 404 but that's good to know it eventually sorted itself out. 

 

Jaap_V4
Shopify Partner
6 0 1

We're also running into this issue. It's causing us to be rejected from the app store when submitting V2 of our app.

lod_dev
Shopify Partner
2 0 0

Did you ever find the solution to this? I'm running into the same thing.