Repository navigation
Add Stripe customer portal for Pro billing management - #7443
Conversation
GET /api/billing/portal resolves the current Stack user with the same cookie semantics as checkout (including the anonymous purchaser fallback), looks up their Stripe customer strictly by stackUserId, and 302s to a Stripe billing portal session; every failure path redirects safely and captures to Sentry. Manage billing links appear for Pro users on /billing/success, /app-pricing, the localized pricing page, and the native Settings Account card, all through the shared presenter and the same billing-origin resolution as checkout. Localized en + ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds Pro entitlement and billing-portal handling to the macOS account flow and to the web pricing and billing pages, with new portal routing, localized billing copy, and tests for the updated flows. ChangesmacOS Account and billing flow
Web billing portal and pricing flow
Estimated code review effort: 3 (Moderate) | ~35 minutes Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (4 errors)
✅ Passed checks (21 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PricingPlansScreen.swift grew 5 lines for the Manage billing action. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1eaefdc2af
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| {snapshot.isPro ? ( | ||
| <div className="space-y-2"> | ||
| <DisabledButton>{t("currentPlan")}</DisabledButton> | ||
| <SecondaryLink href="/api/billing/portal"> | ||
| {t("manageBilling")} |
There was a problem hiding this comment.
Gate the Stripe portal on Stripe-backed Pro state
When a Pro entitlement comes from the legacy Stack product path (the checkout route still supports Stack fallback, and resolveProPlanStatus treats listProducts() as active Pro), snapshot.isPro is true here even though there is no stripe_customers row. The new /api/billing/portal route only looks up stripeCustomers and redirects to billing=unavailable when that row is absent, so existing Stack-backed Pro subscribers now see a Manage billing link that cannot manage or cancel their subscription. This branch needs to distinguish Stripe-backed Pro from legacy Stack Pro, or provide the appropriate Stack billing fallback.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c738561. Configure here.
|
|
||
| const status = await resolveProPlanStatus(user); | ||
| return { isPro: status.isPro }; | ||
| } |
There was a problem hiding this comment.
Signed-in users miss Pro UI
High Severity
The new currentPlanSnapshot resolves the viewer with only anonymous-if-exists, so a normal Stack session from pricing-page cookies is skipped. Signed-in Pro users keep seeing the upgrade CTA instead of “Current plan” and “Manage billing”, unlike /api/billing/portal, which tries return-null first.
Reviewed by Cursor Bugbot for commit c738561. Configure here.
Greptile SummaryThis PR adds a self-serve Stripe billing management flow for Pro subscribers — a new
Confidence Score: 5/5Safe to merge; the portal route, plan refresh, and error fallbacks are all well-tested and follow the established checkout pattern. The portal route correctly gates on both Stack and Stripe configuration, enforces the stripe_customers row before creating a session, and captures errors without exposing internals. The one subtle logic issue is in refreshBillingPlan(): if currentTokens() throws, the request is sent unauthenticated and isProActive is silently reset to false, causing a Pro subscriber to briefly see "Upgrade…" in Settings — but this is an edge case limited to transient token-refresh failures and self-heals on next open. All critical paths (user resolution, session creation, error handling, origin resolution) have matching tests. Sources/Auth/HostAccountFlow.swift — the try?-then-continue pattern in refreshBillingPlan() can produce incorrect plan state during token refresh failures. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant U as User
participant W as Web (/api/billing/portal)
participant S as Stack Auth
participant DB as stripe_customers DB
participant SP as Stripe Portal API
U->>W: GET /api/billing/portal
W->>W: "isStackConfigured() && isStripeBillingConfigured()"
alt not configured
W-->>U: "302 /pricing?billing=unavailable"
end
W->>S: "getUser({ or: "return-null" })"
alt no signed-in user
W->>S: "getUser({ or: "anonymous-if-exists[deprecated]" })"
alt no anonymous purchaser either
W-->>U: 302 /pricing
end
end
W->>DB: "SELECT id FROM stripe_customers WHERE stackUserId = user.id LIMIT 1"
alt no customer row
W-->>U: "302 /pricing?billing=unavailable"
end
W->>SP: "billingPortal.sessions.create({ customer, return_url: /pricing })"
alt Stripe error
W->>W: captureBillingError (+ portal config flag)
W-->>U: "302 /pricing?billing=error"
end
W-->>U: 302 → session.url (Stripe hosted portal)
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant U as User
participant W as Web (/api/billing/portal)
participant S as Stack Auth
participant DB as stripe_customers DB
participant SP as Stripe Portal API
U->>W: GET /api/billing/portal
W->>W: "isStackConfigured() && isStripeBillingConfigured()"
alt not configured
W-->>U: "302 /pricing?billing=unavailable"
end
W->>S: "getUser({ or: "return-null" })"
alt no signed-in user
W->>S: "getUser({ or: "anonymous-if-exists[deprecated]" })"
alt no anonymous purchaser either
W-->>U: 302 /pricing
end
end
W->>DB: "SELECT id FROM stripe_customers WHERE stackUserId = user.id LIMIT 1"
alt no customer row
W-->>U: "302 /pricing?billing=unavailable"
end
W->>SP: "billingPortal.sessions.create({ customer, return_url: /pricing })"
alt Stripe error
W->>W: captureBillingError (+ portal config flag)
W-->>U: "302 /pricing?billing=error"
end
W-->>U: 302 → session.url (Stripe hosted portal)
Reviews (2): Last reviewed commit: "Merge main and regenerate the Swift leng..." | Re-trigger Greptile |
| "title": "Pricing", | ||
| "perMonth": "/month", | ||
| "perUserMonth": "/user/month", |
There was a problem hiding this comment.
Missing translations in 18 of 20 locale files
pricing.currentPlan, pricing.manageBilling, and billingSuccess.manageBilling are added only to en.json and ja.json, but web/messages/ contains 20 locales (ar, bs, da, de, es, fr, it, km, ko, no, pl, pt-BR, ru, th, tr, uk, zh-CN, zh-TW are untouched). The localized pricing page at web/app/[locale]/pricing/page.tsx calls t("currentPlan") and t("manageBilling") via getTranslations, so non-en/ja visitors see the raw key name instead of translated text. Worse, billingSuccessMessages() in web/app/billing/success/page.tsx dynamically imports the locale file and casts billingSuccess directly to BillingSuccessMessages—the new manageBilling field will be undefined at runtime for any locale other than en/ja, so the "Manage billing" button renders with no text for the majority of supported languages.
Rule Used: Flag production user-facing text that is not fully... (source)
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/Auth/HostAccountFlow.swift`:
- Around line 91-117: Add unit tests for HostAccountFlow.refreshBillingPlan() to
cover each decision path: no currentUser should immediately set isProActive
false, a non-2xx HTTPURLResponse from URLSession.shared.data(for:) should also
clear it, a successful 2xx response should decode BillingPlanResponse and set
isProActive from decoded.isPro, and any thrown error should fall back to false.
Use the refreshBillingPlan() method and its isProActive state changes as the
main assertions, and mock coordinator.currentTokens() plus the URLSession
response/data to exercise these branches.
In `@web/app/`[locale]/pricing/page.tsx:
- Around line 272-285: The `currentPlanSnapshot()` logic is duplicated in
multiple pricing pages and should be moved to a single shared source of truth.
Extract the existing
`isStackConfigured`/`ANONYMOUS_IF_EXISTS`/`resolveProPlanStatus` flow from
`currentPlanSnapshot()` into one reusable helper (for example, a shared
billing/pro or lib helper), then update both pricing page implementations to
call that helper instead of keeping separate copies.
- Line 35: The pricing page’s current plan lookup is not protected from
server-side failures, so `currentPlanSnapshot()` can crash `/pricing` when
`getStackServerApp().getUser()` or `resolveProPlanStatus()` throws. Update
`currentPlanSnapshot()` to wrap the user/pro status resolution in a try/catch,
handle any `user.update()`-triggered metadata drift errors safely, and return a
fail-closed result of `{ isPro: false }` on failure.
In `@web/app/api/billing/portal/route.ts`:
- Line 16: The ANONYMOUS_IF_EXISTS sentinel is duplicated across the billing
portal route and the pricing page, so move the shared literal into a single
exported constant in web/app/lib/stack.ts and import it in both call sites.
Update the uses in the route handler and the pricing page to reference that
shared symbol instead of redefining the string inline, keeping the existing
sentinel value consistent in one place.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: c83c8b73-0a4e-47a0-b849-58257f70f722
⛔ Files ignored due to path filters (1)
.github/swift-file-length-budget.tsvis excluded by!**/*.tsv
📒 Files selected for processing (18)
Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Account/AccountFlow.swiftPackages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Account/ProUpgradeCard.swiftResources/Localizable.xcstringsSources/Auth/AuthEnvironment.swiftSources/Auth/HostAccountFlow.swiftSources/PricingPlansScreen.swiftcmuxTests/AuthEnvironmentTests.swiftskills/cmux-billing/SKILL.mdweb/app/[locale]/pricing/page.tsxweb/app/api/billing/portal/route.tsweb/app/app-pricing/page.tsxweb/app/billing/success/page.tsxweb/messages/en.jsonweb/messages/ja.jsonweb/tests/app-pricing-page.test.tsxweb/tests/billing-portal-route.test.tsweb/tests/billing-success-page.test.tsxweb/tests/pricing-page.test.tsx
| func refreshBillingPlan() async { | ||
| guard coordinator.currentUser != nil else { | ||
| isProActive = false | ||
| return | ||
| } | ||
| var request = URLRequest(url: AuthEnvironment.apiBaseURL.appendingPathComponent("api/billing/plan")) | ||
| request.httpMethod = "GET" | ||
| request.setValue("application/json", forHTTPHeaderField: "Accept") | ||
|
|
||
| if let tokens = try? await coordinator.currentTokens() { | ||
| request.setValue("Bearer \(tokens.accessToken)", forHTTPHeaderField: "Authorization") | ||
| request.setValue(tokens.refreshToken, forHTTPHeaderField: "X-Stack-Refresh-Token") | ||
| } | ||
|
|
||
| do { | ||
| let (data, response) = try await URLSession.shared.data(for: request) | ||
| guard let http = response as? HTTPURLResponse, | ||
| (200..<300).contains(http.statusCode) else { | ||
| isProActive = false | ||
| return | ||
| } | ||
| let decoded = try JSONDecoder().decode(BillingPlanResponse.self, from: data) | ||
| isProActive = decoded.isPro | ||
| } catch { | ||
| isProActive = false | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
Consider adding unit coverage for refreshBillingPlan()'s branches.
This method decides whether the UI shows "Upgrade…" vs "Manage billing" and which action openBillingPortal()/openProUpgrade() fires — a correctness-critical entitlement check — but there's no test exercising the no-user / non-2xx / decode-success / thrown-error paths (only AuthEnvironmentTests covers URL resolution, not this fetch/decode logic).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/Auth/HostAccountFlow.swift` around lines 91 - 117, Add unit tests for
HostAccountFlow.refreshBillingPlan() to cover each decision path: no currentUser
should immediately set isProActive false, a non-2xx HTTPURLResponse from
URLSession.shared.data(for:) should also clear it, a successful 2xx response
should decode BillingPlanResponse and set isProActive from decoded.isPro, and
any thrown error should fall back to false. Use the refreshBillingPlan() method
and its isProActive state changes as the main assertions, and mock
coordinator.currentTokens() plus the URLSession response/data to exercise these
branches.
| const ENTERPRISE_CTA_URL = "/enterprise"; | ||
| const ANONYMOUS_IF_EXISTS = "anonymous-if-exists[deprecated]" as const; | ||
|
|
||
| export const dynamic = "force-dynamic"; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## pricing page\n'
sed -n '1,220p' web/app/'[locale]'/pricing/page.tsx | cat -n
printf '\n## billing pro service\n'
sed -n '1,260p' web/services/billing/pro.ts | cat -n
printf '\n## search for related symbols\n'
rg -n "currentPlanSnapshot|resolveProPlanStatus|syncProPlanMetadata|captureBillingError|getStackServerApp\(\)\.getUser" web -SRepository: manaflow-ai/cmux
Length of output: 25473
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '## pricing page'
sed -n '1,220p' web/app/'[locale]'/pricing/page.tsx | cat -n
echo
echo '## billing pro service'
sed -n '1,260p' web/services/billing/pro.ts | cat -n
echo
echo '## symbol search'
rg -n "currentPlanSnapshot|resolveProPlanStatus|syncProPlanMetadata|captureBillingError|getStackServerApp\(\)\.getUser" web -SRepository: manaflow-ai/cmux
Length of output: 25459
🏁 Script executed:
#!/bin/bash
set -euo pipefail
for f in web/app/'[locale]'/pricing/page.tsx web/services/billing/pro.ts; do
echo "===== $f ====="
awk 'NR>=1 && NR<=260 { print NR ":" $0 }' "$f"
echo
doneRepository: manaflow-ai/cmux
Length of output: 19002
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '260,310p' web/app/'[locale]'/pricing/page.tsx | cat -nRepository: manaflow-ai/cmux
Length of output: 943
Guard currentPlanSnapshot() on /pricing getStackServerApp().getUser() and resolveProPlanStatus() can throw and take down the public page, and resolveProPlanStatus() may call user.update() when metadata drifts. Wrap this in try/catch and fail closed to { isPro: false }.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/app/`[locale]/pricing/page.tsx at line 35, The pricing page’s current
plan lookup is not protected from server-side failures, so
`currentPlanSnapshot()` can crash `/pricing` when
`getStackServerApp().getUser()` or `resolveProPlanStatus()` throws. Update
`currentPlanSnapshot()` to wrap the user/pro status resolution in a try/catch,
handle any `user.update()`-triggered metadata drift errors safely, and return a
fail-closed result of `{ isPro: false }` on failure.
|
|
||
| async function currentPlanSnapshot(): Promise<{ isPro: boolean }> { | ||
| if (!isStackConfigured()) { | ||
| return { isPro: false }; | ||
| } | ||
|
|
||
| const user = await getStackServerApp().getUser({ or: ANONYMOUS_IF_EXISTS }); | ||
| if (!user) { | ||
| return { isPro: false }; | ||
| } | ||
|
|
||
| const status = await resolveProPlanStatus(user); | ||
| return { isPro: status.isPro }; | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Duplicate currentPlanSnapshot() implementation across pages.
Per the provided graph context, web/app/app-pricing/page.tsx defines an identical currentPlanSnapshot() (same body, same isStackConfigured/ANONYMOUS_IF_EXISTS/resolveProPlanStatus sequence). This is exactly the kind of duplicated correctness-critical "isPro" resolution the reliability/single-source-of-truth guideline warns about — two independent copies can silently drift when one is patched (e.g., the error-handling fix above) and not the other.
Extract this into a single shared helper (e.g. services/billing/pro.ts or a small lib/current-plan.ts) and have both pages call it.
As per coding guidelines, "For correctness-critical decisions in this PR... ensure you use a single authoritative, structured source of truth rather than name/title heuristics or 'best effort' fallbacks... When you have multiple possible sources... make one the authority."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/app/`[locale]/pricing/page.tsx around lines 272 - 285, The
`currentPlanSnapshot()` logic is duplicated in multiple pricing pages and should
be moved to a single shared source of truth. Extract the existing
`isStackConfigured`/`ANONYMOUS_IF_EXISTS`/`resolveProPlanStatus` flow from
`currentPlanSnapshot()` into one reusable helper (for example, a shared
billing/pro or lib helper), then update both pricing page implementations to
call that helper instead of keeping separate copies.
Source: Path instructions
| export const runtime = "nodejs"; | ||
| export const dynamic = "force-dynamic"; | ||
|
|
||
| const ANONYMOUS_IF_EXISTS = "anonymous-if-exists[deprecated]" as const; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Centralize the ANONYMOUS_IF_EXISTS sentinel instead of duplicating it.
This exact literal ("anonymous-if-exists[deprecated]") is redefined verbatim in web/app/[locale]/pricing/page.tsx. Per official Stack Auth docs, the publicly documented getUser({or}) options are only "return-null" | "redirect" | "throw" — this sentinel appears to be an internal/forked extension, which makes copy-pasting it across files riskier if the SDK ever changes the value.
getUser(options?: { or?: "return-null" | "redirect" | "throw" }): Promise<CurrentServerUser | null>
Export this as a single shared constant from web/app/lib/stack.ts and import it in both call sites.
♻️ Proposed fix
-const ANONYMOUS_IF_EXISTS = "anonymous-if-exists[deprecated]" as const;
+import { ANONYMOUS_IF_EXISTS } from "../../../lib/stack";Also applies to: 54-60
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/app/api/billing/portal/route.ts` at line 16, The ANONYMOUS_IF_EXISTS
sentinel is duplicated across the billing portal route and the pricing page, so
move the shared literal into a single exported constant in web/app/lib/stack.ts
and import it in both call sites. Update the uses in the route handler and the
pricing page to reference that shared symbol instead of redefining the string
inline, keeping the existing sentinel value consistent in one place.
Both this branch and the merged flags PR refreshed the budget file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cb8d10210a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| @MainActor | ||
| static func presentBillingPortal() { | ||
| NSWorkspace.shared.open(AuthEnvironment.billingPortalURL) |
There was a problem hiding this comment.
Authenticate native portal opens
When a Pro user is signed in only in the native app, this opens /api/billing/portal in the default browser without carrying the app's Stack tokens. The Settings row just determined Pro state via refreshBillingPlan() using bearer tokens, but the new portal route resolves the customer only from the browser Stack session/anonymous cookie (getUser in web/app/api/billing/portal/route.ts), so a browser with no matching cookie or a different account redirects to pricing/unavailable instead of Stripe and leaves the user unable to manage or cancel from Settings. Please create the portal session through an authenticated native request or otherwise bind this open to the current app user.
Useful? React with 👍 / 👎.


Customers need self-serve cancel/manage before the pricing flag flips on. GET /api/billing/portal creates a Stripe billing portal session for the current Stack user (customer id strictly from the stripe_customers row for the resolved stackUserId, anonymous purchaser fallback included) and redirects; failures land on safe /pricing banners with Sentry capture. Pro users get Manage billing links on the success page, /app-pricing, the localized pricing page, and the native Settings Account card via the shared presenter, with the portal URL following the same CMUX_BILLING_WWW_ORIGIN-then-app-web-origin resolution as checkout.
/fable loop: two judge rounds (origin-override parity + anonymous-fallback test coverage), 377 web tests green in sorted order, typecheck clean, en+ja localization audited.
🤖 Generated with Claude Code
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Medium Risk
Touches billing auth, Stripe customer lookup, and entitlement-driven UI across web and native; failures are mostly redirects, but wrong customer mapping or plan detection would affect who can manage subscriptions.
Overview
Adds self-serve Stripe billing management via a new
GET /api/billing/portalroute that resolves the Stack user (including anonymous purchaser fallback), loadsstripe_customersfor that user only, creates a portal session, and redirects—or sends users to/pricing?billing=…on failure with Sentry context.Web pricing is now plan-aware: localized
/pricingand/app-pricinguseresolveProPlanStatusto show a current-plan badge, disable upgrade CTAs for Pro, and link Manage billing to the portal; post-checkout/billing/successgets the same link. NewcurrentPlan/manageBillingstrings (en + ja).macOS:
AccountFlowgainsisProActive,refreshBillingPlan()(api/billing/plan), andopenBillingPortal();ProUpgradeCardrefreshes on identity change and switches copy/button between Upgrade and Manage billing.AuthEnvironment.billingPortalURLmirrors checkout origin rules (CMUX_BILLING_WWW_ORIGIN);ProUpgradePresenter.presentBillingPortal()opens it in the default browser.Tests cover the portal route (signed-in, anonymous fallback, misconfig), pricing pages, and portal URL resolution; billing skill doc updated.
Reviewed by Cursor Bugbot for commit cb8d102. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Adds a self-serve Stripe customer portal so Pro users can manage billing or cancel. Includes a new API route, “Manage billing” links across web and macOS, and safe fallbacks with correct origin resolution.
New Features
/api/billing/portal: resolves the current Stack user (with anonymous purchaser fallback), looks up theirstripe_customersbystackUserId, creates a Stripe Billing Portal session with return to/pricing, and 302s; unavailable/error paths redirect to/pricing?billing=…with error capture./billing/success,/app-pricing, and localized/[locale]/pricing(server-rendered viaresolveProPlanStatus).AccountFlowaddsrefreshBillingPlan(),openBillingPortal(), andisProActive;ProUpgradeCardauto-refreshes plan state and switches Upgrade vs Manage;ProUpgradePresenter.presentBillingPortal()opensAuthEnvironment.billingPortalURL.AuthEnvironmentresolves the portal URL withCMUX_BILLING_WWW_ORIGINoverride (else app web origin), matching checkout; tests cover origin override and anonymous fallback. Localized strings updated (en/ja).Migration
STRIPE_SECRET_KEY.CMUX_BILLING_WWW_ORIGINif the billing host differs; otherwise the app’s web origin is used.Written for commit cb8d102. Summary will update on new commits.
Summary by CodeRabbit