// The shapes this portal reads off the api-saas-starter backend, kept LOCAL so // the pages are self-contained. They mirror the api's descriptor outputs // (`session.me`, `billing.subscription`, `billing.invoices`) and the framework's // `/v1/api-keys` REST surface. // The caller's resolved identity, from `session.me`. The SSR gate reads it. export interface Identity { readonly type: string readonly id: string | null readonly tenantId: string | null } // `billing.subscription` streams `{ subscription: … | null }` — null before the // tenant has any subscription row (they're on the implicit free plan). export interface SubscriptionState { readonly subscription: { readonly plan: string readonly status: string readonly quantity: number readonly currentPeriodEnd: string | null readonly cancelAt: string | null } | null } // One row of `billing.invoices`. `hostedUrl` / `pdfUrl` are the PROVIDER's own // pages — the portal links out, it never renders an invoice. export interface Invoice { readonly providerInvoiceId: string readonly amountMinor: number readonly currency: string readonly status: string readonly createdAt: string | null readonly hostedUrl: string | null readonly pdfUrl: string | null } // `billing.previewChange` — the provider's prorated settlement for a change. export interface ChangePreview { readonly plan: string readonly quantity: number readonly prorationMinor: number readonly currency: string } // One key from `GET /v1/api-keys` (the framework's built-in management surface). export interface ApiKey { readonly id: string readonly name: string readonly keyPrefix: string readonly createdAt: number readonly lastUsedAt: number | null readonly revokedAt: number | null } // Minor units (cents) → a display string. Intl handles the currency + locale. export const formatMoney = (minor: number, currency: string, locale: string): string => { try { return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(minor / 100) } catch { return `${(minor / 100).toFixed(2)} ${currency.toUpperCase()}` } }