"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";
import { UnbuiltMarker } from "@/components/ui/UnbuiltMarker";
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}
);
}
/**
* Gate A / A1 (server-26#46). The plan cards below render taglines that claim
* entitlements with no backend behind them. Those claims get marked unbuilt
* inline, on this screen, next to the plan that makes them. This is a labelling
* change only — it does not build any of these, and it must never grow into a
* price or a checkout path (Gate B still bars charging anyone).
*/
const UNBUILT_CLAIMS: Partial> = {
free: ["Retention window"],
pro: ["Retention window"],
enterprise: ["Custom retention", "SSO / SAML", "Uptime SLA", "Data residency"],
};
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 && (
)}
{plan.name}}
/>
{PLANS.map((p) => {
const isCurrent = p.id === sub.planId;
return (
{p.name}
{p.tagline}
{(UNBUILT_CLAIMS[p.id] ?? []).length > 0 && (
{(UNBUILT_CLAIMS[p.id] ?? []).map((claim) => (
{claim} — not yet available
))}
)}
{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}
))}
)}
);
}