"use client"; import { useEffect, useState } from "react"; import { PLANS, getPlan, getCurrentSubscription, getUsageSummary, getInvoices, createCheckoutSession, createBillingPortalSession, type Subscription, type UsageSummary, type Invoice, type PlanId, } from "@/lib/billing"; import { Card, CardHeader } from "@/components/ui/Card"; import { Badge } from "@/components/ui/Badge"; import { Button } from "@/components/ui/Button"; import { SkeletonCard } from "@/components/ui/Skeleton"; function fmtDate(iso: string) { return new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); } function StatusBanner({ sub }: { sub: Subscription }) { if (sub.status === "trialing" && sub.trialEndsAt) { return (

Trial active — ends {fmtDate(sub.trialEndsAt)}. Add a payment method to keep your plan after that.

Trial
); } if (sub.status === "past_due") { return (

Payment failed on your last invoice. Update your payment method to avoid losing access.

Past due
); } if (sub.status === "canceled") { return (

Your subscription is canceled. Reactivate to restore full access.

Canceled
); } return null; } function UsageBar({ label, used, limit }: { label: string; used: number; limit: number | "unlimited" }) { const pct = limit === "unlimited" ? 0 : Math.min(100, Math.round((used / Math.max(limit, 1)) * 100)); const nearLimit = limit !== "unlimited" && used / limit >= 0.9; return (
{label} {used} / {limit === "unlimited" ? "∞" : limit}
); } const INVOICE_TONE: Record = { paid: "success", open: "warning", void: "neutral", uncollectible: "danger", }; export default function BillingSettingsPage() { const [sub, setSub] = useState(null); const [usage, setUsage] = useState(null); const [invoices, setInvoices] = useState([]); const [loading, setLoading] = useState(true); const [actionError, setActionError] = useState(null); const [busyPlan, setBusyPlan] = useState(null); const [portalBusy, setPortalBusy] = useState(false); useEffect(() => { Promise.all([getCurrentSubscription(), getUsageSummary(), getInvoices()]) .then(([s, u, i]) => { setSub(s); setUsage(u); setInvoices(i); }) .finally(() => setLoading(false)); }, []); async function handleChoosePlan(planId: PlanId) { setBusyPlan(planId); setActionError(null); try { const { url } = await createCheckoutSession(planId, sub?.interval ?? "monthly"); window.location.href = url; } catch (e) { setActionError(e instanceof Error ? e.message : String(e)); } finally { setBusyPlan(null); } } async function handleManageBilling() { setPortalBusy(true); setActionError(null); try { const { url } = await createBillingPortalSession(); window.location.href = url; } catch (e) { setActionError(e instanceof Error ? e.message : String(e)); } finally { setPortalBusy(false); } } if (loading || !sub || !usage) { return (
); } const plan = getPlan(sub.planId); return (

Demo data — this page isn't connected to a live payment processor. See lib/billing.ts for the integration plan.

{actionError && (

{actionError}

)} {plan.name}} />
{PLANS.map((p) => { const isCurrent = p.id === sub.planId; return (

{p.name}

{p.tagline}

{p.priceMonthlyUsd === null ? "Custom" : p.priceMonthlyUsd === 0 ? "Free" : `$${p.priceMonthlyUsd}/mo`}

); })}
{invoices.length === 0 ? (

No invoices yet.

) : (
{invoices.map((inv) => (

{inv.description}

{fmtDate(inv.date)}

${inv.amountUsd.toFixed(2)} {inv.status}
))}
)}
); }