import { data, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { Form, useLoaderData, useNavigation } from "@remix-run/react"; import { Page, Layout, Card, BlockStack, InlineStack, Text, Button, Badge } from "@shopify/polaris"; import { TitleBar } from "@shopify/app-bridge-react"; import { authenticate } from "../shopify.server"; import { setShopTier, tierFromPlanName } from "../services/billing.server"; import { PLAN_PRICING, type Tier } from "../lib/billing-plans"; const TIER_LABEL: Record = { free: "Free", starter: "Starter", growth: "Growth", pro: "Pro" }; const FEATURES: Record = { free: ["1 location", "Shipping / Local Delivery / Pickup", "Date/time picker widget", "Blackout dates", "All-plan checkout enforcement"], starter: ["Everything in Free", "Cut-off & prep-time windows", "Up to 3 locations", "Dispatch dashboard"], growth: ["Everything in Starter", "Unlimited locations", "Delivery zones & rates", "Distance auto-assignment"], pro: ["Everything in Growth", "Priority support"], }; const isTest = process.env.NODE_ENV !== "production"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { session, billing } = await authenticate.admin(request); const { appSubscriptions } = await billing.check({ isTest }); const activeSubscription = appSubscriptions[0]; // Reconcile our cached Shop.tier against Shopify's own live billing state // on every visit to this page — the app_subscriptions/update webhook is // the durable sync path, but this catches drift immediately rather than // waiting on webhook delivery. const liveTier = activeSubscription ? tierFromPlanName(activeSubscription.name) : "free"; await setShopTier(session.shop, liveTier); return { tier: liveTier, subscription: activeSubscription ?? null }; }; export const action = async ({ request }: ActionFunctionArgs) => { const { session, billing } = await authenticate.admin(request); const formData = await request.formData(); const intent = formData.get("intent"); const returnUrl = `${new URL(request.url).origin}/app/billing`; if (intent === "downgrade") { const { appSubscriptions } = await billing.check({ isTest }); if (appSubscriptions[0]) { await billing.cancel({ subscriptionId: appSubscriptions[0].id, isTest, prorate: true }); } await setShopTier(session.shop, "free"); return data({ ok: true }); } const plan = PLAN_PRICING.find((p) => p.tier === intent); if (!plan) return data({ ok: false }, { status: 400 }); // billing.request() redirects to Shopify's confirmation page; it never // returns normally (typed Promise). return billing.request({ plan: plan.name, isTest, returnUrl }); }; export default function Billing() { const { tier, subscription } = useLoaderData(); const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; return ( Current plan: {TIER_LABEL[tier]} {subscription && Active} {subscription && ( Renews {new Date(subscription.currentPeriodEnd).toLocaleDateString()}. )} {(["free", "starter", "growth", "pro"] as const).map((planTier) => (
{TIER_LABEL[planTier]} {planTier === tier && Current} {planTier === "free" ? "$0" : `$${PLAN_PRICING.find((p) => p.tier === planTier)!.amount}/mo`} {FEATURES[planTier].map((feature) => ( • {feature} ))} {planTier !== tier && (
)}
))}
); }