feat(web): inbox add-on purchase card on the billing page - #960
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 fromGET /api/billing/plan(price per unit, per-unit grants, quantity ceiling, current stored quantity — no client-side prices or Stripe IDs):max_quantity) + a single action button posting the desired total quantity toPOST /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 untilcurrent.addon_quantityreflects the change (mirrors the existing post-Checkout reconcile pattern, same bounded deadline).lifts_free_daily_cap→ surfaces the "removes the daily send cap" hint.addonentry in the payload (older sidecar) or noNEXT_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