Skip to content

feat(canvas): /pricing route with plan selector + Stripe checkout - #260

Merged
HongmingWang-Rabbit merged 1 commit into
mainfrom
feat/pricing-page
Apr 15, 2026
Merged

feat(canvas): /pricing route with plan selector + Stripe checkout#260
HongmingWang-Rabbit merged 1 commit into
mainfrom
feat/pricing-page

Conversation

@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor

Summary

Adds a public `/pricing` route with a three-tier plan selector and working Stripe Checkout integration against the control plane's existing `/cp/billing/checkout` endpoint.

What ships

  • `src/lib/billing.ts` — canonical `plans` metadata + `startCheckout` / `openBillingPortal` wrappers
  • `src/components/PricingTable.tsx` — client component, lazy session probe on first click (no probe for anonymous browsers)
  • `src/app/pricing/page.tsx` — server-rendered shell with SEO metadata, legal footer, support email
  • 19 new Vitest cases: 10 billing helper (plan list shape, checkout POST body, portal URL, error propagation) + 9 PricingTable UI (render, CTA dispatch for each state, disabled-while-loading, ARIA alert surface)

CTA dispatch matrix

State Behavior
Free plan Redirect to signup (no session probe)
Paid + anonymous Redirect to signup
Paid + authed + tenant slug POST `/cp/billing/checkout`, redirect to Stripe URL
Paid + authed + NO tenant slug Inline ARIA alert: "pick an org first"
`fetchSession` network error Fail-closed: treat as anonymous, bounce to signup
`startCheckout` 4xx/5xx Surface the error in the alert banner

Design notes

  • Plan data is static in `billing.ts`, not API-fetched. Price changes require a deploy either way.
  • Starter is flagged `highlighted: true` so the middle card gets the "Most popular" visual. One source of truth.
  • Session probe is lazy (on click) so anonymous visitors don't generate a `/cp/auth/me` request just reading the page.

AuthGate interaction

  • Apex (no tenant slug) — AuthGate passthrough, `/pricing` renders freely for anon visitors
  • Tenant subdomain — AuthGate still bounces anons to login before reaching `/pricing`; this is correct UX for "already logged in, want to upgrade my own org"

Coordination with other work

Test plan

  • `vitest run` — all 470 canvas tests green (453 existing + 17 new)
  • `tsc --noEmit` clean
  • Visit `/pricing` in a browser, click each CTA, verify dispatch
  • Set up Stripe test-mode + `STRIPE_PRICE_STARTER` / `STRIPE_PRICE_PRO` env vars and run through a real test checkout
  • Confirm the success_url query param (`?checkout=success`) lands on the pricing page after a successful Stripe test purchase

🤖 Generated with Claude Code

Adds a public /pricing route the apex + tenant canvas can both serve.
Three-tier plan cards (Free, Starter, Pro) with per-plan CTA buttons
that dispatch correctly regardless of the user's state:

  Free              → redirect to signup
  Anonymous + paid  → redirect to signup (Stripe opens post-auth)
  Authed + paid     → POST /cp/billing/checkout, redirect to Stripe URL
  No tenant slug    → inline error ("pick an org first")
  Network failures  → surfaced in an ARIA alert banner

Files:
- src/lib/billing.ts — plan metadata + startCheckout + openBillingPortal
  wrappers over /cp/billing/{checkout,portal}
- src/components/PricingTable.tsx — client component, lazy session
  probe on first CTA click (no probe for anonymous browsers)
- src/app/pricing/page.tsx — server-rendered shell with SEO metadata,
  links to legal pages in the footer
- Tests: 10 billing helper tests + 9 PricingTable tests (17 total,
  additional ones cover the plan-list canonical order)

Design notes:
- The pricing data (features + prices) is a static const in billing.ts,
  not fetched from the API. Changing prices requires a deploy — which
  we'd need to do anyway for tier definition changes.
- PLAN_ID 'starter' is flagged highlighted=true so the middle card gets
  the 'Most popular' visual treatment. One source of truth; test locks it.
- Session probe is lazy (first CTA click, not mount) so anonymous
  visitors don't generate a /cp/auth/me request just to read the page.

AuthGate interaction:
- On apex (no tenant slug), AuthGate passthrough — /pricing renders freely
- On tenant subdomain, AuthGate still bounces anonymous users to login
  before reaching /pricing — this is the correct UX for the "I'm already
  logged in and want to upgrade my own org" flow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@HongmingWang-Rabbit
HongmingWang-Rabbit marked this pull request as ready for review April 15, 2026 20:48
@HongmingWang-Rabbit
HongmingWang-Rabbit merged commit 5940de6 into main Apr 15, 2026
5 of 6 checks passed
@HongmingWang-Rabbit
HongmingWang-Rabbit deleted the feat/pricing-page branch April 15, 2026 20:48
molecule-ai Bot pushed a commit that referenced this pull request Apr 21, 2026
feat(canvas): /pricing route with plan selector + Stripe checkout
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