import { useEffect } from "react"; import type { getBilling } from "~/fns/billing"; import { openBillingPortal, startDomainAddonCheckout, startProCheckout, startTopupCheckout } from "~/fns/billing"; import { useAction } from "~/hooks/use-action"; import { formatQuantity, meterDef } from "~/lib/meters"; import { CREDIT_POOLS, CUSTOM_DOMAIN_LIMIT } from "~/lib/pricing"; import type { CreditPoolId } from "~/lib/pricing"; import { usd } from "~/lib/pricing-display"; import { trackEvent } from "~/lib/analytics"; import { Badge } from "~/ui/Badge"; import { Button } from "~/ui/Button"; import { ErrorBanner } from "~/ui/Callout"; import { Panel } from "~/ui/Panel"; import { RelativeTime } from "~/ui/RelativeTime"; import { ComputeRatesNote } from "./ComputeRatesNote"; import { InfoTip } from "./InfoTip"; import { VerifyEmailHint } from "~/components/VerifyEmailHint"; import { useEmailUnverified } from "~/hooks/use-viewer"; type Billing = Awaited>; type Action = { reset: () => void }; function go(result: { url: string } | undefined, kind: string) { if (!result?.url) return; trackEvent("checkout_started", { kind }); window.location.assign(result.url); } /** * Coming back from Stripe with the browser's back button can restore this page from the back-forward cache with * its buttons still pending; clear them so the page is usable again. */ function useResetOnRestore(...actions: Action[]) { useEffect(() => { const onShow = (event: PageTransitionEvent) => { if (event.persisted) for (const action of actions) action.reset(); }; window.addEventListener("pageshow", onShow); return () => window.removeEventListener("pageshow", onShow); }); } const POOL_UNIT: Record = { compute: "seconds", "ai.tokens": "tokens" }; function poolAmount(pool: CreditPoolId, quantity: number): string { if (pool === "compute") return `${formatQuantity("seconds", quantity)} of standard-1`; return `${formatQuantity("tokens", quantity)} tokens`; } function SubscriptionPanel({ billing, org }: { billing: Billing; org: string | null }) { const checkout = useAction(startProCheckout, { invalidate: false }); const portal = useAction(openBillingPortal, { invalidate: false }); const unverified = useEmailUnverified(); useResetOnRestore(checkout, portal); const { stripe, account } = billing; const subscription = stripe.subscription; const active = Boolean(subscription?.status && ["active", "trialing", "past_due"].includes(subscription.status)); const assigned = !subscription && billing.summary.plan.id === "pro"; return (
{unverified && } {stripe.checkoutPaused && (

New purchases and plan changes are paused; you can still update your card.

)} {subscription ? (

{billing.plans.pro.label} {subscription.status ?? "unknown"} {subscription.currentPeriodEnd && ( {subscription.cancelAtPeriodEnd ? "ends" : "renews"}{" "} )}

) : assigned ? (

This account is on {billing.plans.pro.label}, assigned by a site administrator.

) : ( <>

{billing.plans.pro.label} is {usd(billing.plans.pro.priceCents / 100)} per month

Bigger allowances (see Pricing below), and usage past them is billed at the listed rates instead of stopping.

)}
{!active && !assigned && stripe.proAvailable && ( )} {stripe.hasCustomer && ( )}
); } function DomainAddonPanel({ billing, org }: { billing: Billing; org: string | null }) { const checkout = useAction(startDomainAddonCheckout, { invalidate: false }); const unverified = useEmailUnverified(); useResetOnRestore(checkout); const addon = billing.stripe.domainAddon; if (!addon.available) return null; const active = Boolean(addon.status && ["active", "trialing", "past_due"].includes(addon.status)); return (
{unverified && }

{usd(addon.priceCents / 100)} per custom domain per month, up to {CUSTOM_DOMAIN_LIMIT} per account

{addon.status ? {addon.status} : "Not added"} {active && {addon.quantity} billed}

{!active && ( )}
); } function CreditsPanel({ billing, org }: { billing: Billing; org: string | null }) { const topup = useAction(startTopupCheckout, { invalidate: false }); const unverified = useEmailUnverified(); useResetOnRestore(topup); const pools = Object.values(CREDIT_POOLS); return (
{unverified && }

Credit is spent after your plan's included usage and before any overage, at the same rates. It never expires.

{pools.map((pool) => { const packs = billing.stripe.packs.filter((pack) => pack.pool === pool.id); const balance = billing.summary.credits[pool.id] ?? 0; return (

{pool.label}

{pool.id === "compute" && ( )} {balance > 0 ? `${poolAmount(pool.id, balance)} left` : "No credit"}
{packs.length > 0 && (
    {packs.map((pack) => (
  • {pack.label} {usd(pack.cents / 100)}
  • ))}
)}
); })} {!billing.account.canEdit &&

Only account owners can buy credit.

} {billing.ledger.length > 0 && (
Credit history
    {billing.ledger.map((entry) => { const pool = entry.meter as CreditPoolId; const unit = POOL_UNIT[pool] ?? meterDef(entry.meter)?.unit ?? "count"; return (
  • {entry.kind} {CREDIT_POOLS[pool]?.label ?? entry.meter} {entry.quantity > 0 ? "+" : ""} {formatQuantity(unit, entry.quantity)}
  • ); })}
)}
); } /** Stripe-backed controls; without STRIPE_SECRET_KEY they collapse to a single "not configured" note. */ export function StripePanels({ billing, org }: { billing: Billing; org: string | null }) { if (!billing.stripe.enabled) { return (

Billing is not configured on this instance; plans are assigned by a site administrator.

); } return ( <> ); }