| 03-operator-experience |
03 |
ui |
| stripe |
| billing |
| react |
| nextjs |
| tanstack-query |
| shadcn |
| subscription |
|
| phase |
plan |
provides |
| 03-operator-experience |
01 |
POST /api/portal/billing/checkout, POST /api/portal/billing/portal, Tenant billing fields (subscription_status, agent_quota, trial_ends_at) |
|
|
| BillingStatus badge component — color-coded display for all 6 subscription states |
| SubscriptionCard component — plan pricing, agent count adjuster (+/- controls), action buttons driven by status |
| Billing page at /billing — subscription card, past-due warning banner, Checkout success toast on ?session_id= return |
| useCreateCheckoutSession() mutation hook — POST /billing/checkout, returns checkout_url |
| useCreateBillingPortalSession() mutation hook — POST /billing/portal, returns portal_url |
| useUpdateSubscriptionQuantity() mutation hook — POST /billing/update-quantity, invalidates tenant query |
| Billing nav link in dashboard sidebar (CreditCard icon) |
|
| 03-04 (cost dashboard — nav sidebar now has Billing link, Billing/Cost pages are adjacent) |
|
| added |
patterns |
|
|
| subscription status → action button mapping (none/canceled → Subscribe, trialing/active → Manage Billing, past_due/unpaid → Update Payment) |
| Stripe redirect pattern |
| mutate → receive URL → window.location.href redirect (no router.push — external URL) |
|
| use(searchParams) for client components in Next.js 15 — searchParams is a Promise |
| Session_id cleanup |
| replace URL after timeout to avoid re-showing success banner on refresh |
|
|
|
| created |
modified |
| packages/portal/components/billing-status.tsx |
| packages/portal/components/subscription-card.tsx |
| packages/portal/app/(dashboard)/billing/page.tsx |
|
| packages/portal/lib/api.ts (added billing types |
| CheckoutSessionRequest/Response, BillingPortalRequest/Response, UpdateSubscriptionQuantityRequest/Response) |
|
| packages/portal/lib/queries.ts (added useCreateCheckoutSession, useCreateBillingPortalSession, useUpdateSubscriptionQuantity hooks) |
| packages/portal/components/nav.tsx (added Billing nav item with CreditCard icon) |
|
|
| window.location.href used for Stripe redirects (not router.push) — Stripe Checkout/Portal URLs are external, router.push only handles internal Next.js routes |
| use(searchParams) pattern in billing page client component — Next.js 15 searchParams is a Promise, must be unwrapped with React.use() in client components |
| BillingStatus renders inline custom Tailwind classes (not Badge variant prop) — existing Badge variants don't have semantic color variants (blue/green/amber/red), inline classes give precise control |
| Session_id cleared from URL after 8 seconds via router.replace('/billing') — prevents success banner reappearing on manual page refresh |
|
| Stripe redirect flow: useMutation hook → mutateAsync → receive { url } response → window.location.href = url |
| Status-driven action button: single switch on subscription_status in CardFooter renders appropriate action (Subscribe/Manage/Update/Resubscribe) |
| Agent count adjuster: local useState(agentQuota) with +/- buttons, shows real-time monthly total, separate Update Quantity button appears when quantity !== agentQuota |
|
|
8min |
2026-03-24 |