bundle-offers-app/app/routes/app.plans.tsx
metatroncubeswdev 365e45406a Bundle Offers Shopify app: discount function, admin, storefront, deploy stack
Rust discount function (one automatic discount evaluating all offers in priority
order, parity with the Odoo engine), React Router admin with Polaris, theme app
extension and app proxy pages, webhooks incl. GDPR, managed-pricing limits, and
a Docker Compose stack (Postgres + Caddy HTTPS) for a single VPS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:35:58 -04:00

68 lines
3.1 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 || "bundle-offers";
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}</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",
].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);