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>
68 lines
3.1 KiB
TypeScript
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);
|