Icon rebuilt as a full-bleed 1200x1200 square (Shopify rounds corners itself). 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 || "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}</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);
|