Skip to content

feat(web): inbox add-on purchase card on the billing page - #960

Merged
jiashuoz merged 2 commits into
mainfrom
feat/web-addon-purchase
Aug 29, 2026
Merged

feat(web): inbox add-on purchase card on the billing page#960
jiashuoz merged 2 commits into
mainfrom
feat/web-addon-purchase

Conversation

@jiashuoz

Copy link
Copy Markdown
Member

Closes the last mile of pricing v1: the hosted sidecar sells the $2/mo inbox add-on but the dashboard had no purchase UI.

Adds an Add-on section to /billing, entirely catalog-driven from GET /api/billing/plan (price per unit, per-unit grants, quantity ceiling, current stored quantity — no client-side prices or Stripe IDs):

  • Quantity stepper (0…max_quantity) + a single action button posting the desired total quantity to POST /api/billing/addon.
  • {url} response → redirect to hosted Stripe Checkout (no-subscription path; CSRF-safe POST like the existing checkout/portal calls).
  • {updated:true} response → in-place subscription mutation; shows a provisioning notice and fast-polls until current.addon_quantity reflects the change (mirrors the existing post-Checkout reconcile pattern, same bounded deadline).
  • Free plan + lifts_free_daily_cap → surfaces the "removes the daily send cap" hint.
  • Fail-safe hidden: no addon entry in the payload (older sidecar) or no NEXT_PUBLIC_BILLING_API (self-host) → card doesn't render.

Tests: page.addon.test.tsx — render-from-catalog, hidden-when-absent, checkout routing with body assertion, in-place update + pending notice, equal-quantity no-op disable, error path re-enable. Full web suite 892/892 green; tsc clean; prod build clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ScEpytuD7GvaXEssvJ2W32

jiashuoz and others added 2 commits August 28, 2026 23:13
The billing sidecar has sold the $2/mo inbox add-on since pricing v1
(POST /api/billing/addon, catalog in GET /api/billing/plan) but the
dashboard had no way to buy it. Adds an add-on section to /billing:
quantity stepper bounded by the catalog's max_quantity, priced and
described entirely from the sidecar payload (no client-side price or
Stripe data), POSTing the desired total quantity. {url} responses
redirect to hosted Checkout (no-subscription path); {updated:true}
responses show a provisioning notice and fast-poll until the stored
quantity matches, mirroring the post-Checkout reconcile machinery.

Hidden when the payload has no addon entry (older sidecar) and on
self-host (no NEXT_PUBLIC_BILLING_API), like the rest of the billing
surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScEpytuD7GvaXEssvJ2W32
Applies the independent + adversarial review of the add-on purchase
card, all in the money path:

- Lock the action button through the whole provisioning window
  (webhook pending) — it previously re-enabled immediately after
  {updated:true}, letting a second click issue a duplicate charging
  POST while the first was still settling.
- Confirm in-place increases with the computed new monthly total
  before POSTing (the one money action with no Stripe-hosted review
  step), and show a "New total" preview whenever the staged quantity
  diverges — up to $2,000/mo was previously one unlabeled click.
- Stamp a fresh reconcile deadline per update: a second update issued
  during the first's window inherited the old deadline and could
  instantly show a false timeout.
- Don't discard a newer staged edit when the webhook resolves an older
  one, and surface a notice when the server quantity moves out from
  under an in-progress edit (lost-update on total-quantity semantics).
- Clearing the number input no longer stages quantity 0 (a one-click
  cancel-everything); it holds the previous value until a digit arrives.
- Reset actionPending on bfcache restore — Back from Checkout
  previously resurrected it and froze every billing control.
- Match the sidecar's hasReusableSubscription list exactly for the
  Buy/Update label (pending_subscription_event and incomplete mutate
  in place — "Buy" implied a Stripe review page that never comes).
- Structural free-tier detection for the daily-cap hint (price <= 0,
  not a hardcoded plan code); neutral post-Checkout copy (the
  ?status=success marker is shared by plan and add-on purchases);
  friendlier 503 message; correct contract-violation error text;
  stepper group semantics + inputMode for a11y.

Tests: suite grown 6 → 12 including fake-timer coverage of the sync
state machine (webhook lands → unlock; timeout notice), ceiling/floor
clamps, cleared-input guard, confirm decline, and checkout-branch
negative assertions. Full web suite 898/898.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScEpytuD7GvaXEssvJ2W32
@jiashuoz
jiashuoz merged commit 679eb17 into main Aug 29, 2026
29 checks passed
@jiashuoz
jiashuoz deleted the feat/web-addon-purchase branch August 29, 2026 18:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant