Skip to content

feat(web/billing): show all tiers + quotas from the sidecar catalog (SSOT)#340

Open
jiashuoz wants to merge 3 commits into
mainfrom
feat/billing-tier-quota-comparison
Open

feat(web/billing): show all tiers + quotas from the sidecar catalog (SSOT)#340
jiashuoz wants to merge 3 commits into
mainfrom
feat/billing-tier-quota-comparison

Conversation

@jiashuoz

Copy link
Copy Markdown
Member

What

Reworks the in-app billing page (web/src/app/(app)/billing/page.tsx) to show every plan tier and its quota clearly, sourced from the billing sidecar's single source of truth instead of a hardcoded list.

Before

  • Hardcoded a 2-tier catalog (Pro + Scale only — Free never shown) that could silently drift from e2a-ops/billing/internal/plans/plans.go.
  • No side-by-side quota comparison; free users got a picker, paid users only "Manage billing".

After

  • Fetches GET /api/billing/plan (the sidecar SSOT) and renders all tiers (Free/Pro/Scale) with agents / domains / messages-per-month / storage + price.
  • Current tier highlighted with a badge.
  • Per-tier CTAs: free users Upgrade via Checkout; active subscribers Switch / Downgrade via the Stripe Billing Portal (Stripe owns proration).
  • Self-host (no NEXT_PUBLIC_BILLING_API): plan comparison + upgrade affordances hidden; usage still renders.
  • Graceful degradation: if the catalog fetch fails, a retry notice shows and the usage card still renders.
  • Removes the hardcoded PLAN_CATALOG and its "keep both files in sync" drift hazard.

Scope / non-goals

  • Frontend only (single repo). The optional "Cancels on … vs Renews on …" backend change (needs an e2a-ops migration + webhook field) is deferred to a follow-up.
  • No new tiers/prices, no in-app plan switching (paid changes go through the portal), marketing pricing page untouched.

Tests

  • New page.catalog.test.tsx: all-tier render + quota/price formatting, current-tier marking, upgrade→Checkout (asserts plan in POST body), switch/downgrade→portal routing, catalog-fetch-failure degradation.
  • Extended page.test.tsx: Plans section stays hidden on self-host.
  • Full web suite green (264 tests), eslint + tsc --noEmit clean, production next build prerenders /billing.

🤖 Generated with Claude Code

jiashuoz and others added 3 commits June 28, 2026 12:00
… (SSOT)

The billing page hardcoded a 2-tier catalog (Pro + Scale only, Free
omitted) that could drift from plans.go. Replace it with a per-tier
quota comparison sourced from GET /api/billing/plan — the billing
sidecar's single source of truth — so the dashboard can never lag the
caps the webhook actually provisions.

- Fetch the plan catalog (gated on NEXT_PUBLIC_BILLING_API; self-host
  with no sidecar skips it and shows usage only).
- Render every tier (Free/Pro/Scale) with agents/domains/messages/
  storage + price; highlight the current tier with a badge.
- CTAs: free users upgrade via Checkout; active subscribers switch or
  downgrade via the Stripe Billing Portal.
- Degrade gracefully when the catalog fetch fails (retry notice, usage
  still renders).
- Drop the hardcoded PLAN_CATALOG and its drift-warning comment.

Tests: new page.catalog.test.tsx covers catalog render, current-tier
marking, upgrade→Checkout (with plan body), switch/downgrade→portal,
and catalog-fetch degradation; existing page.test.tsx extended to
assert the Plans section stays hidden on self-host.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#339 deleted the duplicate plugins/e2a/.mcp.json (the e2a server is
declared inline in .claude-plugin/plugin.json) but left the validator's
section 5 reading the file unconditionally. readJSON() turns the ENOENT
into a validation failure, so the "Plugin manifests" job has been red on
main since that commit.

Guard the read with existsSync so the standalone manifest is validated
only when present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-ups:
- PlanCTA.onClick is now required (every non-null CTA always supplies
  one); drop the now-dead `|| !cta.onClick` button guard.
- ctaFor: when the current plan can't be determined (both sidecar
  current.code and OSS plan_code empty), render no plan-change buttons
  rather than risk offering Checkout for a plan the user may hold.
  Adds a regression test for the fail-safe.
- Document that max_storage_bytes is an int64 server-side but safe as a
  JS number (largest cap ~100 GiB << 2^53).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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