Skip to main content

Redirect to the plan selection page

A common pattern is to redirect merchants to your plan selection page after they install your app. However, because embedded apps are rendered in the Shopify admin inside an iframe, they don't have permission to manipulate the parent browser window, including redirects.

Shopify's React Router package provides utilities that enable apps to redirect elsewhere in the Shopify admin. This keeps the user experience smooth while working within iframe constraints.



Anchor to Check subscription status in React RouterCheck subscription status in React Router

The example in this section runs in the loader for the app's root route, so it works no matter which app route the user arrives at. It does the following:

  1. Queries the Active Subscription API on the Partner API to check whether the shop has an active Shopify App Pricing subscription. With Shopify App Pricing, subscription status comes from the Partner API, not from the GraphQL Admin API.
  2. If not, then redirects to the plan selection page with the redirect utility from @shopify/shopify-app-react-router, which can navigate outside the app frame.
  3. If there is an active subscription, then renders your app's content normally.

The example reads the following values:

  • YOUR_APP_HANDLE: The handle from your shopify.app.toml file. The store handle comes from the session's shop domain (for example, cool-shop from cool-shop.myshopify.com).
  • SHOPIFY_PARTNER_ORG_ID: Your organization ID, which appears in your Partner Dashboard URL.
  • SHOPIFY_PARTNER_API_ACCESS_TOKEN: The access token for your Partner API client. Refer to Partner API authentication.
  • SHOPIFY_APP_GID: Your app's GID in the form gid://shopify/App/{app_id}, where {app_id} is the numeric ID in your app's Partner Dashboard URL.

activeSubscription requires the shop's GID rather than its myshopify.com domain, so the example queries shop { id } through the GraphQL Admin API. The Partner API has a rate limit of four requests per second for each client, so the example throws on throttled or failed responses instead of treating them as a missing subscription. In production, cache only a confirmed subscription for each shop, with a short expiry such as five minutes. A merchant who just approved a plan is then checked immediately, and a cancellation or freeze is picked up when the cached entry expires.

The route file handles the check and redirect. The Partner API request lives in its own server module so that other routes can reuse it.

app/routes/app.jsx

import { fetchActiveSubscription } from "../partner-api.server";

export const loader = async ({ request }) => {
// Replace with the "handle" from your shopify.app.toml file
const appHandle = "YOUR_APP_HANDLE";

// Authenticate with Shopify credentials to handle server-side queries
const { authenticate } = await import("../shopify.server");

// Get the GraphQL Admin API client and redirect utility
const { admin, redirect, session } = await authenticate.admin(request);

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

// Get the shop GID that the Partner API requires
const shopResponse = await admin.graphql(`{ shop { id } }`);
const { data: { shop: { id: shopId } } } = await shopResponse.json();

// Check whether the store has an active Shopify App Pricing subscription
const subscription = await fetchActiveSubscription(shopId);

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

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

app/partner-api.server.js

export async function fetchActiveSubscription(shopId) {
const res = await fetch(
`https://partners.shopify.com/${process.env.SHOPIFY_PARTNER_ORG_ID}/api/2026-07/graphql.json`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Shopify-Access-Token": process.env.SHOPIFY_PARTNER_API_ACCESS_TOKEN,
},
body: JSON.stringify({
query: `query ($appId: ID!, $shopId: ID!) {
activeSubscription(appId: $appId, shopId: $shopId) { billingPeriod }
}`,
variables: {
appId: process.env.SHOPIFY_APP_GID,
shopId,
},
}),
}
);
const { data, errors } = await res.json();

// Throw on throttling or other failures so the loader doesn't redirect a paying user
if (!res.ok || errors) {
throw new Error(`Partner API request failed: ${JSON.stringify(errors ?? res.status)}`);
}

// activeSubscription is null only when the shop has no Shopify App Pricing contract
return data.activeSubscription;
}
Info

If your app migrated from the Billing API, then existing Billing API subscriptions aren't returned by activeSubscription until you migrate them to Shopify App Pricing. Until then, also check billing.check() before redirecting. It queries currentAppInstallation and returns hasActivePayment: true for both active Billing API subscriptions and active one-time purchases. Remove that call after you've migrated your subscriptions and no existing one-time purchases still grant access.



Was this page helpful?