Repository navigation
Add cmux Pro pricing and one-click Stack checkout - #7143
Conversation
Free / Pro ($30/mo) / Enterprise tiers with a compare table, Cloud VM sizes table, and FAQ. Leans on cmux-native cloud value: Cloud VMs billed by active compute-hour, cmux Vault session backup + search, unlimited session history, a model gateway with usage/cost analytics, and hosted or self-hosted networking to reach your Mac from the iOS app. Enterprise adds SSO/SAML, self-hosting, audit logs, and SOC 2. Wires Pricing into nav, mobile drawer, and footer. Strings localized in en.json and ja.json. Note: the Pro CTA currently points at the download (no public checkout URL yet); PRO_CTA_URL is the single swap point when billing is live.
Vault (cloud session backup, cross-session search, unlimited cross-machine history) isn't shipped yet, so its pricing copy is hidden behind a single SHOW_VAULT flag in page.tsx. Flip to true to restore every Vault entry. - Pro Vault bullets moved to pricing.pro.vaultFeatures (spliced back when on) - Vault-dependent compare rows (session history, Vault backup, cross-session search) and the Vault FAQ marked "vault": true and filtered out - meta description has a no-Vault variant - en.json and ja.json kept in sync Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New Team tier between Pro and Enterprise: per-seat at $35/user/month, billed to the whole team on one invoice. Features: unified billing, centralized seat management, pooled Cloud VM hours, shared team rules/templates, team-wide model gateway with per-member analytics, centralized admin, priority support. - 4th tier card; tier grid is now md:grid-cols-2 lg:grid-cols-4 - Team column added to the compare table + a "Unified billing and seat management" row; Enterprise now builds on Team - perUserMonth string; team FAQ answer updated - en.json and ja.json kept in sync Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Drop the "Compare plans" section (heading, divider, and comparison table) and its now-dead helpers (ColumnHead, CompareCell, CompareRow type, compareRows read). The compare.* i18n data is left in place, unused. - Tier card row is now sticky under the site header: sticky top-12 (clears the 48px h-12 header) with z-20 (below the header's z-30) and a solid background. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tainer - Restore the comparison table (I had removed it) but without the "Compare plans" heading and the top divider, per request. - Make the table HEADER ROW sticky (sticky top-12 under the 48px h-12 site header, z-20 below the header's z-30), not the tier cards. Tier cards are no longer sticky. - Widen the page container max-w-5xl -> max-w-6xl (matches the site header). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous attempt didn't pin in WebKit: the overflow-x-auto wrapper became a scroll container on both axes, so the sticky header anchored to that div (which doesn't scroll) instead of the page. Remove the wrapper and switch the table to border-separate (border-spacing-0), which sticky table headers need; move row separators onto the cells. Verified on localhost: header pins at top:48px under the 48px h-12 site header. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pro product (user-scoped, yearly $240 listed first so the hosted purchase page pre-selects it; monthly $30) lives in the Stack project config. /api/billing/checkout signs the visitor in if needed and redirects to the hosted purchase page; /api/billing/confirm verifies the subscription on return and syncs clientReadOnlyMetadata.cmuxPlan, which existing VM entitlements already read. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a Pro billing and pricing flow across web and macOS: billing helpers, checkout/confirm routes, pricing page and links, VM reconciliation wiring, and native Pro upgrade entry points. Also updates unrelated localization copy in en.json and ja.json. ChangesPro subscription feature
Estimated code review effort: 4 (Complex) | ~60 minutes macOS Pro upgrade UI
Unrelated documentation and marketing copy edits
Estimated code review effort: 2 (Simple) | ~10 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant CheckoutRoute as /api/billing/checkout
participant StackApp as stackServerApp
participant ConfirmRoute as /api/billing/confirm
participant ProService as web/services/billing/pro.ts
User->>CheckoutRoute: GET /api/billing/checkout
CheckoutRoute->>StackApp: getUser()
alt no user
CheckoutRoute-->>User: redirect /handler/sign-in
else active Pro
CheckoutRoute->>ProService: syncProPlanMetadata(true)
CheckoutRoute-->>User: redirect /pricing?welcome=active
else needs checkout
CheckoutRoute->>StackApp: createCheckoutUrl(returnUrl=/api/billing/confirm)
CheckoutRoute-->>User: redirect to hosted checkout
User->>ConfirmRoute: GET /api/billing/confirm
ConfirmRoute->>ProService: hasActiveProSubscription (polled)
ConfirmRoute->>ProService: syncProPlanMetadata(isPro)
ConfirmRoute-->>User: redirect /pricing?welcome=success|pending
end
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (3 errors)
✅ Passed checks (22 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 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bb6aa5d9ff
ℹ️ 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".
|
|
||
| if (isPro) { | ||
| if (current === PRO_PLAN_ID) return; | ||
| metadata.cmuxPlan = PRO_PLAN_ID; |
There was a problem hiding this comment.
Reconcile lapsed Pro subscriptions before trusting metadata
When a Pro subscription later cancels or payment fails outside the checkout return flow, this persisted cmuxPlan is never cleared: VM auth reads only clientReadOnlyMetadata in web/services/vms/auth.ts, and the new code only calls syncProPlanMetadata from checkout/confirm. A user who lapses after being upgraded therefore keeps paid VM limits indefinitely until they happen to hit a billing sync path again; add a webhook or entitlement-time reconciliation before using this metadata as the durable source of truth.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR adds cmux Pro paid conversion: a
Confidence Score: 4/5Safe to merge with two targeted fixes: non-localized badge strings and the unguarded debug window controller in production source. Two issues in the macOS layer need attention before this reaches non-English users. ProBadgeStyle.text returns hardcoded "Get Pro" and "Pro" strings outside String(localized:) for the majority of badge variants — any persisted non-default style shows unlocalized text in the sidebar and titlebar on every locale. ProBadgeDebugWindowController ships in the release binary without a #if DEBUG class guard; its call site is debug-only so users never see the window, but the class and its ProBadgeDebugView subtree inflate release binaries unnecessarily and violate the no-debug-seam-in-production-source policy. The billing routes, checkout URL construction, subscription reconciliation, and PostHog feature flag wiring all look correct and are well-covered by the 17 new unit tests. Sources/ProBadgeStyle.swift — non-localized badge text strings and unguarded debug window controller class. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant App as macOS App
participant Web as /app-pricing
participant Checkout as /api/billing/checkout
participant Stripe as Stripe Checkout
participant Complete as /api/billing/complete
participant Confirm as /api/billing/confirm
participant Stack as Stack Auth
App->>Web: ProUpgradePresenter.present() opens /app-pricing (browser split)
Web->>Checkout: "GET /api/billing/checkout?plan=pro&cmux_external_browser=1"
Checkout->>Stack: getUser (or anonymous)
Checkout->>Stripe: "sessions.create(price, successUrl=/api/billing/complete)"
Stripe-->>Checkout: session.url
Checkout-->>App: redirect to Stripe hosted checkout (external browser)
App->>Stripe: user completes payment
Stripe-->>Complete: "redirect to /api/billing/complete?session_id=..."
Complete->>Stripe: sessions.retrieve(expand subscription+customer)
Complete->>Stack: recordCheckoutCompletion then syncProPlanMetadata(pro)
Complete-->>App: "redirect /billing/success?cmux_scheme=..."
App->>App: native callback deep link
note over Confirm: Legacy Stack path
App->>Confirm: GET /api/billing/confirm (Stack return URL)
loop up to 4 attempts x 1500ms
Confirm->>Stack: hasActiveProSubscription
end
Confirm->>Stack: syncProPlanMetadata(pro)
Confirm-->>App: "redirect /pricing?welcome=success or pending"
%%{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 App as macOS App
participant Web as /app-pricing
participant Checkout as /api/billing/checkout
participant Stripe as Stripe Checkout
participant Complete as /api/billing/complete
participant Confirm as /api/billing/confirm
participant Stack as Stack Auth
App->>Web: ProUpgradePresenter.present() opens /app-pricing (browser split)
Web->>Checkout: "GET /api/billing/checkout?plan=pro&cmux_external_browser=1"
Checkout->>Stack: getUser (or anonymous)
Checkout->>Stripe: "sessions.create(price, successUrl=/api/billing/complete)"
Stripe-->>Checkout: session.url
Checkout-->>App: redirect to Stripe hosted checkout (external browser)
App->>Stripe: user completes payment
Stripe-->>Complete: "redirect to /api/billing/complete?session_id=..."
Complete->>Stripe: sessions.retrieve(expand subscription+customer)
Complete->>Stack: recordCheckoutCompletion then syncProPlanMetadata(pro)
Complete-->>App: "redirect /billing/success?cmux_scheme=..."
App->>App: native callback deep link
note over Confirm: Legacy Stack path
App->>Confirm: GET /api/billing/confirm (Stack return URL)
loop up to 4 attempts x 1500ms
Confirm->>Stack: hasActiveProSubscription
end
Confirm->>Stack: syncProPlanMetadata(pro)
Confirm-->>App: "redirect /pricing?welcome=success or pending"
Reviews (33): Last reviewed commit: "Capture mocked-module originals by value..." | Re-trigger Greptile |
| for (let attempt = 0; attempt < VERIFY_ATTEMPTS; attempt++) { | ||
| if (attempt > 0) { | ||
| await new Promise((resolve) => setTimeout(resolve, VERIFY_SPACING_MS)); | ||
| } | ||
| // App-level lookup each attempt so no per-object store caching can | ||
| // return a stale product list mid-poll. | ||
| isPro = await hasActiveProSubscription({ | ||
| listProducts: (options) => | ||
| app.listProducts({ userId: user.id, ...options }), | ||
| }); | ||
| if (isPro) break; | ||
| } |
There was a problem hiding this comment.
Raw
setTimeout polling without cancellation-aware abstraction
The retry loop uses new Promise((resolve) => setTimeout(resolve, VERIFY_SPACING_MS)) to poll for Stripe subscription confirmation — up to 3 sleeps × 1 500 ms = 4.5 s of wall-clock blocking in the route handler. This is a retry wait for external-system readiness with no cancellation support: if the route handler is cancelled (edge timeout, client disconnect) mid-sleep, the outstanding setTimeout fires and the remaining awaits run against a dead request. The loop itself also has no tests — billing-pro.test.ts covers hasActiveProSubscription and syncProPlanMetadata but not the polling behavior. Per the no-hacky-sleeps rule, retry waits need either a real signal from the owning subsystem or a dedicated cancellation-aware abstraction with tests.
Rule Used: Flag fixed sleeps, delayed dispatch, timers, polli... (source)
|
|
||
| await syncProPlanMetadata(user, isPro); | ||
| return NextResponse.redirect( | ||
| new URL(isPro ? "/pro?welcome=1" : "/pro?welcome=pending", request.url), |
There was a problem hiding this comment.
The confirm route returns
?welcome=1 for a successful subscription, but ProWelcomeBanner only checks for "pending" and "active" — anything else falls through to welcomeSuccess. Using the opaque value "1" instead of a descriptive string like "success" is inconsistent with the other two values and makes the banner's logic fragile: a future new state added to the else-branch would accidentally show the success message.
| new URL(isPro ? "/pro?welcome=1" : "/pro?welcome=pending", request.url), | |
| new URL(isPro ? "/pro?welcome=success" : "/pro?welcome=pending", request.url), |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
6 issues found across 12 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="web/app/api/billing/confirm/route.ts">
<violation number="1" location="web/app/api/billing/confirm/route.ts:29">
P2: This fixed-sleep polling loop can keep the confirm route open for up to ~4.5 seconds before returning. In a request handler, this kind of timer-based wait is fragile under timeouts/load; a signal-driven reconciliation path (for example webhook/background sync) or an abort-aware retry helper would make this flow more reliable.</violation>
<violation number="2" location="web/app/api/billing/confirm/route.ts:34">
P1: Billing confirmation may fail before syncing Pro status because this adapter calls `listProducts` on `stackServerApp` instead of a Stack customer object. Re-fetching the user each poll and calling `freshUser.listProducts(options)` keeps the no-cache intent while using the documented payments API.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| // App-level lookup each attempt so no per-object store caching can | ||
| // return a stale product list mid-poll. | ||
| isPro = await hasActiveProSubscription({ | ||
| listProducts: (options) => |
There was a problem hiding this comment.
P1: Billing confirmation may fail before syncing Pro status because this adapter calls listProducts on stackServerApp instead of a Stack customer object. Re-fetching the user each poll and calling freshUser.listProducts(options) keeps the no-cache intent while using the documented payments API.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/app/api/billing/confirm/route.ts, line 34:
<comment>Billing confirmation may fail before syncing Pro status because this adapter calls `listProducts` on `stackServerApp` instead of a Stack customer object. Re-fetching the user each poll and calling `freshUser.listProducts(options)` keeps the no-cache intent while using the documented payments API.</comment>
<file context>
@@ -0,0 +1,44 @@
+ // App-level lookup each attempt so no per-object store caching can
+ // return a stale product list mid-poll.
+ isPro = await hasActiveProSubscription({
+ listProducts: (options) =>
+ app.listProducts({ userId: user.id, ...options }),
+ });
</file context>
| let isPro = false; | ||
| for (let attempt = 0; attempt < VERIFY_ATTEMPTS; attempt++) { | ||
| if (attempt > 0) { | ||
| await new Promise((resolve) => setTimeout(resolve, VERIFY_SPACING_MS)); |
There was a problem hiding this comment.
P2: This fixed-sleep polling loop can keep the confirm route open for up to ~4.5 seconds before returning. In a request handler, this kind of timer-based wait is fragile under timeouts/load; a signal-driven reconciliation path (for example webhook/background sync) or an abort-aware retry helper would make this flow more reliable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/app/api/billing/confirm/route.ts, line 29:
<comment>This fixed-sleep polling loop can keep the confirm route open for up to ~4.5 seconds before returning. In a request handler, this kind of timer-based wait is fragile under timeouts/load; a signal-driven reconciliation path (for example webhook/background sync) or an abort-aware retry helper would make this flow more reliable.</comment>
<file context>
@@ -0,0 +1,44 @@
+ let isPro = false;
+ for (let attempt = 0; attempt < VERIFY_ATTEMPTS; attempt++) {
+ if (attempt > 0) {
+ await new Promise((resolve) => setTimeout(resolve, VERIFY_SPACING_MS));
+ }
+ // App-level lookup each attempt so no per-object store caching can
</file context>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 `@web/app/`[locale]/components/site-header.tsx:
- Line 4: The mobile drawer’s Pro link in site-header.tsx is missing the same
badge-click analytics that the desktop pill and NavLinks Pro entry already send.
Update the drawer’s Pro link handler to use the same shared tracking
action/pattern as the other Pro entrypoints (the existing
posthog.capture("cmuxterm_pro_badge_clicked", ...) flow in site-header and
NavLinks), while still closing the drawer afterward. Ensure all Pro entrypoints
route through the same tracking logic so the mobile drawer does not silently
skip the event.
In `@web/app/api/billing/confirm/route.ts`:
- Around line 26-38: The polling in confirm route uses a fixed sleep-based retry
loop around hasActiveProSubscription, which violates the no-hacky-sleeps
guidance. Replace the wall-clock wait in the confirm handler with a real
completion signal or synchronous confirmation path from Stripe/Stack (for
example, a webhook-confirmed state transition or documented confirm call), and
keep the existing hasActiveProSubscription/app.listProducts lookup as the source
of truth only after that signal is available.
In `@web/messages/en.json`:
- Around line 2288-2313: The new cmux Pro copy introduces benefits that are
still attributed elsewhere to Founders Edition, creating conflicting product
messaging. Update the FAQ entries that mention early access and support
(especially the iOS, AI, and Cloud VMs references) so they consistently point to
the same offering name, or add an explicit explanation in the same localization
set describing how Founders Edition and Pro differ. Use the existing `pro` and
FAQ message keys in `web/messages/en.json` to keep the claims aligned.
🪄 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: 7a774335-8835-4152-b6c3-0c831905c65a
📒 Files selected for processing (12)
web/app/[locale]/components/nav-links.tsxweb/app/[locale]/components/pro-checkout-button.tsxweb/app/[locale]/components/pro-welcome-banner.tsxweb/app/[locale]/components/site-header.tsxweb/app/[locale]/page.tsxweb/app/[locale]/pro/page.tsxweb/app/api/billing/checkout/route.tsweb/app/api/billing/confirm/route.tsweb/messages/en.jsonweb/messages/ja.jsonweb/services/billing/pro.tsweb/tests/billing-pro.test.ts
| "use client"; | ||
|
|
||
| import { useTranslations } from "next-intl"; | ||
| import posthog from "posthog-js"; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Mobile drawer "Pro" link doesn't emit the badge-click event.
The desktop pill (line 68) and NavLinks' center-nav Pro link both fire posthog.capture("cmuxterm_pro_badge_clicked", ...), but the mobile drawer's Pro link (lines 124-130) only calls close, silently dropping analytics for a third Pro entry point. This undercounts Pro conversion-funnel data for mobile users specifically.
📊 Add tracking to the drawer entry point
<Link
href="/pro"
- onClick={close}
+ onClick={() => {
+ posthog.capture("cmuxterm_pro_badge_clicked", {
+ location: "mobile_drawer",
+ });
+ close();
+ }}
className="hover:text-foreground transition-colors py-1"
>
{t("pro")}
</Link>As per coding guidelines: "When a behavior is exposed through multiple entrypoints... implement one shared action/model path and verify every entrypoint that should invoke it. Do not patch one surface while leaving the others with duplicated logic."
Also applies to: 65-75, 124-130
🤖 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]/components/site-header.tsx at line 4, The mobile drawer’s
Pro link in site-header.tsx is missing the same badge-click analytics that the
desktop pill and NavLinks Pro entry already send. Update the drawer’s Pro link
handler to use the same shared tracking action/pattern as the other Pro
entrypoints (the existing posthog.capture("cmuxterm_pro_badge_clicked", ...)
flow in site-header and NavLinks), while still closing the drawer afterward.
Ensure all Pro entrypoints route through the same tracking logic so the mobile
drawer does not silently skip the event.
Source: Coding guidelines
| let isPro = false; | ||
| for (let attempt = 0; attempt < VERIFY_ATTEMPTS; attempt++) { | ||
| if (attempt > 0) { | ||
| await new Promise((resolve) => setTimeout(resolve, VERIFY_SPACING_MS)); | ||
| } | ||
| // App-level lookup each attempt so no per-object store caching can | ||
| // return a stale product list mid-poll. | ||
| isPro = await hasActiveProSubscription({ | ||
| listProducts: (options) => | ||
| app.listProducts({ userId: user.id, ...options }), | ||
| }); | ||
| if (isPro) break; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Fixed wall-clock polling to wait out Stripe confirmation violates the no-hacky-sleeps rule.
The loop waits VERIFY_SPACING_MS (1500ms) between up to 4 attempts purely on elapsed time, to make async Stripe/Stack confirmation "appear reliable" — this is the exact anti-pattern the repo's runtime guideline prohibits for production code: retry logic keyed to wall-clock time instead of a real completion signal (e.g. a Stripe/Stack webhook, an event, or a documented synchronous confirmation call). If confirmation genuinely takes longer than ~4.5s total, the user is shown welcome=pending even though the purchase succeeded, and there's no readiness signal driving the retry — only a fixed backoff.
Consider replacing this with a real signal: e.g., have the checkout return URL fire only after a webhook-confirmed subscription event, or check whether Stack/Stripe expose a synchronous "confirm checkout session" call that doesn't require guessing at timing.
As per coding guidelines and path instructions, "Do not implement retry, teardown, startup, keepalive, debounce, or handoff logic that depends on elapsed wall-clock time instead of a cancellation-aware scheduler, callback, notification, file descriptor or process event, async sequence, state transition, or explicit completion point."
🤖 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/confirm/route.ts` around lines 26 - 38, The polling in
confirm route uses a fixed sleep-based retry loop around
hasActiveProSubscription, which violates the no-hacky-sleeps guidance. Replace
the wall-clock wait in the confirm handler with a real completion signal or
synchronous confirmation path from Stripe/Stack (for example, a
webhook-confirmed state transition or documented confirm call), and keep the
existing hasActiveProSubscription/app.listProducts lookup as the source of truth
only after that signal is available.
Sources: Coding guidelines, Path instructions
…ates hasActiveProSubscription now treats a past currentPeriodEnd as inactive. reconcileProPlanMetadata syncs cmuxPlan both directions at VM-create time (skipping manual cmuxVmPlan overrides), so a purchase that missed /api/billing/confirm or a lapsed subscription corrects itself where paid limits are consumed. Confirm route redirects welcome=success, stops polling on client abort; checkout wraps the fallback createCheckoutUrl and fails to /pro?billing=error. Pending banner gains a one-click 'Check again' link that re-runs /api/billing/confirm. 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: 44e469a839
ℹ️ 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".
| const { locale } = await params; | ||
| setRequestLocale(locale); | ||
|
|
||
| const t = await getTranslations("pro"); |
There was a problem hiding this comment.
Localize Pro copy for every routed locale
The new pro namespace, plus nav.pro and home.pro*, was only added to web/messages/en.json and web/messages/ja.json; I checked the catalogs for the locales listed in web/i18n/routing.ts, and ar/bs/da/de/es/fr/it/km/ko/no/pl/pt-BR/ru/th/tr/uk/zh-CN/zh-TW are missing these keys. Because web/i18n/request.ts deep-merges the English fallback, those localized /pro and home pages now render English checkout copy; .github/review-bot-rules/full-internationalization.md expects web message keys to be represented across every routed locale, so please add matching entries to each web/messages/<locale>.json or intentionally change the locale registry.
Useful? React with 👍 / 👎.
| await syncProPlanMetadata(user, isPro); | ||
| return NextResponse.redirect( | ||
| new URL( | ||
| isPro ? "/pro?welcome=success" : "/pro?welcome=pending", |
There was a problem hiding this comment.
Preserve locale when returning from Pro checkout
When checkout starts from a localized page such as /ja/pro, the flow has no locale state: the CTA goes to /api/billing/checkout, checkout sends signed-out users back to /api/billing/checkout, and confirm redirects to the hard-coded default-locale /pro?... here. After payment, pending, active, or error returns, Japanese and other localized users are dropped onto the English route instead of their original locale; carry the originating locale/path through checkout and confirm before building this redirect.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/messages/en.json (1)
51-52: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winAdd the new Pro keys to every supported locale.
web/i18n/routing.tslists 20 locales, but onlyweb/messages/en.jsonandweb/messages/ja.jsonincludenav.pro,home.proSection/home.proTitle/home.proDesc/home.proPrice, and thepro.*namespace; the other locale files still need matching entries.🤖 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/messages/en.json` around lines 51 - 52, The new Pro translation keys are only present in en.json and ja.json, but every locale in web/i18n/routing.ts must include the same keys. Add matching nav.pro, home.proSection, home.proTitle, home.proDesc, home.proPrice, and the full pro.* namespace to each remaining locale file so the message schema stays consistent across all supported languages.Source: Path instructions
🤖 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 `@web/app/`[locale]/components/pro-welcome-banner.tsx:
- Around line 15-26: Replace the nested ternary used to compute the message in
pro-welcome-banner’s message selection with a simple lookup/map for the welcome
and billing states. Keep the same route contract and fallback behavior (null
when no match), but make the logic easier to scan and maintain. Use the existing
message keys referenced in the current conditional chain (welcomeSuccess,
welcomeActive, welcomePending, billingError, billingUnavailable) and preserve
the current order of precedence.
In `@web/app/api/vm/route.ts`:
- Around line 241-260: The best-effort billing reconciliation in route handling
can still block VM creation because getStackServerApp().getUser and
reconcileProPlanMetadata are awaited on the critical path inside the VM route.
Update the logic in the VM route’s reconciliation block to use a request-abort
or deadline-aware wrapper, or defer the reconciliation off the main entitlement
resolution path, so verifyRequest and VM creation can proceed without waiting on
slow Stack/billing calls. Keep the change localized around the billing_reconcile
flow and the catch/logging around the Pro plan reconcile.
---
Outside diff comments:
In `@web/messages/en.json`:
- Around line 51-52: The new Pro translation keys are only present in en.json
and ja.json, but every locale in web/i18n/routing.ts must include the same keys.
Add matching nav.pro, home.proSection, home.proTitle, home.proDesc,
home.proPrice, and the full pro.* namespace to each remaining locale file so the
message schema stays consistent across all supported languages.
🪄 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: 9b712b40-01f1-4e05-89d0-e3905e9945b9
📒 Files selected for processing (10)
web/app/[locale]/components/pro-welcome-banner.tsxweb/app/api/billing/checkout/route.tsweb/app/api/billing/confirm/route.tsweb/app/api/vm/route.tsweb/messages/en.jsonweb/messages/ja.jsonweb/services/billing/pro.tsweb/services/vms/timings.tsweb/tests/billing-pro.test.tsweb/tests/vm-route-auth.test.ts
| const message = | ||
| welcome === "success" | ||
| ? t("welcomeSuccess") | ||
| : welcome === "active" | ||
| ? t("welcomeActive") | ||
| : welcome === "pending" | ||
| ? t("welcomePending") | ||
| : billing === "error" | ||
| ? t("billingError") | ||
| : billing === "unavailable" | ||
| ? t("billingUnavailable") | ||
| : null; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Consider a lookup table instead of nested ternaries.
The welcome/billing → message chain works correctly and matches the checkout/confirm route contract, but the 6-way nested ternary is harder to scan than a simple map/lookup.
♻️ Optional refactor
- const message =
- welcome === "success"
- ? t("welcomeSuccess")
- : welcome === "active"
- ? t("welcomeActive")
- : welcome === "pending"
- ? t("welcomePending")
- : billing === "error"
- ? t("billingError")
- : billing === "unavailable"
- ? t("billingUnavailable")
- : null;
+ const welcomeKey =
+ welcome === "success" || welcome === "active" || welcome === "pending"
+ ? (`welcome${welcome[0].toUpperCase()}${welcome.slice(1)}` as const)
+ : billing === "error"
+ ? "billingError"
+ : billing === "unavailable"
+ ? "billingUnavailable"
+ : null;
+ const message = welcomeKey ? t(welcomeKey) : null;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const message = | |
| welcome === "success" | |
| ? t("welcomeSuccess") | |
| : welcome === "active" | |
| ? t("welcomeActive") | |
| : welcome === "pending" | |
| ? t("welcomePending") | |
| : billing === "error" | |
| ? t("billingError") | |
| : billing === "unavailable" | |
| ? t("billingUnavailable") | |
| : null; | |
| const welcomeKey = | |
| welcome === "success" || welcome === "active" || welcome === "pending" | |
| ? (`welcome${welcome[0].toUpperCase()}${welcome.slice(1)}` as const) | |
| : billing === "error" | |
| ? "billingError" | |
| : billing === "unavailable" | |
| ? "billingUnavailable" | |
| : null; | |
| const message = welcomeKey ? t(welcomeKey) : null; |
🤖 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]/components/pro-welcome-banner.tsx around lines 15 - 26,
Replace the nested ternary used to compute the message in pro-welcome-banner’s
message selection with a simple lookup/map for the welcome and billing states.
Keep the same route contract and fallback behavior (null when no match), but
make the logic easier to scan and maintain. Use the existing message keys
referenced in the current conditional chain (welcomeSuccess, welcomeActive,
welcomePending, billingError, billingUnavailable) and preserve the current order
of precedence.
| // Read-time reconcile: a Pro purchase that never hit | ||
| // /api/billing/confirm, or a lapsed subscription, is corrected here | ||
| // right before paid limits apply. Best-effort — billing reads must | ||
| // not block VM creation. | ||
| try { | ||
| if (isStackConfigured()) { | ||
| const changed = await measureVmAsync(timing, "billing_reconcile", async () => { | ||
| const serverUser = await getStackServerApp().getUser(user.id); | ||
| return serverUser ? reconcileProPlanMetadata(serverUser) : false; | ||
| }); | ||
| if (changed) { | ||
| const reconciledUser = await measureVmAsync(timing, "auth", () => | ||
| verifyRequest(request, { requestedTeamId: requestedBillingTeamId }) | ||
| ); | ||
| if (reconciledUser) user = reconciledUser; | ||
| } | ||
| } | ||
| } catch (err) { | ||
| console.error("[VM] Pro plan reconcile failed", err); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Bound best-effort billing reconciliation so VM creation cannot hang.
Line 247 awaits Stack/billing reconciliation before entitlements; rejected calls are caught, but a slow or hung getUser/listProducts/update still stalls the VM create route despite the “must not block VM creation” contract. Add a request-abort/deadline-aware wrapper or move this best-effort work off the critical path before resolving entitlements. The helper’s product pagination/update behavior is confirmed by web/services/billing/pro.ts.
🤖 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/vm/route.ts` around lines 241 - 260, The best-effort billing
reconciliation in route handling can still block VM creation because
getStackServerApp().getUser and reconcileProPlanMetadata are awaited on the
critical path inside the VM route. Update the logic in the VM route’s
reconciliation block to use a request-abort or deadline-aware wrapper, or defer
the reconciliation off the main entitlement resolution path, so verifyRequest
and VM creation can proceed without waiting on slow Stack/billing calls. Keep
the change localized around the billing_reconcile flow and the catch/logging
around the Pro plan reconcile.
There was a problem hiding this comment.
1 issue found across 10 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="web/app/api/vm/route.ts">
<violation number="1" location="web/app/api/vm/route.ts:247">
P1: VM creation currently awaits best-effort billing reconciliation on the request path, so a slow or hung Stack call can still delay or hang VM provisioning. Consider adding a deadline/abort wrapper (or moving reconciliation off-path) so this step cannot block VM creation.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Free and Pro tiers side by side, Pricing in the nav and mobile drawer, homepage Pro section and corner badge removed (the upgrade entry moves into the macOS app). Billing routes now land on /pricing; page is registered in the sitemap and agent-readable variants. 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: 9ab831003b
ℹ️ 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".
| const changed = await measureVmAsync(timing, "billing_reconcile", async () => { | ||
| const serverUser = await getStackServerApp().getUser(user.id); | ||
| return serverUser ? reconcileProPlanMetadata(serverUser) : false; |
There was a problem hiding this comment.
Put a deadline on VM billing reconciliation
On the POST /api/vm create path, this best-effort reconcile is still awaited before entitlements and provider provisioning. When Stack's admin getUser/listProducts call is slow or hangs during a billing outage or network stall, every VM create now waits here even though failures are only ignored after the promise rejects; the previous create path could otherwise proceed with the cached entitlement. Please bound this call with a short timeout or move it off the critical path so billing outages do not stall VM provisioning.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 `@web/app/`[locale]/components/site-header.tsx:
- Around line 112-118: The mobile drawer pricing link currently only calls
close, so it bypasses the nav-click analytics event that desktop NavLinks
already sends. Update the site-header.tsx drawer Link for pricing to use the
same shared tracking action/model as the desktop pricing entry, ensuring
cmuxterm_pricing_nav_clicked is emitted with location: "nav" before/while
closing the drawer. Keep the fix centralized by reusing the existing NavLinks
analytics path rather than adding one-off logic only in this Link.
In `@web/app/api/billing/checkout/route.ts`:
- Around line 41-43: The checkout route is granting Pro based on
`isAlreadyGrantedError(error)`, which relies on provider error text and can
misclassify users. In the `route.ts` handler, replace that fallback with a fresh
read of the authoritative product state before calling `syncProPlanMetadata`,
and only pass `true` when the real billing state confirms Pro. Keep
`isAlreadyGrantedError` from being the source of truth for `cmuxPlan` so wording
changes or false matches cannot grant access incorrectly.
In `@web/app/api/billing/confirm/route.ts`:
- Around line 43-46: The `confirm` route in `route.ts` is clearing `cmuxPlan` by
calling `syncProPlanMetadata(user, isPro)` even when `isPro` is false and the
confirmation is still pending. Update the `confirm` flow so
`syncProPlanMetadata` is only called on confirmed Pro success, and do not write
any negative/clearing metadata from this handler. Keep pending behavior limited
to the redirect outcome, and move any lapsed-state cleanup to the separate
authoritative reconcile path.
🪄 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: 12121a32-f5d3-4bc2-a358-79349101222c
📒 Files selected for processing (11)
web/app/[locale]/components/nav-links.tsxweb/app/[locale]/components/pro-checkout-button.tsxweb/app/[locale]/components/pro-welcome-banner.tsxweb/app/[locale]/components/site-header.tsxweb/app/[locale]/pricing/page.tsxweb/app/api/billing/checkout/route.tsweb/app/api/billing/confirm/route.tsweb/app/lib/agent-page-paths.tsweb/app/sitemap.tsweb/messages/en.jsonweb/messages/ja.json
| <Link | ||
| href="/pricing" | ||
| onClick={close} | ||
| className="hover:text-foreground transition-colors py-1" | ||
| > | ||
| {t("pricing")} | ||
| </Link> |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Mobile drawer's Pricing link doesn't fire the nav-click analytics event.
The desktop NavLinks pricing entry captures cmuxterm_pricing_nav_clicked with location: "nav", but this mobile drawer entry only calls close, dropping analytics for this entry point — same gap previously flagged for the /pro link before it was renamed to /pricing.
📊 Add tracking to the drawer entry point
<Link
href="/pricing"
- onClick={close}
+ onClick={() => {
+ posthog.capture("cmuxterm_pricing_nav_clicked", {
+ location: "mobile_drawer",
+ });
+ close();
+ }}
className="hover:text-foreground transition-colors py-1"
>
{t("pricing")}Based on coding guidelines: "When a behavior is exposed through multiple entrypoints... implement one shared action/model path and verify every entrypoint that should invoke it. Do not patch one surface while leaving the others with duplicated logic."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Link | |
| href="/pricing" | |
| onClick={close} | |
| className="hover:text-foreground transition-colors py-1" | |
| > | |
| {t("pricing")} | |
| </Link> | |
| <Link | |
| href="/pricing" | |
| onClick={() => { | |
| posthog.capture("cmuxterm_pricing_nav_clicked", { | |
| location: "mobile_drawer", | |
| }); | |
| close(); | |
| }} | |
| className="hover:text-foreground transition-colors py-1" | |
| > | |
| {t("pricing")} | |
| </Link> |
🤖 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]/components/site-header.tsx around lines 112 - 118, The
mobile drawer pricing link currently only calls close, so it bypasses the
nav-click analytics event that desktop NavLinks already sends. Update the
site-header.tsx drawer Link for pricing to use the same shared tracking
action/model as the desktop pricing entry, ensuring cmuxterm_pricing_nav_clicked
is emitted with location: "nav" before/while closing the drawer. Keep the fix
centralized by reusing the existing NavLinks analytics path rather than adding
one-off logic only in this Link.
Source: Coding guidelines
| await syncProPlanMetadata(user, isPro); | ||
| return NextResponse.redirect( | ||
| new URL( | ||
| isPro ? "/pricing?welcome=success" : "/pricing?welcome=pending", |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Do not clear Pro metadata while confirmation is pending.
This route treats isPro === false as welcome=pending, but Line 43 passes that same false value to syncProPlanMetadata, which deletes cmuxPlan. A delayed confirmation can temporarily revoke Pro entitlements; only write metadata on confirmed success, and clear lapsed state from a separate authoritative reconcile path.
Suggested fix
- await syncProPlanMetadata(user, isPro);
+ if (isPro) {
+ await syncProPlanMetadata(user, true);
+ }
return NextResponse.redirect(As per path instructions, correctness-critical state must not come from an unreliable fallback where a wrong value is a correctness bug.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await syncProPlanMetadata(user, isPro); | |
| return NextResponse.redirect( | |
| new URL( | |
| isPro ? "/pricing?welcome=success" : "/pricing?welcome=pending", | |
| if (isPro) { | |
| await syncProPlanMetadata(user, true); | |
| } | |
| return NextResponse.redirect( | |
| new URL( | |
| isPro ? "/pricing?welcome=success" : "/pricing?welcome=pending", |
🤖 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/confirm/route.ts` around lines 43 - 46, The `confirm`
route in `route.ts` is clearing `cmuxPlan` by calling `syncProPlanMetadata(user,
isPro)` even when `isPro` is false and the confirmation is still pending. Update
the `confirm` flow so `syncProPlanMetadata` is only called on confirmed Pro
success, and do not write any negative/clearing metadata from this handler. Keep
pending behavior limited to the redirect outcome, and move any lapsed-state
cleanup to the separate authoritative reconcile path.
Source: Path instructions
| if (isAlreadyGrantedError(error)) { | ||
| await syncProPlanMetadata(user, true); | ||
| return NextResponse.redirect(new URL("/pricing?welcome=active", request.url)); |
There was a problem hiding this comment.
Billing entitlement granted from an untyped error-message heuristic
isAlreadyGrantedError matches the regex /already.{0,20}granted/i against the raw error string and, when it hits, immediately calls syncProPlanMetadata(user, true) — writing cmuxPlan: "pro" to the user's metadata and redirecting them to the active-plan page. This is reached only after hasActiveProSubscription returned false, so the only authority for the grant is the regex, not a verified subscription read. Any Stack error whose text happens to contain "already" followed by "granted" (e.g. a database-level grant conflict, a permission grant, a quota grant) would silently elevate the user to Pro. The safer path when createCheckoutUrl throws an unrecognised error is to redirect to /pricing?welcome=pending and let the user trigger the confirm route's bounded poll — the same mechanism that already handles Stripe async lag — rather than trust a string match to write billing state.
Rule Used: Flag correctness-critical detection/identity deriv... (source)
There was a problem hiding this comment.
2 issues found across 13 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="web/app/[locale]/pricing/page.tsx">
<violation number="1" location="web/app/[locale]/pricing/page.tsx:103">
P2: Non-English pricing visitors lose their locale during checkout status redirects. This CTA enters `/api/billing/checkout`, whose success/error/active redirects are hardcoded to `/pricing`; consider passing the current locale/return path through checkout or making the billing routes redirect back to the localized pricing URL.</violation>
</file>
<file name="web/app/[locale]/components/site-header.tsx">
<violation number="1" location="web/app/[locale]/components/site-header.tsx:113">
P3: Pricing clicks from the mobile drawer are not tracked, so nav analytics undercount this entrypoint compared with the desktop pricing link. This link now routes to `/pricing` but its handler only calls `close`; consider emitting `cmuxterm_pricing_nav_clicked` here as well (for example with `location: "mobile_drawer"`) before closing.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| ))} | ||
| </ul> | ||
| <div> | ||
| <ProCheckoutButton size="sm" location="pricing_pro" /> |
There was a problem hiding this comment.
P2: Non-English pricing visitors lose their locale during checkout status redirects. This CTA enters /api/billing/checkout, whose success/error/active redirects are hardcoded to /pricing; consider passing the current locale/return path through checkout or making the billing routes redirect back to the localized pricing URL.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/app/[locale]/pricing/page.tsx, line 103:
<comment>Non-English pricing visitors lose their locale during checkout status redirects. This CTA enters `/api/billing/checkout`, whose success/error/active redirects are hardcoded to `/pricing`; consider passing the current locale/return path through checkout or making the billing routes redirect back to the localized pricing URL.</comment>
<file context>
@@ -0,0 +1,112 @@
+ ))}
+ </ul>
+ <div>
+ <ProCheckoutButton size="sm" location="pricing_pro" />
+ </div>
+ </section>
</file context>
|
|
||
| <div className="flex flex-col gap-3 text-sm text-muted px-4 pb-4"> | ||
| <Link | ||
| href="/pricing" |
There was a problem hiding this comment.
P3: Pricing clicks from the mobile drawer are not tracked, so nav analytics undercount this entrypoint compared with the desktop pricing link. This link now routes to /pricing but its handler only calls close; consider emitting cmuxterm_pricing_nav_clicked here as well (for example with location: "mobile_drawer") before closing.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/app/[locale]/components/site-header.tsx, line 113:
<comment>Pricing clicks from the mobile drawer are not tracked, so nav analytics undercount this entrypoint compared with the desktop pricing link. This link now routes to `/pricing` but its handler only calls `close`; consider emitting `cmuxterm_pricing_nav_clicked` here as well (for example with `location: "mobile_drawer"`) before closing.</comment>
<file context>
@@ -122,11 +110,11 @@ export function SiteHeader({
<div className="flex flex-col gap-3 text-sm text-muted px-4 pb-4">
<Link
- href="/pro"
+ href="/pricing"
onClick={close}
className="hover:text-foreground transition-colors py-1"
</file context>
One shared destination (AuthEnvironment.pricingURL: CMUX_WWW_ORIGIN env, then DEBUG-only ~/.cmux-dev.env override, then cmux.com/pricing) opened from three surfaces: a cmux Pro card in Settings > Account, an Upgrade to cmux Pro command palette entry, and a Help menu item. Localized en+ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 12 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AccountSection.swift">
<violation number="1" location="Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AccountSection.swift:38">
P3: The Pro upgrade card has a `settingsSearchAnchors(["setting:account:pro"])` modifier, but there's no corresponding curated search entry in the index — unlike the identity card above which has a matching `setting:account:account` entry. Result: searching for "pro" in settings won't find this card. Either add a curated entry in `CuratedSettingEntry+Default.swift` for it, or drop the anchor if search visibility isn't needed.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| SettingsCard { | ||
| ProUpgradeCard(flow: accountFlow) | ||
| } | ||
| .settingsSearchAnchors(["setting:account:pro"]) |
There was a problem hiding this comment.
P3: The Pro upgrade card has a settingsSearchAnchors(["setting:account:pro"]) modifier, but there's no corresponding curated search entry in the index — unlike the identity card above which has a matching setting:account:account entry. Result: searching for "pro" in settings won't find this card. Either add a curated entry in CuratedSettingEntry+Default.swift for it, or drop the anchor if search visibility isn't needed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AccountSection.swift, line 38:
<comment>The Pro upgrade card has a `settingsSearchAnchors(["setting:account:pro"])` modifier, but there's no corresponding curated search entry in the index — unlike the identity card above which has a matching `setting:account:account` entry. Result: searching for "pro" in settings won't find this card. Either add a curated entry in `CuratedSettingEntry+Default.swift` for it, or drop the anchor if search visibility isn't needed.</comment>
<file context>
@@ -31,6 +32,10 @@ public struct AccountSection: View {
+ SettingsCard {
+ ProUpgradeCard(flow: accountFlow)
+ }
+ .settingsSearchAnchors(["setting:account:pro"])
}
}
</file context>
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
bun's mock.module is process-global, so the billing confirm test's db/client stub must preserve the teardown export vm tests import; CI's test order surfaced the missing export as an unhandled error between tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unions the Stripe billing tables with main's subrouter tenants table in schema.ts and both sides' env additions in env.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stack.ts mock was missing stackServerApp, which CI's test order surfaced as an unhandled error in unrelated tests importing the real export after the process-wide mock installed. Also fills out the next/navigation and next/headers mocks so later suite imports can't break the same way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| isPro ? "/pricing?welcome=success" : "/pricing?welcome=pending", | ||
| request.url, | ||
| ), | ||
| ); |
There was a problem hiding this comment.
Confirm redirect drops app pricing context
Medium Severity
After Stack hosted checkout, /api/billing/confirm always redirects to /pricing?welcome=…, not /app-pricing with cmux_app=1. If checkout completes in the embedded browser (or the user returns to the in-app panel), the split pane loads the public pricing page instead of the app pricing UI and its welcome banners.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 136f95d. Configure here.
billingCheckoutURL previously hard-pinned https://cmux.com so dev-build checkout landed on production (which still serves the legacy Stack purchase page until live Stripe env exists). Checkout now resolves CMUX_BILLING_WWW_ORIGIN first, then the same appWebOrigin resolution the app-pricing page uses, so every entrypoint targets the origin that rendered pricing. Stripe Checkout binds the purchaser to the server-created session, so same-origin is required for the dev flow. Release with no env still resolves to cmux.com. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Checkout now defaults to the monthly $30 price (interval=year still selects $240/year), with pricing copy monthly-first across web en/ja and the Settings subtitle. Adds web/scripts/stripe/dev-stack.sh (one command brings up the tagged dev server plus stripe webhook forwarding and prints the verification commands), an idempotent web/scripts/stripe/provision-live.sh for live-mode go-live, and a skills/cmux-billing runbook wired into the skill map covering the billing architecture, dev workflow, test resources, flags, prod runbook, and the CI-order test gotchas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
web/scripts/stripe/dev-reset.sh <email> un-Pros a dev-project account: cancels test-mode Stripe subscriptions by stackUserId, cancels the Stack Pro product subscription, clears cmuxPlan metadata, optionally deletes the local DB billing rows, and warns explicitly when a Stack-era paid period or comped grant remains (no API early-revoke) so the operator knows the account stays Pro until it lapses. Refuses the production Stack project and live Stripe keys. The billing skill documents the repeat-dogfood paths: private window for a fresh anonymous buyer, dev-reset for signed-in accounts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The billing suites' process-global cloudDb stubs fed fixture data to vm-workflows in CI's DB-enabled run. The db/client mocks now spread the real module and delegate cloudDb back to it outside their own suite. That delegation exposed vm-route-auth reaching the real pool through the VM route's Pro reconcile (connection retry hang), so it now self-shields with a stub throwing the missing-DATABASE_URL error the reconcile is designed to catch. dev-reset's residual recheck parses with node instead of jq to honor the script's dependency contract. Verified: full suite order-independent locally, vm-workflows green against a real isolated DB via bun run db:test, typecheck clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
||
| var body: some View { | ||
| if CmuxFeatureFlags.shared.isProUpgradeUIEnabled, | ||
| !ProBadgeStyleStore.shared.isDismissed { |
There was a problem hiding this comment.
Pro UI ignores flag updates
Medium Severity
Release builds gate Pro surfaces on PostHog’s pro-upgrade-ui-enabled-release, but the sidebar/titlebar badges and Help menu read CmuxFeatureFlags.shared once and never subscribe to cmuxFeatureFlagsDidChange, so enabled flags may not appear until restart.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit f1f594e. Configure here.
bun test discovers files in filesystem readdir order, so CI runs a different file order than local and our added test files reshuffled it, arming pre-existing process-global mock landmines: vm-route-auth's workflows mock fed plain async stubs to vm-workflows' Effect calls (.pipe TypeError). All mocks of modules that other suites consume for real (services/vms/workflows, db/client in vm-route-auth, subrouter-accounts, notifications-push) now capture the real module first and delegate outside their own suite via a beforeAll/afterAll flag, the same pattern as the billing suites. Verified in CI's poisoning order, reverse alphabetical, and the natural order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 4 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 22fbba1. Configure here.
| queryItems.append(URLQueryItem(name: "cmux_external_browser", value: "1")) | ||
| queryItems.append(URLQueryItem(name: "cmux_scheme", value: callbackScheme)) | ||
| components.queryItems = queryItems | ||
| return components.url! |
There was a problem hiding this comment.
Mac checkout defaults monthly Stripe
Medium Severity
The macOS billingCheckoutURL built for ProUpgradePresenter.presentCheckout() and in-app upgrade flows omits an interval=year query parameter. When Stripe billing is enabled, /api/billing/checkout treats a missing interval as monthly, so users can be sent to monthly pricing despite copy that advertises yearly-first ($240/year) checkout.
Reviewed by Cursor Bugbot for commit 22fbba1. Configure here.
| ProUpgradeCard(flow: accountFlow) | ||
| } | ||
| .settingsSearchAnchors(["setting:account:pro"]) | ||
| } |
There was a problem hiding this comment.
Settings Pro card ignores flag updates
Medium Severity
The Account Pro upgrade card is gated on accountFlow?.isProUpgradeAvailable, but SettingsRuntime holds accountFlow as an AccountFlow? existential. When PostHog loads and HostAccountFlow updates isProUpgradeAvailable, SwiftUI views reading through the protocol often do not re-render, so the card can stay hidden or visible until Settings is reopened.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 22fbba1. Configure here.
Delegating test mocks previously called through captured module namespace objects; bun's mock.module can mutate an already-loaded namespace in place, so on the CI runner the "real" call resolved back into the wrapper and recursed (vm-workflows exec failures reproduced only there). Every delegating mock now copies the original function references by value before mock.module, which is correct under either registry semantics. The web-typecheck job also runs bun test with an explicit sorted file list so CI's execution order is reproducible locally instead of readdir roulette. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>


Free→paid conversion, per the Garry Tan office-hours notes, revised after review: the upgrade entry lives in the macOS app, and the website gets a pricing page instead of a homepage upsell.
Website
/pricingpage: Free and Pro side by side ($0 vs $20/month billed yearly, monthly $30 fallback),Pricingin the nav and mobile drawer, registered in sitemap and agent-readable (.md/.txt) variants. The earlier homepage Pro section and corner badge are removed.GET /api/billing/checkout: one click to the Stack hosted purchase page (signed-out visitors round-trip through sign-in viaafter_auth_return_to). Yearly is pre-selected because it is listed first in the product's prices map (the hosted page picks the first price key; there is no URL param for it).GET /api/billing/confirm(checkout return URL): verifies the subscription with a short bounded poll, syncsclientReadOnlyMetadata.cmuxPlan = "pro"(whichservices/vmsentitlements already read), clears it on lapse, redirects to/pricing?welcome=.... A read-time reconcile at VM-create time syncs the metadata in both directions, so a missed confirmation or a lapsed subscription self-corrects where paid limits are consumed. Per the hexclave source there are no payment webhooks, so read-time reconciliation is the correct mechanism, and there is no prebuilt pricing component, so a hand-built page is the documented pattern.macOS app
AuthEnvironment.pricingURL(CMUX_WWW_ORIGINenv → DEBUG-only~/.cmux-dev.envoverride →https://cmux.com/pricing) opened from three entrypoints: acmux Procard with anUpgrade…button in Settings > Account (searchable), anUpgrade to cmux Procommand palette entry, and a Help menu item. Strings localized en+ja inResources/Localizable.xcstrings.Pricing config (Stack project config, not in repo): product
prounder user-scoped linecmux-pro, yearly $240 listed first, monthly $30, includedItemcmux-pro-access. Live on the dev project; prod needs the same write plus Stripe Connect onboarding before launch.Verified
bun test/typecheck green; live dev-server E2E of sign-in redirect, checkout URL creation (yearly pre-selected per validate-code), and confirm-route metadata sync/clear with throwaway dev users.Known launch gaps (Stack-side)
grantProductnever surfacing inlistProducts). Before launch: prod config write, Stripe Connect onboarding, one real purchase+refund smoke test.🤖 Generated with Claude Code
Note
Medium Risk
Touches billing/checkout URL construction and in-app browser navigation for paid conversion; rollout is flag-gated but incorrect origin or checkout handling could send users to the wrong environment.
Overview
Adds cmux Pro upgrade surfaces across the macOS app, wired to environment-aware pricing and checkout URLs and rollout via PostHog feature flags.
Upgrade flow:
ProUpgradePresenteropens/app-pricingin a transparent browser split (appearance/background query params) with fallbacks to a browser tab or the system browser. Checkout usesAuthEnvironment.billingCheckoutURL(optionalCMUX_BILLING_WWW_ORIGIN,cmux_external_browser+ app callback scheme). In-panel checkout links are forced to the system browser; restored sessions remap/app-pricingwithcmux_app/cmux_scheme. A DEBUG-only native pricing window previews plan cards and callsGET api/billing/plan.Entrypoints (gated by
pro-upgrade-ui-enabled-release): Settings AccountProUpgradeCard, command palette command, Help menu (plus DEBUG native preview), sidebar footer badge, and titlebar Pro badge—all behindCmuxFeatureFlags, started at launch and observed inHostAccountFlowso Settings updates live.Mobile Connect: Right titlebar accessory with iPhone button (flag
mobile-connect-button-enabled-release), built-incmux.mobileconnectaction, and palette/AppDelegate routing toMobilePairingWindowController.CI/docs:
scripts/lint-feature-flags.pyin CI, deterministic sortedbun testfile order,cmux-billingskill inCLAUDE.md, and Swift file-length budget bumps for new/changed files. Large en/ja localization additions for pricing and upgrade copy.Reviewed by Cursor Bugbot for commit f5f7009. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
/pricingpage with plan tiers, comparisons, FAQs, and conditional Vault/checkout content./pricingand related help text.Update: the pricing page is now adopted from #6791 (branch merged in; this PR supersedes it or lands after it, either order works). Its
PRO_CTA_URLswap point now targets/api/billing/checkoutbehind aSHOW_CHECKOUTflag mirroring the page'sSHOW_VAULTpattern: on in local dev, off in production,NEXT_PUBLIC_CMUX_CHECKOUT_ENABLED=1|0forces either way. The Pro card shows $20/month with a "billed yearly ($240/year), or $30 month-to-month" note so the page matches the hosted checkout's yearly-first default (the FAQ billing answer is updated to match). The post-checkout banner mounts above the tier cards. The macOS app additionally gets a cornerProbadge in the sidebar footer, opening the same shared pricing URL as Settings, palette, and Help menu.