Repository navigation
Checkout buttons: spinner + direct Stripe redirect (no blank /api page) - #7810
Conversation
Buttons fetch the resolved checkout URL as JSON and redirect straight to Stripe
with a spinner, instead of navigating to /api/billing/checkout and flashing its
blank page while the Stripe session is built server-side.
- checkout route: ?format=json returns { url } (the same destination it would
302 to), reusing all existing logic; plain nav still 302s (no-JS fallback).
- CheckoutButton + useCheckoutRedirect (progressive enhancement: <a> that
intercepts a plain left-click, shows a spinner, redirects).
- Wired into app-pricing + [locale]/pricing Get Pro / Get Team CTAs and the
ProCtaLink pro CTA. Download/fallback links untouched. No new strings.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughCheckout links now support JSON-resolved destinations, client-side redirect handling with pending indicators, and fallback navigation. Pricing CTAs use the shared checkout button, while route tests cover configured and unavailable billing responses. ChangesCheckout flow
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Visitor
participant CheckoutButton
participant CheckoutRoute
participant Stripe
Visitor->>CheckoutButton: Click checkout link
CheckoutButton->>CheckoutRoute: Request checkout URL as JSON
CheckoutRoute->>Stripe: Create checkout session
Stripe-->>CheckoutRoute: Return session URL
CheckoutRoute-->>CheckoutButton: Return JSON URL
CheckoutButton->>Visitor: Navigate to checkout
Possibly related PRs
🚥 Pre-merge checks | ✅ 24 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (24 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.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c3f97ec. Configure here.
| ) { | ||
| return; | ||
| } | ||
| if (!href.startsWith(CHECKOUT_PATH)) return; |
There was a problem hiding this comment.
Absolute checkout URLs not intercepted
Medium Severity
useCheckoutRedirect only treats hrefs that start with the relative path /api/billing/checkout as checkout links. app-pricing passes absolute URLs from appPricingCheckoutURL (for example https://cmux.com/api/billing/checkout?plan=pro&…), so start exits early, the click is not intercepted, and those buttons never show the spinner or use format=json—they still do a full navigation through the blank checkout route.
Reviewed by Cursor Bugbot for commit c3f97ec. Configure here.
Greptile SummaryThis PR changes checkout CTAs to fetch the redirect target before navigating. The main changes are:
Confidence Score: 4/5The changed checkout flow needs fixes before merging. Absolute app-pricing links still use the old blank-page path, and the JSON wrapper can lose anonymous-session cookies.
web/app/components/checkout-navigation.tsx, web/app/api/billing/checkout/route.ts Important Files Changed
Reviews (1): Last reviewed commit: "checkout: spinner on Get Pro/Team, no bl..." | Re-trigger Greptile |
| ) { | ||
| return; | ||
| } | ||
| if (!href.startsWith(CHECKOUT_PATH)) return; |
There was a problem hiding this comment.
Absolute Checkout URLs Bypass Spinner
When app-pricing passes an absolute same-origin checkout URL, this raw prefix check returns false because the string starts with https://... instead of /api/.... Those buttons keep doing the old full navigation to /api/billing/checkout, so users still see the blank API page before Stripe.
| if (!href.startsWith(CHECKOUT_PATH)) return; | |
| const checkoutUrl = new URL(href, window.location.origin); | |
| if (checkoutUrl.origin !== window.location.origin || checkoutUrl.pathname !== CHECKOUT_PATH) return; |
| return NextResponse.json({ | ||
| url: location ?? new URL("/pricing?billing=error", request.url).toString(), | ||
| }); |
There was a problem hiding this comment.
Anonymous Checkout Drops Cookies
The JSON path builds a fresh response and only copies the redirect URL. On signed-out checkout, resolveCheckout() can create an anonymous Stack user before redirecting; if that sets the session cookie on the original response, the browser reaches Stripe but returns to /api/billing/complete without the anonymous session needed to attach the purchase to that user.
| if (pending) return; | ||
| setPending(true); |
There was a problem hiding this comment.
Pending Guard Allows Duplicate Sessions
The duplicate-click guard depends on React state that updates after the handler returns. A fast second activation can run the old callback with pending === false, issue a second format=json fetch, and create another Stripe Checkout Session before the first redirect wins.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/components/checkout-navigation.tsx`:
- Around line 33-34: Update the checkout link handling in the checkout
navigation component to parse href with the URL API and compare its pathname
against CHECKOUT_PATH, rather than using href.startsWith. Preserve the existing
event prevention and loading behavior after matching checkout URLs, including
absolute URLs returned by appPricingCheckoutURL.
🪄 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: 778f9fed-f388-49a7-ae8e-cd209fefcae9
📒 Files selected for processing (6)
web/app/[locale]/components/pro-cta-link.tsxweb/app/[locale]/pricing/page.tsxweb/app/api/billing/checkout/route.tsweb/app/app-pricing/page.tsxweb/app/components/checkout-navigation.tsxweb/tests/billing-checkout-route.test.ts
| if (!href.startsWith(CHECKOUT_PATH)) return; | ||
| event?.preventDefault(); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 1. Check billing URL helper implementations and path constants
rg -n -A10 'withCheckoutExternalBrowserIntent|appPricingCheckoutURL|TEAM_CHECKOUT_PATH|PRO_CHECKOUT_PATH' web/app/lib/billing.ts
# 2. Check if --button-foreground CSS variable is defined (affects CheckoutButton vs ProCtaLink color consistency)
rg -n -e '--button-foreground' web/ -g '*.css' -g '*.scss' -g '*.tsx'
# 3. Check PrimaryLink style for comparison (previous component replaced by CheckoutButton)
rg -n -A15 'function PrimaryLink' web/app/components/pricing-shared.tsxRepository: manaflow-ai/cmux
Length of output: 2391
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== checkout-navigation =="
sed -n '1,220p' web/app/components/checkout-navigation.tsx
echo
echo "== billing helpers =="
sed -n '1,220p' web/app/lib/billing.ts
echo
echo "== app-pricing page call site =="
sed -n '1,220p' web/app/app-pricing/page.tsx
echo
echo "== any origin-sensitive checkout URL builder =="
rg -n -A8 -B4 'configuredAppPricingCheckoutURL|appPricingCheckoutURL|withCheckoutExternalBrowserIntent|CHECKOUT_EXTERNAL_BROWSER_PARAM|CHECKOUT_PATH' web/app/lib web/app/app-pricingRepository: manaflow-ai/cmux
Length of output: 20607
Match checkout links by pathname, not raw string prefix
appPricingCheckoutURL(...) can return absolute URLs, so href.startsWith(CHECKOUT_PATH) skips interception on the app-pricing page and the button falls back to a plain navigation with no spinner. Normalize the URL and compare the pathname instead.
🤖 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/components/checkout-navigation.tsx` around lines 33 - 34, Update the
checkout link handling in the checkout navigation component to parse href with
the URL API and compare its pathname against CHECKOUT_PATH, rather than using
href.startsWith. Preserve the existing event prevention and loading behavior
after matching checkout URLs, including absolute URLs returned by
appPricingCheckoutURL.


