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>
69 lines
3.2 KiB
TypeScript
69 lines
3.2 KiB
TypeScript
// Plans use Shopify managed pricing (brief §18): Shopify hosts the plan page, trials and proration.
|
|
import type { HeadersFunction, LoaderFunctionArgs } from "react-router";
|
|
import { useLoaderData } from "react-router";
|
|
import { boundary } from "@shopify/shopify-app-react-router/server";
|
|
import { authenticate } from "../shopify.server";
|
|
import { syncShop } from "../lib/shop.server";
|
|
import { PLANS, planOf } from "../lib/plans";
|
|
|
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
const { admin, session } = await authenticate.admin(request);
|
|
const shop = await syncShop(admin, session.shop, { fresh: true }); // current plan from Shopify App Pricing
|
|
const store = session.shop.replace(".myshopify.com", "");
|
|
const handle = process.env.SHOPIFY_APP_HANDLE || "offerlyst";
|
|
return {
|
|
current: shop.planCode,
|
|
pricingUrl: `https://admin.shopify.com/store/${store}/charges/${handle}/pricing_plans`,
|
|
plans: Object.entries(PLANS).map(([code, p]) => ({ code, ...p, runningOffers: p.runningOffers === Infinity ? "Unlimited" : String(p.runningOffers) })),
|
|
currentName: planOf(shop.planCode).name,
|
|
};
|
|
};
|
|
|
|
export default function Plans() {
|
|
const d = useLoaderData<typeof loader>();
|
|
return (
|
|
<s-page heading="Plans">
|
|
<s-button slot="primary-action" variant="primary" href={d.pricingUrl} target="_top">Change plan</s-button>
|
|
<s-section>
|
|
<s-paragraph>You're on the <s-text type="strong">{d.currentName}</s-text> plan. Paid plans include a 14-day free trial; development stores are free.</s-paragraph>
|
|
</s-section>
|
|
<s-section heading="Compare">
|
|
<s-table>
|
|
<s-table-header-row>
|
|
<s-table-header listSlot="primary">Plan</s-table-header>
|
|
<s-table-header>Price</s-table-header>
|
|
<s-table-header>Running offers</s-table-header>
|
|
<s-table-header>Includes</s-table-header>
|
|
</s-table-header-row>
|
|
<s-table-body>
|
|
{d.plans.map((p) => (
|
|
<s-table-row key={p.code}>
|
|
<s-table-cell>{p.name}{p.code === d.current ? " (current)" : ""}</s-table-cell>
|
|
<s-table-cell>{p.price}{p.yearly ? ` or ${p.yearly}` : ""}</s-table-cell>
|
|
<s-table-cell>{p.runningOffers}</s-table-cell>
|
|
<s-table-cell>
|
|
{[
|
|
p.types.length === 3 ? "All bundle types" : "Fixed bundles",
|
|
p.scheduling && "scheduling",
|
|
p.homepage && "homepage strip",
|
|
p.cartHints && "cart hints",
|
|
p.templates && "festival templates",
|
|
p.analytics && "analytics",
|
|
p.markets && "market targeting",
|
|
p.translations && "translations",
|
|
p.monthlyOrders !== Infinity && `${p.monthlyOrders} bundle orders/month`,
|
|
p.branding && "“Powered by” link",
|
|
...(p.extras || []),
|
|
].filter(Boolean).join(", ")}
|
|
</s-table-cell>
|
|
</s-table-row>
|
|
))}
|
|
</s-table-body>
|
|
</s-table>
|
|
</s-section>
|
|
</s-page>
|
|
);
|
|
}
|
|
|
|
export const headers: HeadersFunction = (headersArgs) => boundary.headers(headersArgs);
|