New Shopify Certification now available: Liquid Storefronts for Theme Developers

Recurring billing Approval Page showing Twice in oAuth

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 in calling the App Subscription page

 

In the oAuth the server will check if the user have any active subscriptions and if not the getSubscription Url is called to activate the subscription.

 

so if the user cancel the subscription and the app is installed and if the user select the app from the shopify admin dashboard the user could get access to the app. So in order to declined the access if the user have no ACTIVE subscription we implemented a logic to check the subscription and if not show the payment page.

in

server.js

 

 

const verifyIfSubscription= async (ctx, next) => {
    const shop  = process.env.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}`);
      return;
    }
    const hasSubscription = await handlers.getAppSubscriptionStatus(ctx);
    if (!hasSubscription) {
      console.log("inside",hasSubscription)
      server.context.client = await handlers.createClient(shop, token);
      await handlers.getSubscriptionUrl(ctx);
      return;
    }else {
      return next();
    }
  };


router.get("(.*)", verifyIfSubscription, handleRequest);

 

 

 

So this cause the issue 

So because of this during the Initial oAuth of the application the payment page if appeared for the user to process the payment because the hasSubscription value will be false there.

 

But the issue is that if we click on the "Approve" button to activate the plan after the oAuth,  One more time the payment page will appear here there and the payment Approve page will appear twice during the installation.

7.PNG

 

This page will appear twice

This is because in  the  "verifyIfSubscription"  function the value of the "hasSubscription" function will initially return false so the page will appear and it show some delay in getting the "hasSubscription" to true and redirect to the App dashboard.

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 * as handlers from "./handlers/index";

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.keys = [Shopify.Context.API_SECRET_KEY];
  let token;
  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;
        token = accessToken;
        ACTIVE_SHOPIFY_SHOPS[shop] = scope;
        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
        // ctx.redirect(`/?shop=${shop}&host=${host}`);
        //server.context.client = await handlers.createClient(shop, accessToken);
        //await handlers.getSubscriptionUrl(ctx);
        server.context.client = await handlers.createClient(shop, accessToken);
        const hasSubscription = await handlers.getAppSubscriptionStatus(ctx);
        console.log("subccrioAuth",hasSubscription)
        if (hasSubscription) {
          next();
          ctx.redirect(`/?shop=${shop}&host=${host}`);
        } else {
          await handlers.getSubscriptionUrl(ctx);
        }

      },
    })
  );

  const handleRequest = async (ctx) => {
    await handle(ctx.req, ctx.res);
    // console.log(shop);
    ctx.respond = false;
    ctx.res.statusCode = 200;
  };
  const verifyIfSubscription = async (ctx, next) => {
    const shop  = process.env.SHOP;
    console.log("shop",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}`);
      return;
    }
    const hasSubscription = await handlers.getAppSubscriptionStatus(ctx);
    console.log("subscr",hasSubscription)
    if (!hasSubscription) {
      console.log("inside",hasSubscription)
      server.context.client = await handlers.createClient(shop, token);
      await handlers.getSubscriptionUrl(ctx);
      return;
    }else {
      return next();
    }
  };

  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 = process.env.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);
  //   }
  // });
 router.get("(.*)", verifyIfSubscription, handleRequest);

  server.use(router.allowedMethods());
  server.use(router.routes());
  server.listen(port, () => {
    console.log(`> Ready on http://localhost:${port}`);
  });
});

 

 

 

If anyone can covey there thoughts on this and share there suggestions on this it will be awesome and very helpful.

@sgray 

@Henry_Tao 

@SteveTT 

@Busfox 

@hassain 

@FireHydrant 

if you can provide any input it will be very helpful

 

Thanks

 

 

 

Replies 3 (3)
SteveTT
Shopify Partner
39 4 15

I don't fully understand your flow, but since you tagged me, here goes ...

 

Firstly I'm pretty sure you can keep generating new RecurringApplicationCharge(s) forever, so I think it's important to generate charges only when you want the merchant to accept it. I get that the flag is false, and there's some delay etc.

 

How about seperating the logic of installation/OAuth from asking the merchant to accept the charge into 2 different steps? ie. they can install, then present a screen for them to accept the RecurringApplicationCharge. If they don't accept the charge, the app is non-functional. If they do, go to the RecurringApplicationCharge logic.

 

Again, apologies as if I'm not getting how your app works, but it seems to me that the logic of the inital OAuth is combined with the RecurringApplicationCharge logic.

Arjun98
Shopify Partner
13 0 1

Hi Steve thanks for the replay

 

but the issue of occurring recurring bill approval page two times after the oAuth is resolved i just make a change in the logic of the verifyIfSubscription function logic in the server.js

 

const verifyIfSubscription = async (ctx, next) => {
    const shop = process.env.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}`);
      return;
    }
    server.context.client = await handlers.createClient(shop, token);
    const hasSubscription = await handlers.getAppSubscriptionStatus(ctx);
    if (!hasSubscription) {
      await handlers.getSubscriptionUrl(ctx);
      return;
    } else {
      return next();
    }
  };

 

 

and now the pricing approval page will obly appear once and if the user choose the plan and approve it than they will navigate to the apps dashboard.

 

But their is an issue regarding this , if the user cancel the subscription page after the oAuth and then they go back to the /admin/apps in there store and open the app so now 

The value of the subscription status will be false and so there must be shown the Subscription page again but here the value of hasSubscription is false here and instead of showing the subscription page they show the unable to connect message in the app dashboard.

Screenshot (11).png

 

is their any change should be made in the logic of the "verifyIfSubscription" function inorder to make this show the payment approval page.

 

any suggestions and reference links will be very helpful.

thanks @SteveTT  for the valuable suggestion.

Thanks

SteveTT
Shopify Partner
39 4 15

But their is an issue regarding this , if the user cancel the subscription page after the oAuth and then they go back to the /admin/apps in there store and open the app so now 

IMHO, at this point, since the user has OAuth successfully, there should not be any "connection error" presented to the user. Getting the user to accept the RecurringApplicationCharge should be a completely different block of logic, if we see this as a flowchart.

 

To determine if the user has accepted the RecurringApplicationCharge, I suggest that you keep track of that yourself, eg. store in a database. You could retrieve a list of recurring application charges (https://shopify.dev/api/admin-rest/2022-01/resources/recurringapplicationcharge#[get]/admin/api/2022...) but that may introduce an delay if you don't get a response quickly enough and not be a good UX.

 

So I suggest, after OAuth, check if the user has previously accepted the RecurringApplicationCharge:

if yes - user can continue to use the app

if no - begin RecurringApplicationCharge flow, ie. generate new RecurringApplicationCharge​, if user comes back to your confirmation url, then the user has actually accepted the RecurringApplicationCharge.

 

Remember that just because you've generated a RecurringApplicationCharge doesn't mean the user has to, or did accept it.

 

Better yet, show a page explaining to user what is about to happen and why (eg. your app requires subscription to continue), click "OK" to continue.