App Store review 1.2.2: the app didn't register the chosen plan. App Pricing doesn't populate currentAppInstallation.activeSubscriptions or send billing webhooks, so the plan now comes from the Partner API activeSubscription query. Shops with no subscription (new install or reinstall) are sent to Shopify's plan selection page; Partner API failures never redirect or downgrade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
63 lines
2.5 KiB
TypeScript
63 lines
2.5 KiB
TypeScript
import type { HeadersFunction, LoaderFunctionArgs } from "react-router";
|
|
import { Outlet, useLoaderData, useRouteError } from "react-router";
|
|
import { boundary } from "@shopify/shopify-app-react-router/server";
|
|
import { AppProvider } from "@shopify/shopify-app-react-router/react";
|
|
|
|
import { authenticate } from "../shopify.server";
|
|
import { gql } from "../lib/shop.server";
|
|
import { fetchActiveSubscription, partnerApiConfigured } from "../lib/partner.server";
|
|
|
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
const { admin, redirect, session } = await authenticate.admin(request);
|
|
|
|
// Shopify App Pricing: a shop without a plan (new install, or reinstall after the old one was cancelled)
|
|
// goes to Shopify's plan selection page. A Partner API failure never redirects a paying merchant.
|
|
if (partnerApiConfigured()) {
|
|
// the welcome link adds plan_handle after a plan is approved: skip the cache so the new plan shows at once
|
|
const fresh = new URL(request.url).searchParams.has("plan_handle");
|
|
let sub: Awaited<ReturnType<typeof fetchActiveSubscription>> | undefined;
|
|
try {
|
|
const { shop } = await gql(admin, `#graphql
|
|
query OfferlystShopId { shop { id } }`);
|
|
sub = await fetchActiveSubscription(shop.id, { fresh });
|
|
} catch (e) {
|
|
console.error("subscription check failed", e);
|
|
}
|
|
if (sub === null) {
|
|
const store = session.shop.replace(".myshopify.com", "");
|
|
const handle = process.env.SHOPIFY_APP_HANDLE || "offerlyst";
|
|
throw redirect(`https://admin.shopify.com/store/${store}/charges/${handle}/pricing_plans`, { target: "_top" });
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-undef
|
|
return { apiKey: process.env.SHOPIFY_API_KEY || "" };
|
|
};
|
|
|
|
export default function App() {
|
|
const { apiKey } = useLoaderData<typeof loader>();
|
|
|
|
return (
|
|
<AppProvider apiKey={apiKey}>
|
|
<s-app-nav>
|
|
<s-link href="/app">Home</s-link>
|
|
<s-link href="/app/offers">Offers</s-link>
|
|
<s-link href="/app/templates">Festival templates</s-link>
|
|
<s-link href="/app/analytics">Analytics</s-link>
|
|
<s-link href="/app/settings">Settings</s-link>
|
|
<s-link href="/app/plans">Plans</s-link>
|
|
</s-app-nav>
|
|
<Outlet />
|
|
</AppProvider>
|
|
);
|
|
}
|
|
|
|
// Shopify needs React Router 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);
|
|
};
|