Skip to content

feat(billing): Stripe Customer Portal route + email + /thanks link#537

Merged
blove merged 1 commit into
mainfrom
claude/customer-portal
May 25, 2026
Merged

feat(billing): Stripe Customer Portal route + email + /thanks link#537
blove merged 1 commit into
mainfrom
claude/customer-portal

Conversation

@blove
Copy link
Copy Markdown
Contributor

@blove blove commented May 25, 2026

Summary

Buyers can self-serve subscription management (cancel, switch billing cycle, update card, view invoices) via Stripe's hosted Customer Portal.

  • New `/api/portal/session` route (GET + POST) resolves a customer id from a Checkout session or a direct `cus_…` and redirects to a freshly-minted Stripe portal session
  • `/thanks?session_id=cs_…` now renders a "Manage subscription" button alongside the existing CTAs
  • License email gains a "Manage subscription" link (HTML + text) using the customer id directly — works even after the original `/thanks` URL expires

Operational follow-up

Enable Customer Portal in the Stripe dashboard (Settings → Billing → Customer portal) for both test and live modes. Configuration includes:

  • What features to expose (cancel, update plan, payment method, invoices)
  • Branding (logo, colors)
  • Return URL is set per-request to /thanks

Test plan

  • minting-service tests pass (LicenseEmailVars + handlers.ts wiring)
  • Website production build green
  • Smoke: visit /thanks?session_id= → click "Manage subscription" → portal opens → cancel works

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel Bot commented May 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
threadplane Ready Ready Preview, Comment May 25, 2026 4:26pm

Request Review

Lets buyers self-serve subscription management (cancel, switch
monthly↔annual, update card, view invoices) via Stripe's hosted Customer
Portal.

apps/website/src/app/api/portal/session/route.ts (NEW)
- POST + GET handlers
- Resolves a customer id from either:
  - session_id (Checkout Session) → fetches the session and reads .customer
  - customer_id (cus_…) directly — used by email links
- Creates a Stripe billingPortal session and 303-redirects to its URL

apps/website/src/app/thanks/page.tsx
- When ?session_id=cs_… is present in the success URL, render a
  "Manage subscription" button alongside "Installation & licensing"
- Page is now async so we can read the searchParams

apps/minting-service/src/lib/email.ts
- LicenseEmailVars gains required stripeCustomerId
- Both text and HTML bodies of the license email now include a
  "Manage subscription" link to /api/portal/session?customer_id=…
- portalUrl() reads PORTAL_BASE_URL when set (preview deploys),
  defaults to https://threadplane.ai

apps/minting-service/src/lib/handlers.ts
- Pass stripeCustomerId in the email vars at mint time

Tests
- email.spec fixtures gain stripeCustomerId on every renderLicenseEmail call
- handlers.spec uses expect.objectContaining for vars so the new field
  doesn't tighten existing assertions

Out of band: enable the Customer Portal in the Stripe dashboard
(Settings → Billing → Customer portal) so the API call actually returns
a URL. Test mode and live mode have separate portal configs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@blove blove force-pushed the claude/customer-portal branch from 4b4cb10 to 128b118 Compare May 25, 2026 16:23
@blove blove merged commit 30f0477 into main May 25, 2026
23 checks passed
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