Q2 2026 TECH SPEC
Stratus Frontend Q2
Eight changes to the billing dashboard — six with zero backend dependency. Banners consolidated. Error 1210 handled. Checkout deduped. UC expanded. Each makes the next easier.
| Status | DRAFT — for frontend review |
|---|---|
| Driver | Jared Goguen (investigation), Frontend Engineering (implementation) |
| Reviewers | FinTech Engineering Review — Mar 30 |
| Repo | stratus (src/apps/dash/react/pages/billing/, src/apps/dash/react/pages/checkout/) |
| Staffing | 1 frontend engineer, 1 quarter |
| Cross-refs | Smart Checkout (PM gate, UC expansion), Honest Invoicing (bad debt, invoices) |
The billing dashboard has accumulated two checkout systems, twelve notification banner variants, two on-session payment hooks, and zero explicit handling for the most common billing error code. This section maps the surface as it exists today.
- 2
- checkout systems
- 12+
- banner variants
- 6
- on-session-payment consumers
- 0
- error 1210 handlers
Multisku (Redux, CardElement, 31 SKUs) and UC (React Query, PaymentElement, per-product gates). Different HTTP clients, state management, error handling. Same backend.
12+ notification variants. Three versioned iterations × two scope levels × two debt states. Plus enterprise dunning — a separate KV-targeting system entirely.
Two useOnSessionPayment hooks — effect-based (4 consumers) and callback-based (2 consumers). Same purpose, different API. Neither contains gate logic.
Error 1210 (PM required) has zero frontend handlers. Multisku shows raw messages. UC shows generic "unexpected error." 122 can't-pay-now tickets/month.
Four changes that need only stratus code. No backend deploys, no gate flips, no API changes. Ship independently, in any order.
The notification system in notifications.tsx and NewFeatureNotifications.tsx has accumulated twelve-plus banner variants across three versioned iterations and two scope levels. Each variant is gated separately. The user experience: which of twelve banners will I see, and do they say the same thing?
| Notification ID | Scope | Condition |
|---|---|---|
| bad_debt_on_account_notice_stripe | Account | bad debt, no stripe pay |
| bad_debt_on_account_notice_stripe_v2 | Billing | bad debt, stripe invoice pay, no stripe bad debt pay |
| bad_debt_on_account_notice_stripe_v3 | Billing | bad debt, both stripe pays enabled — includes Pay now link |
| bad_debt_zone_stripe | Zone | bad debt, no stripe pay (zone level) |
| bad_debt_zone_stripe_v2 | Zone | bad debt, stripe invoice pay, no stripe bad debt pay |
| bad_debt_zone_stripe_v3 | Zone | bad debt, both stripe pays enabled |
| outstanding_balance_on_account_notice_stripe | Billing | outstanding balance, no bad debt |
| outstanding_balance_on_account_notice_stripe_v2 | Billing | outstanding balance + stripe pay enabled |
| unpaid_invoices_zone_stripe | Zone | unpaid invoices, no bad debt |
| unpaid_invoices_zone_stripe_v2 | Zone | unpaid invoices + stripe pay enabled |
| DunningBanner (enterprise) | Account | dx-dunning-banner gate + KV targeting |
(1) Outstanding balance + Pay Now CTA. (2) Bad debt + Pay Now CTA. (3) Enterprise dunning (keep as-is). Same message at account and zone level.
The v1/v2/v3 versioned variants. The gate-combination filter chain. The distinction between "stripe invoice pay" and "stripe bad debt pay" — both enabled now.
Error code 1210 means "payment method required." The PM gate in subscriptions-api returns HTTP 402 with this code. The frontend has zero explicit handlers. This is the single highest-impact small change — 122 can't-pay-now tickets per month trace directly to users hitting this error with no guidance.
fetchWithoutErrorNotifications suppresses the global error bar. Raw backend message surfaces directly. Error code used only as a React key.
ApiError caught in useSubmitPurchase. Backend error discarded. Generic i18n string shown instead. Error code sent to analytics on wrong field.
| Checkout | File | Change |
|---|---|---|
| Multisku | billing/checkout/components/view/PaymentCheckoutView.tsx | Check error code 1210 → show targeted PM recovery message |
| UC | checkout/hooks/useSubmitPurchase.ts | Check ApiError.status === 402 → show PM guidance with link |
| Both | common/components/billing/ (new) | Shared PaymentMethodRequiredError component |
Two hooks, same purpose, different API. Plus a code duplication within UC where PurchaseForm.tsx and useSubmitPurchase.ts contain near-identical executeOnSessionPayment logic.
| Hook | Location | API Style | Consumers |
|---|---|---|---|
| Legacy | billing/checkout/hooks.ts | Effect-based | PaymentCheckoutView, PayOutstandingBalance, SelectPlan, useChangePlanHandler |
| UC | checkout/components/payment/hooks/useOnSessionPayment.ts | Callback-based | useSubmitPurchase, PurchaseForm |
Extract to common/components/billing/. Callback-based API (UC pattern). Both checkouts consume it. Gate checks stay at call sites.
PM gate removal touches all 6 consumers. Dedup first → gate removal becomes mechanical. Also fixes PurchaseForm/useSubmitPurchase duplication.
useSubmitPurchase.ts only handles on-session payment for subs.length === 1. Multi-subscription checkouts silently skip 3DS confirmation. When the billing-on-session-payment gate becomes unconditional, this is a real bug — subscriptions created without payment confirmation.
Extend executeOnSessionPayment to iterate over all client_secrets when multiple subscriptions are returned. Confirm each sequentially (Stripe requires serial confirmation). Can be done as part of the useOnSessionPayment dedup (1.3).
Two changes that need gate flips in LaunchDarkly or minor config changes, but no backend code. Can ship as soon as gates are confirmed.
Nine products moving from Multisku to Unified Checkout. Each product has a per-product feature gate. Independent of the Deferred Collection chain — runs in parallel. Full details in the Smart Checkout spec.
| # | Product | Gate | Complexity | Notes |
|---|---|---|---|---|
| 1 | Workers Paid | deseng-workers-unified-checkout | Medium | Usage-based pricing |
| 2 | Pages | (needs gate) | Low | Fixed pricing |
| 3 | R2 | billing-unified-checkout-r2 | Medium | Usage-based pricing |
| 4 | Stream | billing-unified-checkout-stream | Medium | Usage-based pricing |
| 5 | Images | billing-unified-checkout-images | Low | Fixed pricing |
| 6 | Durable Objects | (needs gate) | Medium | Usage + fixed |
| 7 | Queues | (needs gate) | Low | Fixed pricing |
| 8 | D1 | (needs gate) | Low | Fixed pricing |
| 9 | Vectorize | (needs gate) | Low | Fixed pricing |
Full UC expansion details, per-product flow, and the invariant enforcement plan live in the parent spec. This section covers only the frontend implementation perspective.
The two-button problem: users see two invoices (original + synthetic consolidated) and don't know which to pay. The PayOutstandingBalancePage has a gate split — billing-use-pay-bad-debt-endpoint routes between consolidated and per-invoice payment. The UX doesn't explain the difference.
Gate ON → /pay-bad-debt (consolidated). Gate OFF → /pay-invoice (per-invoice). Bad debt skips invoice selection. Nothing explains WHY or WHAT the amount represents.
Add clear copy explaining the consolidated amount. Suppress confusing individual Pay buttons. Show breakdown on the outstanding balance card.
Two changes that need backend work or API shape confirmation before the frontend can implement. Prep work can start now.
"Uncollectible" appears nowhere in stratus. Dashboard shows no special state. Need: badge, explanation, CTA. Requires API shape confirmation.
6 consumer locations become unconditional. Code change is trivial. Test matrix is not: 6 paths × 3 states × 2 checkouts. Write stubs now.
Key files in stratus that these changes touch. All paths relative to src/apps/dash/react/.
| Area | File | Changes |
|---|---|---|
| Banners | ../../notifications.tsx | Consolidate variants |
| Banners | common/components/NewFeatureNotifications.tsx | Simplify filter chain |
| Error 1210 | pages/billing/checkout/components/view/PaymentCheckoutView.tsx | Add 1210 handler |
| Error 1210 | pages/checkout/hooks/useSubmitPurchase.ts | Add 402/1210 handler |
| Hook dedup | pages/billing/checkout/hooks.ts | Extract → shared hook |
| Hook dedup | pages/checkout/components/payment/hooks/useOnSessionPayment.ts | Extract → shared hook |
| Hook dedup | pages/checkout/components/purchase/PurchaseForm.tsx | Remove duplicated logic |
| Multi-sub | pages/checkout/hooks/useSubmitPurchase.ts | Iterate client_secrets |
| Bad debt | pages/billing/pay-outstanding-balance/components/PayOutstandingBalancePage.tsx | Add explanatory copy |
| Bad debt | pages/billing/invoices/components/OutstandingBalanceCard.tsx | Add breakdown display |
| Bad debt | pages/billing/invoices/components/InvoicesCard.tsx | Suppress confusing Pay |
| UC expansion | pages/checkout/ (various) | Per-product adapters |
| Selectors | common/selectors/billingSelectors.ts | Simplify gate combos |
Recommended execution order based on impact, effort, and dependency. The Tier 1 items can start immediately. UC expansion runs in parallel from start.
| Change | Effort | Impact | Backend Dep? | Tickets Δ | Prep Value |
|---|---|---|---|---|---|
| Error 1210 handler | 2–3 days | High | None | −122/mo | PM gate removal |
| Banner consolidation | ~1 week | Medium | None | indirect | Clarity |
| Hook dedup + multi-sub | ~1 week | Low direct | None | — | PM gate removal |
| UC expansion (9 products) | ~9 weeks | High | Gate flips | — | Multisku sunset |
| Bad debt pay clarity | ~1 week | High | Gate confirm | indirect | Honest invoicing |
| PM gate removal prep | ~1 week | Future-high | Test only | — | DC readiness |
| # | Decision | Owner | Context |
|---|---|---|---|
| 1 | Are billing-stripe-bad-debt-payment-enabled and billing-pay-my-bill-enabled fully rolled out? | Frontend | Determines if v1/v2 banner variants are dead code |
| 2 | Which products need new UC gates vs. already have them? | Frontend | R2, Stream, Images, Workers have gates. Pages, DO, Queues, D1, Vectorize may not. |
| 3 | Can the useOnSessionPayment dedup use the UC API (callback-based) as the canonical pattern? | Frontend | Legacy consumers (4 call sites) need migration to callback API |
| 4 | Is billing-use-pay-bad-debt-endpoint fully rolled out? | Frontend | If yes, the per-invoice path in PayOutstandingBalance is dead code |
| 5 | Multisku sunset timeline — products remaining after UC Q2 cohort? | Frontend | Determines whether Multisku can be decommissioned in Q2 or requires a Q3 tail |
| 6 | Error 1210 analytics — is the Sparrow event firing today? | Frontend | Need baseline measurement before building the handler |
Six of eight changes need zero backend work. The error 1210 handler and banner consolidation can ship next week. The hook dedup makes PM gate removal mechanical. UC expansion runs in parallel for the full quarter.