bundle-offers-app/app/routes/app.plans.tsx
metatroncubeswdev b59c2d0668 Plans page: show yearly prices and Plus extras
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:58:22 -04:00

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); // refresh the plan from activeSubscriptions
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);