Clicking Get Pro / Get Team navigated to
/api/billing/checkout, which built the Stripe session server-side and then 302'd to Stripe — so you saw that route's blank page for the 300–800ms round-trip. Now the buttons show a spinner and redirect straight to Stripe.How
GET /api/billing/checkout?format=jsonruns the exact same logic, then returns the destination as{ url }instead of a 302. The url is whatever it would have redirected to (the Stripe Checkout URL on success, or a/pricing?...state URL otherwise), so the client just navigates to it either way. The plain navigation still 302s — a no-JS fallback.CheckoutButton/useCheckoutRedirectrender as an<a>(open-in-new-tab and no-JS still work) but intercept a plain left-click: spinner on, fetch the JSON,window.location.assign(url). Wired into the Get Pro / Get Team CTAs onapp-pricingand[locale]/pricing, and intoProCtaLink(the PostHog-flagged pro CTA). Download/fallback links are untouched.No hover prefetch (it would mint side-effectful Stripe sessions for everyone who hovers) and no new user-facing strings (the spinner is visual +
aria-busy).Verification
bun run typecheckclean; full web suite green.format=jsonreturns the Stripe URL as JSON on success, and the/pricing?billing=unavailabledestination as JSON when Stripe is unconfigured (no session created).Web-only (no Swift); Vercel preview below.
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Medium Risk
Touches the billing checkout entry path and client redirect behavior; server checkout logic is unchanged with 302 and fetch fallbacks, but a JSON/client bug could affect paid upgrade flows.
Overview
Get Pro / Get Team no longer send users through a blank
/api/billing/checkoutpage while Stripe sessions are created. Checkout CTAs stay real<a>links (no-JS and open-in-new-tab still work), but a plain left-click shows a spinner and navigates straight to the final destination.The checkout API gains
?format=json, which runs the same logic as before and returns{ url }(Stripe Checkout or a/pricing?billing=…URL) instead of a 302.CheckoutButtonanduseCheckoutRedirectfetch that JSON and callwindow.location.assign; on failure they fall back to the original href.ProCtaLinkonly intercepts the checkout href—the download fallback is unchanged.Wired on
[locale]/pricing,app-pricing, and compare-table team CTAs; tests cover JSON success and unconfigured Stripe.Reviewed by Cursor Bugbot for commit c3f97ec. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Checkout buttons now show a spinner and redirect straight to Stripe, removing the blank
/apipage flash. No-JS users still get the normal 302 redirect.GET /api/billing/checkout?format=jsonreturns{ url }for the same destination the route would 302 to; default navigation still 302s.CheckoutButtonanduseCheckoutRedirect: anchors that intercept a plain left-click, show a spinner, fetch JSON, andwindow.location.assign(url); open-in-new-tab and no-JS work.app-pricing,[locale]/pricing, andProCtaLink; download/fallback links unchanged.Written for commit c3f97ec. Summary will update on new commits.
Summary by CodeRabbit
New Features
Bug Fixes
Tests