// Billing — URL `/billing`. The current plan (live from `billing.subscription`), // a card-management button that redirects to the PROVIDER's hosted portal // (`billing.portalUrl`), and a plan change that QUOTES the provider's prorated // settlement first (`billing.previewChange`) before applying it // (`billing.changePlan`) — never a locally-estimated figure. import type { ReactNode } from 'react' import { useState } from 'react' import type { PageMeta } from '@voltro/web' import { useSubscription, useAction } from '@voltro/client' import { T, useLocale, useT } from '@voltro/i18n' import { getCatalog } from '../../../locales/index' import { formatMoney, type ChangePreview, type SubscriptionState } from '../../../lib/api' export const renderMode = 'ssr' as const export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({ title: getCatalog(locale)['meta.billing.title'], }) // The plan a free tenant upgrades to — matches api-saas-starter's `plans`. const UPGRADE_PLAN = 'pro' export default function Billing(): ReactNode { const locale = useLocale() const { data } = useSubscription('app', 'billing.subscription') const portal = useAction<{ returnUrl: string }, { url: string }>('app', 'billing.portalUrl') const preview = useAction<{ plan?: string; quantity?: number }, ChangePreview>('app', 'billing.previewChange') const change = useAction<{ plan: string }, ChangePreview>('app', 'billing.changePlan') const [quote, setQuote] = useState(null) const [changed, setChanged] = useState(false) const sub = data?.subscription ?? null const freeLabel = useT('billing.plan.free') const busy = portal.pending || preview.pending || change.pending const onManageCard = (): void => { const here = typeof window === 'undefined' ? '/billing' : window.location.href void portal.run({ returnUrl: here }) .then((r) => { if (r && typeof window !== 'undefined') window.location.assign(r.url) }) .catch(() => { /* surfaced via portal.error */ }) } const onPreview = (): void => { setChanged(false) void preview.run({ plan: UPGRADE_PLAN }).then((q) => setQuote(q)).catch(() => setQuote(null)) } const onConfirm = (): void => { void change.run({ plan: UPGRADE_PLAN }).then(() => { setChanged(true); setQuote(null) }).catch(() => { /* change.error */ }) } return (

{sub?.plan ?? freeLabel}
{sub?.status ?? '—'}
{sub?.quantity ?? 1}

{quote ? (

) : null} {changed ?

: null}
{quote ? ( ) : ( )}
) }