Skip to content

Add cmux Pro pricing and one-click Stack checkout - #7143

Merged
lawrencecchen merged 56 commits into
mainfrom
feat-pro-plan
Jul 6, 2026
Merged

lawrencecchen merged 56 commits into
mainfrom
feat-pro-plan

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

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

  • New /pricing page: Free and Pro side by side ($0 vs $20/month billed yearly, monthly $30 fallback), Pricing in 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 via after_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, syncs clientReadOnlyMetadata.cmuxPlan = "pro" (which services/vms entitlements 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

  • One shared destination AuthEnvironment.pricingURL (CMUX_WWW_ORIGIN env → DEBUG-only ~/.cmux-dev.env override → https://cmux.com/pricing) opened from three entrypoints: a cmux Pro card with an Upgrade… button in Settings > Account (searchable), an Upgrade to cmux Pro command palette entry, and a Help menu item. Strings localized en+ja in Resources/Localizable.xcstrings.

Pricing config (Stack project config, not in repo): product pro under user-scoped line cmux-pro, yearly $240 listed first, monthly $30, includedItem cmux-pro-access. Live on the dev project; prod needs the same write plus Stripe Connect onboarding before launch.

Verified

  • Web: 17 unit tests on the billing helpers plus full 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.
  • macOS: cloud-built tagged app; drove Settings→Account via the debug socket and screenshot-verified the Pro card and Upgrade button render with the identity card.

Known launch gaps (Stack-side)

  • The dev Stack project cannot complete payments (no Stripe Connect account; its test-mode purchase endpoint 500s server-side — consistent with their bulldozer timefold queue not draining, which also explains API grantProduct never surfacing in listProducts). Before launch: prod config write, Stripe Connect onboarding, one real purchase+refund smoke test.
  • Hexclave cancel gotcha for later: their subscription cancel API cancels live Stripe subs immediately, not at period end.

🤖 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: ProUpgradePresenter opens /app-pricing in a transparent browser split (appearance/background query params) with fallbacks to a browser tab or the system browser. Checkout uses AuthEnvironment.billingCheckoutURL (optional CMUX_BILLING_WWW_ORIGIN, cmux_external_browser + app callback scheme). In-panel checkout links are forced to the system browser; restored sessions remap /app-pricing with cmux_app / cmux_scheme. A DEBUG-only native pricing window previews plan cards and calls GET api/billing/plan.

Entrypoints (gated by pro-upgrade-ui-enabled-release): Settings Account ProUpgradeCard, command palette command, Help menu (plus DEBUG native preview), sidebar footer badge, and titlebar Pro badge—all behind CmuxFeatureFlags, started at launch and observed in HostAccountFlow so Settings updates live.

Mobile Connect: Right titlebar accessory with iPhone button (flag mobile-connect-button-enabled-release), built-in cmux.mobileconnect action, and palette/AppDelegate routing to MobilePairingWindowController.

CI/docs: scripts/lint-feature-flags.py in CI, deterministic sorted bun test file order, cmux-billing skill in CLAUDE.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

  • New Features
    • Added a localized /pricing page with plan tiers, comparisons, FAQs, and conditional Vault/checkout content.
    • Added “cmux Pro” upgrade entry points across web navigation and macOS (sidebar badge, account card, command palette, and help).
    • Introduced billing checkout/confirmation API flows and a Pro welcome banner reflecting success/active/pending/error states.
  • Bug Fixes
    • Improved Pro subscription status synchronization and reconciliation during VM creation.
  • Documentation
    • Updated English/Japanese copy and localization strings, plus sitemap entry for /pricing and related help text.
  • Tests
    • Added unit coverage for Pro billing helper logic and pagination behavior.

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_URL swap point now targets /api/billing/checkout behind a SHOW_CHECKOUT flag mirroring the page's SHOW_VAULT pattern: on in local dev, off in production, NEXT_PUBLIC_CMUX_CHECKOUT_ENABLED=1|0 forces 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 corner Pro badge in the sidebar footer, opening the same shared pricing URL as Settings, palette, and Help menu.

lawrencecchen and others added 7 commits June 25, 2026 19:50
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>
@vercel

vercel Bot commented Jul 1, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 6, 2026 5:04am
cmux-staging Building Building Preview, Comment Jul 6, 2026 5:04am

@coderabbitai

coderabbitai Bot commented Jul 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds 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.

Changes

Pro subscription feature

Layer / File(s) Summary
Pro billing helpers and tests
web/services/billing/pro.ts, web/tests/billing-pro.test.ts
Adds Pro product and plan constants, subscription detection, metadata synchronization, reconciliation helpers, and tests covering pagination, metadata updates, and reconciliation outcomes.
Checkout API route
web/app/api/billing/checkout/route.ts
Adds the /api/billing/checkout handler that checks Stack availability, resolves the user, handles active Pro status, creates hosted checkout URLs, and redirects on sign-in or billing errors.
Confirm API route
web/app/api/billing/confirm/route.ts
Adds the /api/billing/confirm handler that polls for Pro subscription activation, stops on request abort, syncs metadata, and redirects to /pricing with a welcome status.
VM reconciliation wiring
web/app/api/vm/route.ts, web/services/vms/timings.ts, web/tests/vm-route-auth.test.ts
Adds best-effort Pro metadata reconciliation to VM creation, extends VM timing stages, and updates the VM auth test to reflect the extra user lookup and pagination mock.
Pricing page and navigation
web/app/[locale]/pricing/page.tsx, web/app/[locale]/components/{nav-links,site-header,site-footer,pro-welcome-banner}.tsx, web/app/sitemap.ts, web/app/lib/agent-page-paths.ts, web/messages/{en,ja}.json
Adds the pricing page, welcome banner, pricing links in navigation, sitemap and agent-readable page entries, and pricing translations.

Estimated code review effort: 4 (Complex) | ~60 minutes

macOS Pro upgrade UI

Layer / File(s) Summary
Account settings Pro upgrade card
Packages/macOS/CmuxSettingsUI/.../AccountFlow.swift, .../ProUpgradeCard.swift, .../AccountSection.swift, Sources/Auth/AuthEnvironment.swift, Sources/Auth/HostAccountFlow.swift
Adds openProUpgrade() protocol method, a ProUpgradeCard view, and pricingURL resolution logic wired into the Account section.
Help menu, command palette, and sidebar badge
Sources/App/CmuxHelpCommands.swift, Sources/App/CmuxHelpResource.swift, Sources/ContentView+ProCommandPalette.swift, Sources/ContentView.swift, Sources/SettingsNavigation.swift, Sources/SidebarProBadge.swift, Resources/Localizable.xcstrings, cmux.xcodeproj/project.pbxproj
Adds a Help menu item, command palette command, sidebar Pro badge, settings search entry, localization strings, and Xcode project registration for new files.

Unrelated documentation and marketing copy edits

Layer / File(s) Summary
Localization copy revisions
web/messages/en.json, web/messages/ja.json
Revises iOS FAQ, blog, remote tmux, workspace groups, config schema, CLI, testimonial, and landing page link/FAQ copy.

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
Loading

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (3 errors)

Check name Status Explanation Resolution
Cmux No Hacky Sleeps ❌ Error web/app/api/billing/confirm/route.ts adds a fixed 1500ms setTimeout polling loop to wait for Stripe/Stack subscription propagation after redirect. Replace the time-based poll with a real completion signal or a dedicated cancellation-aware timeout abstraction; if confirmation is async, use callback/webhook or re-check on user action.
Cmux Full Internationalization ❌ Error New /pricing web copy is only in en/ja, but routing.ts supports 20 locales; the new Swift Pro strings in Localizable.xcstrings are also only en/ja. Add pricing to every web/messages/{locale}.json and translate the new Pro/help/sidebar keys in Resources/Localizable.xcstrings for every supported locale.
Cmux Architecture Rethink ❌ Error Four new Pro entrypoints each open pricing directly, so the upgrade flow has no single action owner; it’s duplicate wiring that can drift across surfaces. Move pricing navigation into one injected openProUpgrade()/router owned by the host, then have sidebar, palette, help, and settings call that shared entrypoint.
✅ Passed checks (22 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed Only UI-facing SwiftUI additions changed; no new value-model, service-protocol, Sendable, or background-store isolation regressions were introduced.
Cmux Swift Blocking Runtime ✅ Passed New Swift code only adds Pro UI entrypoints and URL opens; no new waits, sleeps, asyncAfter, sync calls, or locks were introduced.
Cmux Browser Automation Off-Main ✅ Passed Browser waits/callbacks route via socketWorkerMethods and the worker router; handlers use explicit v2MainSync/main hops, and policy tests cover the worker-lane methods.
Cmux Expensive Synchronous Load ✅ Passed No new main-actor agent-history load was added; the new Swift code only opens AuthEnvironment.pricingURL or renders simple UI, while existing RestorableAgentSessionIndex.load sites are unchanged.
Cmux Cache Substitution Correctness ✅ Passed PASS: Billing reconcile uses live listProducts, re-fetches user after metadata changes, and skips only for the documented cmuxVmPlan override precedence.
Cmux Algorithmic Complexity ✅ Passed No new unbounded scans or nested rescans; the added billing/product walks are linear and bounded, and the pricing UI filters small fixed arrays.
Cmux Swift Concurrency ✅ Passed The added Swift code is synchronous UI wiring and URL-opening; no new DispatchQueue, Combine state, completion-handler APIs, or fire-and-forget Tasks were introduced.
Cmux Swift @Concurrent ✅ Passed No changed Swift code adds invalid @concurrent or off-actor async work; the new Pro entrypoints are synchronous UI actions, and existing async methods remain @MainActor-bound.
Cmux Swift File And Package Boundaries ✅ Passed Touched Swift files are small UI/bridge/protocol code; the largest, AuthEnvironment.swift, is 373 lines and the new logic stays cohesive.
Cmux Swiftpm Lockfiles ✅ Passed No cmux-owned .gitignore ignores Package.resolved; cmux package deps have sibling Package.resolved files, and cmux.xcodeproj includes the root Package.resolved with its package refs.
Cmux Swift Logging ✅ Passed The only added log is cmuxDebugLog under #if DEBUG; no new print/NSLog/Logger changes in production Swift code.
Cmux User-Facing Error Privacy ✅ Passed User-facing billing/status copy is generic; no end-user text exposes Stack/Stripe, internal ids, env vars, or raw upstream errors.
Cmux Swiftui State Layout ✅ Passed New SwiftUI code is stateless except a local hover @State in SidebarProBadge; no new ObservableObject/@Published/GeometryReader, lazy-row store refs, or render-time state writes.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds settings/pricing views and browser links, but no new NSWindow/NSPanel/WindowGroup or close-shortcut routing; cmuxAuxiliaryWindowIdentifiers is untouched.
Cmux Source Artifacts ✅ Passed Changed paths are source, tests, config, docs, or localization catalogs; none are scratch/build artifacts or forbidden artifact dirs.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Touched Sources code adds real Pro entrypoints/calls; the only new #if DEBUG is command-palette logging, not a test-observability accessor.
Cmux No Ambient Global State ✅ Passed PASS: the PR adds pricing/auth behavior as methods and a view on existing types; no new top-level API funcs, mutable globals, or singletons were introduced.
Title check ✅ Passed The title clearly matches the main change: adding cmux Pro pricing plus a Stack checkout flow.
Description check ✅ Passed The description is mostly complete with summary, testing, and implementation details, but the demo video and checklist sections are not fully filled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-pro-plan

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread web/app/api/billing/confirm/route.ts
Comment thread web/app/api/billing/checkout/route.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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-apps

greptile-apps Bot commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds cmux Pro paid conversion: a /pricing web page, Stripe and Stack checkout/confirm routes, read-time subscription reconciliation, and Pro upgrade entrypoints across the macOS app (sidebar badge, Settings Account card, command palette, Help menu), all gated behind a PostHog feature flag.

  • Web billing: new /api/billing/checkout (Stripe + legacy Stack paths), /api/billing/complete (Stripe webhook-free completion), /api/billing/confirm (bounded poll for Stack async lag), and /api/billing/plan; syncProPlanMetadata writes cmuxPlan: "pro" to clientReadOnlyMetadata and a read-time reconcile at VM-create corrects missed confirmations and lapses.
  • macOS app: ProUpgradePresenter opens /app-pricing as a transparent in-window browser split; CmuxFeatureFlags (PostHog) gates all upgrade surfaces; AuthEnvironment adds pricingURL/appPricingURL/billingCheckoutURL following the existing env-override resolution chain; en+ja localization added to Localizable.xcstrings.

Confidence Score: 4/5

Safe 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

Filename Overview
Sources/ProBadgeStyle.swift New file (380 lines) defining Pro badge variants, UserDefaults-backed style store, and a debug window controller. Two issues: non-localized "Get Pro"/"Pro" strings in user-facing badge text, and ProBadgeDebugWindowController shipped in the production binary without a #if DEBUG class guard.
Sources/PricingPlansScreen.swift New 768-line file with ProUpgradePresenter, NativePricingWindowController, and the full native pricing SwiftUI view. NativePricingPlanStore uses legacy ObservableObject/@published instead of @observable; NativePricingWindowController is private so its debug singleton is contained.
web/app/api/billing/checkout/route.ts New checkout route handling both Stripe and legacy Stack paths. The isAlreadyGrantedError heuristic was strengthened (requires a verified subscription read before granting Pro) since the prior review thread; redirect logic and error handling look correct.
web/app/api/billing/confirm/route.ts Bounded polling route (4 attempts × 1500ms) for Stack async subscription confirmation. Uses request.signal.aborted to short-circuit on disconnect.
web/services/billing/pro.ts Core Pro subscription helpers: paginated product lookup, metadata sync, read-time reconciliation, and Stripe DB query. Logic is clean and well-tested; the pagination cap (10 pages × 50 items) is a reasonable bound.
docs/pro-badge-options.html Design-review scratch document ("Pick a badge and I'll ship one, deleting the rest") with ephemeral references to a branch name and build tag. Should not be committed to source control.
Sources/FeatureFlags.swift New PostHog-backed feature flag class using @observable. The boolFlag helper correctly threads the per-flag fallback default on absent keys, addressing the prior review concern about ?? false fallbacks.
web/app/[locale]/pricing/page.tsx New /pricing page correctly registered in sitemap.ts and agentReadablePages. Uses next-intl for all visible copy; ProCtaLink handles the checkout/flag gate; ProWelcomeBanner renders post-checkout state.
Sources/Auth/AuthEnvironment.swift Adds pricingURL, appPricingURL, and billingCheckoutURL resolution following the existing env-override → DEBUG file → production fallback pattern. Tests in AuthEnvironmentTests.swift cover the new URL construction paths.

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"
Loading
%%{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"
Loading

Reviews (33): Last reviewed commit: "Capture mocked-module originals by value..." | Re-trigger Greptile

Comment on lines +27 to +38
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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)

Comment thread web/app/api/billing/confirm/route.ts Outdated

await syncProPlanMetadata(user, isPro);
return NextResponse.redirect(
new URL(isPro ? "/pro?welcome=1" : "/pro?welcome=pending", request.url),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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!

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread web/services/billing/pro.ts Outdated
// App-level lookup each attempt so no per-object store caching can
// return a stale product list mid-poll.
isPro = await hasActiveProSubscription({
listProducts: (options) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread web/services/billing/pro.ts
Comment thread web/app/[locale]/components/pro-welcome-banner.tsx
Comment thread web/app/api/billing/checkout/route.ts Outdated
let isPro = false;
for (let attempt = 0; attempt < VERIFY_ATTEMPTS; attempt++) {
if (attempt > 0) {
await new Promise((resolve) => setTimeout(resolve, VERIFY_SPACING_MS));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2313855 and bb6aa5d.

📒 Files selected for processing (12)
  • web/app/[locale]/components/nav-links.tsx
  • web/app/[locale]/components/pro-checkout-button.tsx
  • web/app/[locale]/components/pro-welcome-banner.tsx
  • web/app/[locale]/components/site-header.tsx
  • web/app/[locale]/page.tsx
  • web/app/[locale]/pro/page.tsx
  • web/app/api/billing/checkout/route.ts
  • web/app/api/billing/confirm/route.ts
  • web/messages/en.json
  • web/messages/ja.json
  • web/services/billing/pro.ts
  • web/tests/billing-pro.test.ts

"use client";

import { useTranslations } from "next-intl";
import posthog from "posthog-js";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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

Comment on lines +26 to +38
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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

Comment thread web/messages/en.json Outdated
…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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread web/app/[locale]/pro/page.tsx Outdated
const { locale } = await params;
setRequestLocale(locale);

const t = await getTranslations("pro");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread web/app/api/billing/confirm/route.ts Outdated
await syncProPlanMetadata(user, isPro);
return NextResponse.redirect(
new URL(
isPro ? "/pro?welcome=success" : "/pro?welcome=pending",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Add the new Pro keys to every supported locale. web/i18n/routing.ts lists 20 locales, but only web/messages/en.json and web/messages/ja.json include nav.pro, home.proSection/home.proTitle/home.proDesc/home.proPrice, and the pro.* 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

📥 Commits

Reviewing files that changed from the base of the PR and between bb6aa5d and 44e469a.

📒 Files selected for processing (10)
  • web/app/[locale]/components/pro-welcome-banner.tsx
  • web/app/api/billing/checkout/route.ts
  • web/app/api/billing/confirm/route.ts
  • web/app/api/vm/route.ts
  • web/messages/en.json
  • web/messages/ja.json
  • web/services/billing/pro.ts
  • web/services/vms/timings.ts
  • web/tests/billing-pro.test.ts
  • web/tests/vm-route-auth.test.ts

Comment on lines +15 to +26
const message =
welcome === "success"
? t("welcomeSuccess")
: welcome === "active"
? t("welcomeActive")
: welcome === "pending"
? t("welcomePending")
: billing === "error"
? t("billingError")
: billing === "unavailable"
? t("billingUnavailable")
: null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
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.

Comment thread web/app/api/vm/route.ts
Comment on lines +241 to +260
// 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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread web/app/api/vm/route.ts Outdated
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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread web/app/api/vm/route.ts Outdated
Comment on lines +247 to +249
const changed = await measureVmAsync(timing, "billing_reconcile", async () => {
const serverUser = await getStackServerApp().getUser(user.id);
return serverUser ? reconcileProPlanMetadata(serverUser) : false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 44e469a and 9ab8310.

📒 Files selected for processing (11)
  • web/app/[locale]/components/nav-links.tsx
  • web/app/[locale]/components/pro-checkout-button.tsx
  • web/app/[locale]/components/pro-welcome-banner.tsx
  • web/app/[locale]/components/site-header.tsx
  • web/app/[locale]/pricing/page.tsx
  • web/app/api/billing/checkout/route.ts
  • web/app/api/billing/confirm/route.ts
  • web/app/lib/agent-page-paths.ts
  • web/app/sitemap.ts
  • web/messages/en.json
  • web/messages/ja.json

Comment on lines +112 to +118
<Link
href="/pricing"
onClick={close}
className="hover:text-foreground transition-colors py-1"
>
{t("pricing")}
</Link>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
<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

Comment thread web/app/api/billing/checkout/route.ts Outdated
Comment thread web/app/api/billing/confirm/route.ts Outdated
Comment on lines +43 to +46
await syncProPlanMetadata(user, isPro);
return NextResponse.redirect(
new URL(
isPro ? "/pricing?welcome=success" : "/pricing?welcome=pending",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Suggested change
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

Comment thread web/app/api/billing/checkout/route.ts Outdated
Comment on lines +41 to +43
if (isAlreadyGrantedError(error)) {
await syncProPlanMetadata(user, true);
return NextResponse.redirect(new URL("/pricing?welcome=active", request.url));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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)

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread web/app/[locale]/pricing/page.tsx Outdated
))}
</ul>
<div>
<ProCheckoutButton size="sm" location="pricing_pro" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Comment thread web/app/api/billing/checkout/route.ts
Comment thread web/app/api/billing/confirm/route.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@socket-security

socket-security Bot commented Jul 6, 2026 •

Copy link
Copy Markdown

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.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm webpack is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ? → npm/@sentry/nextjs@10.63.0 → npm/webpack@5.108.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/webpack@5.108.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

lawrencecchen and others added 3 commits July 5, 2026 18:29
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,
),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 136f95d. Configure here.

Comment thread web/app/app-pricing/page.tsx
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>
Comment thread Sources/Auth/AuthEnvironment.swift
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>
lawrencecchen and others added 2 commits July 5, 2026 20:13
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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 4 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ 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!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 22fbba1. Configure here.

ProUpgradeCard(flow: accountFlow)
}
.settingsSearchAnchors(["setting:account:pro"])
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 22fbba1. Configure here.

lawrencecchen and others added 2 commits July 5, 2026 21:27
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>

This branch was successfully deployed

1 active deployment
Preview – cmux — f5f70091 Deployed Jul 6, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant