Shopify Plan Selection Page

I’m currently testing my app on the dev store, after merchant (dev store) installed the app, my code check to see if merchant has an active billing, if not, we re-direct them to the plan selection page.

export const loader = async ({ request }: LoaderFunctionArgs) => {
  const { billing, redirect, session } = await authenticate.admin(request);

  // Check whether the store has an active subscription
  const { hasActivePayment } = await billing.check();

  // If there's no active subscription, redirect to the plan selection page...
  if (!hasActivePayment) {
    const shop = session.shop; // e.g., "cool-shop.myshopify.com"
    const storeHandle = shop.replace(".myshopify.com", "");
    const appHandle = "myapp";
    const host = new URL(request.url).searchParams.get("host");

    const planUrl = `https://admin.shopify.com/store/${storeHandle}/charges/${appHandle}/pricing_plans?host=${host}`;
    console.log(
      `[Billing] No active payment for ${shop}. Redirecting to: ${planUrl}`,
    );
    // Use the redirect utility from authenticate.admin
    return redirect(planUrl, {
      target: "_top", // required since the URL is outside the embedded app scope
    });
  }

the billing check logic looks correct, i can see the console logging the correct redirect url but the problem is in the re-direction itself. I can see the Plan selection page show up briefly but something block it, instead, shopify re-directs me to the ‘Apps and Sales Channel page’
i’ve check the network tab but couldn’t figure out whats causing the second re-direction (the first redirection should be to the plan selection page)
not sure whats causing this? does anyone have an idea?
it’s an embedded app with remix. i’ve setup the plans via managed pricing
I can successfully select a plan if i go to the ‘apps and sales channel page’ click on my app > view details > manage plan from there

Hi @zoombie-lab

I believe the behaviour you’re seeing is expected. The doc that will be helpful here is this: Redirect to the plan selection page

When you redirect to the plan selection page from within your embedded app, you’re using a top-level redirect to a Shopify admin URL. The plan selection page appears briefly, but then Shopify redirects you to the “Apps and Sales Channel” page. This is expected behavior if the merchant already has an active (test) subscription, or if the plan selection page is being accessed in a way Shopify doesn’t expect (such as outside the intended onboarding flow).

If you want to test plan selection, you must ensure the store does not already have an active subscription for your app. Uninstalling and reinstalling the app on the dev store should clear the subscription and allow you to see the plan selection page again.

Hey Liam,

Thats the thing, the merchant do not have an active plan as such the expected behavior is for that plan selection page to appear and merchant select a plan
I can’t figure out whats causing the re-direction? any idea?

UPDATE: Fixed! It was a typo in my .env file for the SHOPIFY_APP_HANDLE! Leaving my response for reference

@Liam-Shopify I am having the exact same issue. If I uninstall the app (on a development site) and then reinstall the app. I am stuck in a loop where I’m passed to the App sales & Channels page with the url /settings/apps?before=&after=&tab=installed

If I click on the app name on that page, then “Open app” it just sends me very quickly to the plans page then immediately back to the App sales & Channels page.

If I click the app name at the sidebar I get the same issue.

The only way to break the cycle is to go to the “About this app” page and click ”Select Plan” in the Billing section.

I’m using the code, pretty much copy and paste from here Redirect to the plan selection page

I’m using the Shopify Remix Template. Here is my app.tsx file.

import type { HeadersFunction, LoaderFunctionArgs } from "@remix-run/node";
import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react";
import { boundary } from "@shopify/shopify-app-remix/server";
import { AppProvider } from "@shopify/shopify-app-remix/react";
import { NavMenu } from "@shopify/app-bridge-react";
import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";

import { authenticate } from "../shopify.server";

export const links = () => [{ rel: "stylesheet", href: polarisStyles }];

export const loader = async ({ request }: LoaderFunctionArgs) => {
  await authenticate.admin(request);

  // Start of Billing Check.
  const appHandle = process.env.SHOPIFY_APP_HANDLE;

  // Initiate billing and redirect utilities
  const { billing, redirect, session } = await authenticate.admin(request);
  
  // Check whether the store has an active subscription
  const { hasActivePayment } = await billing.check();

  // Extract the store handle from the shop domain
  // e.g., "cool-shop" from "cool-shop.myshopify.com"
  const shop = session.shop; // e.g., "cool-shop.myshopify.com"
  const storeHandle = shop.replace('.myshopify.com', '');

  // If there's no active subscription, redirect to the plan selection page...
  if (!hasActivePayment) {    
    return redirect(`https://admin.shopify.com/store/${storeHandle}/charges/${appHandle}/pricing_plans`, {
      target: "_top", // required since the URL is outside the embedded app scope
    });
  }

  // Otherwise, continue loading the app as normal
  // End of Billing Check.
  return { apiKey: process.env.SHOPIFY_API_KEY || "" };
};

export default function App() {
  const { apiKey } = useLoaderData<typeof loader>();

  return (
    <AppProvider isEmbeddedApp apiKey={apiKey}>
      <NavMenu>
        <Link to="/app" rel="home">
          Home
        </Link>
        <Link to="/app/settings">Settings</Link>
        <Link to="/app/hidden-tags">Hidden Tags</Link>
        <Link to="/app/faqs">FAQs</Link>
      </NavMenu>
      <Outlet />
    </AppProvider>
  );
}

// Shopify needs Remix to catch some thrown responses, so that their headers are included in the response.
export function ErrorBoundary() {
  return boundary.error(useRouteError());
}

export const headers: HeadersFunction = (headersArgs) => {
  return boundary.headers(headersArgs);
};

Glad you figured this out @Stephen_O_Hara :slight_smile: