App Store review 1.2.2 ("fails to register the plan"): follow Offerlyst's approved
managed-pricing setup. The plan is read from the Partner API on every app load,
shops without a plan (new install or reinstall) go to Shopify's plan selection
page, and app_subscriptions/update keeps it in sync. Billing API code removed.
Reviewer's checkout video: Shopify's checkout shows pickup by location name, so
the delivery label rename never reached the shopper. New checkout-time extension
shows the chosen date and time on the thank-you page (all plans) and next to
pickup/shipping options (Plus). Checkout display copy now matches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
47 lines
2.1 KiB
TypeScript
47 lines
2.1 KiB
TypeScript
// Plans use Shopify managed pricing: Shopify hosts the plan page, trials, approval and proration.
|
|
// The app layout loader keeps the stored plan in sync with the subscription.
|
|
import type { HeadersFunction, LoaderFunctionArgs } from "react-router";
|
|
import { useLoaderData } from "react-router";
|
|
import { boundary } from "@shopify/shopify-app-react-router/server";
|
|
import { adminContext } from "../lib/admin.server";
|
|
import { pricingUrl } from "../lib/partner.server";
|
|
import { PLAN_COPY } from "../services/plans";
|
|
|
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
const { cfg, session } = await adminContext(request, { fresh: true });
|
|
return { plan: cfg.shop.plan, pricingUrl: pricingUrl(session.shop) };
|
|
};
|
|
|
|
export default function Plans() {
|
|
const { plan, pricingUrl } = useLoaderData<typeof loader>();
|
|
const current = PLAN_COPY.find((p) => p.id === plan) ?? PLAN_COPY[0];
|
|
return (
|
|
<s-page heading="Plans">
|
|
<s-button slot="primary-action" variant="primary" href={pricingUrl} target="_top">Change plan</s-button>
|
|
<s-section>
|
|
<s-paragraph>
|
|
You're on the <s-text type="strong">{current.name}</s-text> plan. Paid plans include a 14-day free trial; development stores are free.
|
|
</s-paragraph>
|
|
</s-section>
|
|
<s-section heading="Compare">
|
|
<s-grid gridTemplateColumns="repeat(auto-fit, minmax(220px, 1fr))" gap="base">
|
|
{PLAN_COPY.map((p) => (
|
|
<s-box key={p.id} padding="base" border="base" borderRadius="base" background={plan === p.id ? "subdued" : "base"}>
|
|
<s-stack gap="small">
|
|
<s-heading>{p.name}</s-heading>
|
|
<s-text type="strong">{p.price}</s-text>
|
|
<s-unordered-list>
|
|
{p.points.map((x) => <s-list-item key={x}>{x}</s-list-item>)}
|
|
</s-unordered-list>
|
|
{plan === p.id ? <s-badge tone="success">Current plan</s-badge> : null}
|
|
</s-stack>
|
|
</s-box>
|
|
))}
|
|
</s-grid>
|
|
</s-section>
|
|
</s-page>
|
|
);
|
|
}
|
|
|
|
export const headers: HeadersFunction = (headersArgs) => boundary.headers(headersArgs);
|