Skip to content

feat(web): in-app Cap Pro checkout with Stripe Checkout elements - #3603

Merged
richiemcilroy merged 10 commits into
mainfrom
feat/inline-pro-checkout
Oct 31, 2026
Merged

richiemcilroy merged 10 commits into
mainfrom
feat/inline-pro-checkout

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

Cap Pro checkout on the web now happens on Cap's own /checkout page instead of redirecting to hosted Stripe Checkout. It is built on Checkout Sessions with ui_mode: "elements" (Payment Element + Express Checkout Element), so Stripe still owns the session, Radar, 3DS, tax ID collection and promotion codes, and fulfilment still runs through the existing checkout.session.completed webhook.

The page only turns on when STRIPE_PUBLISHABLE_KEY is set. Without it, every route behaves exactly as on main.

Desktop Mobile
Before (hosted Checkout)
After (/checkout)

Flow

sequenceDiagram
    participant B as Browser
    participant P as /checkout (RSC)
    participant A as createProCheckout
    participant S as Stripe
    participant W as /api/webhooks/stripe
    B->>P: Pricing "Get Pro" (router.push)
    P->>A: start session during render (not awaited)
    A->>S: checkout.sessions.create(ui_mode: elements, currency)
    P-->>B: page streams, client secret arrives as a promise
    B->>S: Payment Element / Apple Pay confirm (3DS if needed)
    S->>W: checkout.session.completed
    W->>W: find or create user, set Pro, link customer.metadata.userId
    S-->>B: return_url /checkout/complete
Loading

Changes

  • Session creation apps/web/actions/billing/checkout.ts:69
    • Price, seats and currency are decided on the server; the client only sends plan, seats and onboarding.
    • Signed-out sessions go through the rl_guest_checkout firewall rate limit.
    • Sessions pin subscription_data.billing_mode.type = "classic" (:130), because the existing seat-change previews on API 2023-10-16 reject flexible billing subscriptions.
    • Uses a separate stripe-v23 client (apps/web/lib/checkout/stripe.ts:10) since elements mode and checkout.sessions.update are not in stripe-node 14. The rest of the app stays on stripe-node 14.
  • Monthly / yearly switch apps/web/actions/billing/checkout.ts:178
    • Authorised by a timing-safe match on the session client secret, limited to open elements sessions and the two Pro prices.
  • Currency apps/web/lib/visitor-country.ts:5
    • GBP or EUR only when x-vercel-ip-country comes from Vercel's edge (VERCEL=1, two-letter code), and only if both plan prices support it (apps/web/lib/checkout/plans.ts:50). Everything else is USD. /api/currency uses the same check.
  • Signed-in Pro users apps/web/actions/billing/current-plan.ts:64
    • /checkout shows the current plan and an owner-only "Add seats" flow (previewAddSeats / addSeats) that wraps the existing seat update path.
  • Routing
    • subscribe and guest-checkout return /checkout?plan=&seats= for web when enabled (apps/web/app/api/settings/billing/subscribe/route.ts:46). Desktop, mobile and hosted: true callers keep hosted Checkout.
    • Pricing cards navigate straight to /checkout and warm Stripe.js on pointer/touch/focus.
    • /checkout is frame protected (apps/web/proxy.ts:47).
  • Webhook fulfilment apps/web/app/api/webhooks/stripe/route.ts:526
    • Falls back to session.metadata.dubCustomerId when the Stripe customer has no userId metadata and its email no longer matches the account.
    • Links customer.metadata.userId after every completed checkout (:582), so later customer.subscription.updated / deleted events find the user. Guests get exactly one customer update.
  • Billing card fix apps/web/actions/organization/get-subscription-details.ts
    • Tiered prices have no unit_amount, so the per-seat price is now derived from the tiers instead of showing $0.
// apps/web/lib/checkout/enabled.ts
export const customCheckoutEnabled = () =>
	Boolean(serverEnv().STRIPE_SECRET_KEY && serverEnv().STRIPE_PUBLISHABLE_KEY);

Rollout

  • STRIPE_PUBLISHABLE_KEY turns the page on; removing it and redeploying sends every buyer back to hosted Checkout, no code change needed.
  • Apple Pay requires the serving domain to be registered as a Stripe payment method domain.

RetriggerView in GreptileConfidence Score: 5/5

The latest changes appear safe to merge; no new blocking issue was found.

Summary

Adds in-app Cap Pro checkout, with hosted Stripe Checkout kept as a fallback.

  • Cap creates Stripe sessions for the new web checkout.
  • Web buyers can complete Cap Pro checkout on Cap’s site.
  • Cap Pro owners can add seats and see the updated price.
  • Completed web purchases link the Stripe customer to the Cap account.
  • Eligible web buyers are routed to in-app checkout while hosted paths stay available.

All four earlier findings were rechecked. No new actionable issue was found in the latest changes.

Reviews (2) · Last reviewed commit: "fix(web): sign out before switching acco..." · Reviewed by Greptile

@socket-security

socket-security Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​stripe@​23.0.19910010096100
Addednpm/​@​stripe/​react-stripe-js@​7.0.19910010099100
Updatednpm/​@​stripe/​stripe-js@​3.5.0 ⏵ 10.2.0100100100100 +1100

View full report

@vercel

vercel Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

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

1 Skipped Deployment
Project Deployment Actions Updated
cap-status Skipped Skipped Oct 31, 2026 4:08pm UTC

Comment thread apps/web/lib/checkout/enabled.ts Resolved

@capy-ai capy-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.

Capy found 2 potential issues (2 medium).

View 1 other finding in Capy.

Open in Capy

@vercel
vercel Bot temporarily deployed to Preview – cap-status October 31, 2026 16:08 Inactive
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@capy-ai capy-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.

Capy found no issues.

Open in Capy

@richiemcilroy
richiemcilroy merged commit 6d78eb2 into main Oct 31, 2026
41 checks passed

This branch was successfully deployed

1 active and 1 inactive deployments
Preview – cap-web — 699e8f8f Deployed Oct 31, 2026 by vercel[bot]
Preview – cap-status — 699e8f8f Deployed Oct 31, 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