Skip to content

feat(checkout): three-step checkout (PR #194 variant B) + payment-flow battle-test harness#196

Merged
kilbot merged 11 commits into
mainfrom
worktree-checkout-v2
Jul 2, 2026
Merged

feat(checkout): three-step checkout (PR #194 variant B) + payment-flow battle-test harness#196
kilbot merged 11 commits into
mainfrom
worktree-checkout-v2

Conversation

@kilbot

@kilbot kilbot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Two things in one coherent change, built for the launch in two days:

  1. Checkout v2 — the design picked in prototype: /pro pricing + checkout variants (do not merge) #194 (variant B): Account → Billing → Payment as collapsing steps with a sticky order summary.
  2. A battle-test harness for the money path: the mocked backend now covers registration, cart completion, license issuance, all three payment providers, and the failure modes — so the launch-critical flows run in CI on every push.

Checkout v2

  • Inline account step — signed-out buyers create their account without leaving checkout (register; on 409 ACCOUNT_EXISTS the same form flips to sign-in). Middleware no longer bounces /pro/checkout to /login; the cart APIs still enforce auth server-side. OAuth remains available via /login with redirect-back.
  • Billing address step — collected once, persisted to the Medusa cart (billing_address through the existing PATCH route), collapsible/editable.
  • Payment step — Stripe Express Checkout wallets (Apple/Google Pay/Link) on top when available; radio accordion below with Card default and expanded, PayPal and Bitcoin as first-class rows (the prototype: /pro pricing + checkout variants (do not merge) #194 payment spec). The wallet path confirms through the exact same safety pipeline as the card form.
  • No blocking init spinner — the form renders immediately; the cart initializes in the background (billing submit awaits it when racing).
  • Safety machinery untouched: order_pending full-page block, payment_uncertain persistence, sessionStorage restore, owner alerting, customer-safe Stripe error mapping — all reused verbatim and re-pinned by tests.

The battle-test harness

Mock backend additions (e2e/mocks/server.mjs): emailpass register/login + customer creation (dynamic personas), cart completion that mints orders with license metadata, provider-shaped session data, a BTCPay invoice simulator (redirect → simulate payment → webhook-settles the order → returns to success), and deterministic failure injections keyed by email prefix (fail-session+, order-pending+, fail-complete+).

Coverage now proven in CI (71/71 mocked e2e, chromium):

Flow Covered by
New customer: register inline → billing → pay with Bitcoin → success page → license visible in the new account journey e2e (full browser)
Existing email → sign-in flip → continue; wrong password stays on step 1 with error journey e2e
Signed-in customer full Bitcoin purchase journey e2e
Order-pending (paid, no order) blocks checkout across reloads; not re-payable journey e2e (seeded protective state) + unit
Payment-session init failure surfaces an actionable error journey e2e (fail-session+ injection)
API-level provider matrix: cart→session→complete for pp_stripe_stripe, pp_paypal_paypal, pp_btcpay_btcpay → real order + license metadata request-level e2e
Completion returning no order → 409 order_pending wire contract; Medusa 5xx never reports success request-level e2e
Fresh checkout-created account immediately reads its own licenses via API request-level e2e
Method switching, billing persistence/edit, account collapse, cart-init failure, no-product error, auth gating of cart APIs pro-checkout e2e + 22 client unit tests

What still needs real credentials (can't be mocked honestly)

  • Stripe card confirm + wallets: e2e/pro-checkout-integration.spec.ts (@integration) updated to drive the new UI — needs NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY + E2E_TEST_EMAIL/PASSWORD + MEDUSA_API_KEY against staging. Wallets additionally need a real device (manual: open checkout on a phone with Apple/Google Pay in test mode).
  • PayPal approval popup: PayPal sandbox account, manual run-through.
  • BTCPay live invoice: one manual invoice against the real BTCPay server.
  • Vercel env: confirm NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, NEXT_PUBLIC_PAYPAL_CLIENT_ID, NEXT_PUBLIC_BTCPAY_ENABLED are set for production so all three rows render.

Validation

  • Unit: 915 tests / 134 files passing (client suite rewritten for the stepper: 22 tests incl. all safety pins)
  • E2E: 71/71 mocked suite (chromium), including the 12 new journey tests
  • Lint clean; production build passes (built twice by the e2e webServer)

Notes

Summary by CodeRabbit

  • New Features
    • Roll out a step-based Pro checkout experience with inline account sign in/create, billing address capture, and payment method selection (including BTCPay/Bitcoin where enabled), plus an order-complete screen and improved step navigation/editing.
  • Bug Fixes
    • Unauthenticated users no longer get redirected away from checkout; the flow now proceeds to inline account creation/sign-in.
    • Improved checkout recovery/safety behavior for pending or failed payment scenarios, including clearer error messaging and state handling across retries/reloads.
    • Strengthened API security by enforcing cart ownership and rejecting cross-origin auth requests.

…t harness

Implements the checkout design picked in PR #194 (variant B) and builds
the launch test harness around it.

Checkout v2 (Account → Billing → Payment, collapsing steps + sticky
summary):
- Inline account step: signed-out buyers register (or sign in on 409
  ACCOUNT_EXISTS) without leaving checkout; middleware no longer gates
  /pro/checkout (cart APIs still enforce auth server-side); OAuth stays
  available via /login with redirect-back.
- Billing address step persisted to the Medusa cart (billing_address
  passes through the existing PATCH route).
- Payment step: Stripe Express Checkout wallets on top; radio accordion
  with Card default/expanded and PayPal + Bitcoin as first-class rows.
  All provider components (CheckoutForm/PayPalButton/BTCPayButton) and
  the entire checkout-safety machinery (order_pending protection,
  payment_uncertain persistence, owner alerting) are reused unchanged;
  wallets confirm through the same safety path as the card form.
- No blocking init spinner: the form renders instantly and the cart
  initializes in the background (billing submit awaits it if needed).

Battle-test harness (mocked, runs in CI):
- Mock backend now covers the full money path: emailpass register/login,
  customer creation, dynamic personas, cart completion that mints orders
  with license metadata, per-provider session data, a BTCPay invoice
  simulator (redirect → pay → webhook-settle → success), and failure
  injections (fail-session+, order-pending+, fail-complete+ emails).
- e2e/pro-checkout-journeys.spec.ts: new-customer registers inline and
  buys with Bitcoin end-to-end, license appears in the new account;
  existing-email sign-in flip; wrong-password stays put; signed-in BTC
  purchase; order-pending blocks checkout across reloads; session
  failure surfaces; API-level provider matrix (stripe/paypal/btcpay
  complete → order + license metadata; order_pending 409 contract;
  5xx completion never reports success); fresh account reads its own
  licenses via the API.
- pro-checkout.spec.ts reworked for the stepper; auth/middleware specs
  updated to pin the new no-login-bounce contract; @integration Stripe
  spec updated to drive the new UI (billing step, card row).

Validation: 915 unit tests, 71/71 mocked e2e (chromium), lint clean,
production build via the e2e webServer.

@greptile-apps greptile-apps 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.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@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

Checkout now uses inline account, billing, and payment steps, with cart ownership checks, same-origin auth guards, BTCPay support, and updated mock backend and E2E coverage.

Changes

Inline checkout redesign

Layer / File(s) Summary
Auth and origin guards
src/middleware.ts, src/middleware.test.ts, e2e/auth.spec.ts, src/lib/api/same-origin.ts, src/app/api/auth/login/route.ts, src/app/api/auth/register/route.ts
/pro/checkout is no longer middleware-gated, same-origin checks were added to auth routes, and checkout/account access tests now reflect inline checkout behavior.
Checkout page and client core
src/app/[locale]/(main)/pro/checkout/page.tsx, src/components/pro/checkout-client.tsx, src/components/pro/checkout-form.tsx, src/components/pro/stripe-provider.tsx
The checkout page drops the locale redirect, derives offer summary data, and wires the step-based checkout client with background cart initialization, billing submission, and payment-session selection.
Account and billing steps
src/components/pro/checkout/step-shell.tsx, src/components/pro/checkout/account-step.tsx, src/components/pro/checkout/billing-step.tsx
New step shell, inline account register/sign-in form, and billing address form render the first two checkout steps.
Payment step and express checkout
src/components/pro/checkout/payment-step.tsx, src/components/pro/checkout/express-checkout.tsx
A payment step selector is added for Stripe, PayPal, and BTCPay, along with Stripe express checkout confirmation handling.
Cart API ownership checks
src/app/api/store/cart/route.ts, src/app/api/store/cart/line-items/route.ts, src/app/api/store/cart/payment-sessions/route.ts, src/app/api/store/cart/complete/route.ts, src/app/api/store/cart/*.test.ts
Cart read, update, line-item, payment-session, and completion routes now verify cart ownership against the authenticated customer.
Medusa auth exchange
src/lib/medusa-auth.ts, src/lib/medusa-auth.test.ts
register now exchanges the registration token for a session token after customer creation, and the test expects the extra fetch.
Mock backend and E2E coverage
e2e/mocks/server.mjs, e2e/helpers/checkout.ts, e2e/pro-checkout.spec.ts, e2e/pro-checkout-integration.spec.ts, e2e/pro-checkout-journeys.spec.ts, playwright.config.ts
The mock backend adds dynamic personas, BTCPay simulation, and cart completion behavior, while shared helpers and Playwright specs exercise inline checkout and provider flows.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • wcpos/wcpos-com#116: Both PRs change the checkout E2E and mock backend payment-session behavior.
  • wcpos/wcpos-com#145: Both PRs modify src/app/api/store/cart/complete/route.ts completion behavior and failure classification.
  • wcpos/wcpos-com#178: Both PRs touch the checkout safety-state flow in src/components/pro/checkout-client.tsx.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: a three-step checkout flow and a payment-flow test harness.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-checkout-v2

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

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-548g9gs2f-wcpos.vercel.app

@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: a04b40c6e0

ℹ️ 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 src/components/pro/checkout-client.tsx Outdated
Comment thread e2e/mocks/server.mjs
@wcpos-bot

wcpos-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review-fix triage before code changes:

# Source File Class Category Decision Outcome
1 chatgpt-codex-connector checkout-client.tsx Logic P1 correctness Fix Refresh local cart from billing PATCH response and recreate payment session before advancing.
2 chatgpt-codex-connector server.mjs Logic P2 test isolation Fix Prevent completed checkout orders from mutating shared fixture personas.

Skipped threads: none at triage time; both unresolved threads are actionable and planned for fixes.

@greptile-apps greptile-apps 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.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@wcpos-bot

wcpos-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review-thread fixes pushed in f4168af.

Thread File Issue Decision Commit
discussion_r3509641024 checkout-client.tsx Refresh cart/payment session after billing changes so recalculated totals and active provider session stay in sync. Fixed f4168af
discussion_r3509641029 server.mjs Isolate completed orders from shared fixture personas in the mock backend. Fixed f4168af

Excluded threads:

Thread Reason for skipping
None No unresolved review threads were skipped.

Validation:

  • pnpm install --frozen-lockfile passed.
  • pnpm lint passed with existing warnings in src/services/core/business/electron-service.ts.
  • pnpm exec tsc --noEmit passed.
  • pnpm test:unit passed: 916 tests.
  • Focused e2e regression passed: pnpm test:e2e --project=chromium -g "completed fixture-account checkouts do not mutate shared fixture orders".
  • Full pnpm test:e2e --project=chromium is locally blocked by missing Chromium system library libglib-2.0.so.0; pnpm exec playwright install --with-deps chromium could not elevate (su: Authentication failure).

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-41t76iguh-wcpos.vercel.app

@wcpos-bot

wcpos-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Follow-up PR-fix status after fresh review-thread inventory (2026-07-02T00:10:57Z): unresolved review threads remaining: 0.

Thread File Issue Decision Commit
discussion_r3509641024 checkout-client.tsx Refresh cart/payment session after billing changes so recalculated totals and active provider session stay in sync. Fixed and resolved f4168af
discussion_r3509641029 server.mjs Isolate completed orders from shared fixture personas in the mock backend. Fixed and resolved f4168af

Excluded threads:

Thread Reason for skipping
None Fresh GraphQL inventory returned no unresolved review threads.

Current validation state:

  • GitHub Actions Test passed.
  • GitHub Actions E2E Tests passed: 72 passed.
  • CodeQL checks passed.
  • Deploy Preview passed.
  • Remaining merge-state blocker: CodeRabbit status context is still pending / review in progress.

@wcpos-bot

wcpos-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review-fix triage after fixes:

Thread File Issue Decision Commit
PRRT_kwDOQ0-C_M6Nvr-l checkout-client.tsx Refresh cart/payment session after billing changes so totals and active provider state stay current. Fixed f4168af
PRRT_kwDOQ0-C_M6Nvr-n server.mjs Prevent completed fixture-account checkouts from mutating shared fixture persona orders. Fixed f4168af

Skipped threads: none. Fresh unresolved inline review-thread inventory returned 0.

@wcpos-bot

wcpos-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Follow-up PR-fix status after a fresh unresolved-thread inventory: unresolved inline review threads remaining: 0.

No additional code changes were needed in this round. The PR head is still f4168af, which already contains the single batched fix commit for the two actionable review threads.

Thread File Issue Decision Commit
PRRT_kwDOQ0-C_M6Nvr-l checkout-client.tsx Refresh cart/payment session after billing changes so recalculated totals and active provider session stay current. Fixed and resolved f4168af
PRRT_kwDOQ0-C_M6Nvr-n server.mjs Prevent completed checkout orders from mutating shared fixture personas across fullyParallel mocked specs. Fixed and resolved f4168af

Excluded threads:

Thread Reason for skipping
None Fresh GraphQL inventory returned no unresolved review threads.

Current blocker: GitHub Actions and CodeQL are passing, but the CodeRabbit status context is still pending, so GitHub reports mergeStateStatus: UNSTABLE until that external status completes.

@wcpos-bot

wcpos-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Follow-up PR-fix status after fresh unresolved-thread inventory: unresolved inline review threads remaining: 0.

No additional code changes were needed in this round. The PR head remains f4168af, which already contains the single batched fix commit for the two actionable review threads.

Thread File Issue Decision Commit
PRRT_kwDOQ0-C_M6Nvr-l checkout-client.tsx Refresh cart/payment session after billing changes so recalculated totals and active provider state stay current. Fixed and resolved f4168af
PRRT_kwDOQ0-C_M6Nvr-n server.mjs Prevent completed checkout orders from mutating shared fixture personas across fullyParallel mocked specs. Fixed and resolved f4168af

Excluded threads:

Thread Reason for skipping
None Fresh GraphQL inventory returned no unresolved review threads.

Validation in this round:

  • pnpm install --frozen-lockfile passed.
  • pnpm lint passed with 4 existing warnings in src/services/core/business/electron-service.ts.
  • pnpm exec tsc --noEmit passed.
  • pnpm test:unit passed: 134 files / 916 tests.
  • Local pnpm test:e2e --project=chromium is blocked by the runner image missing libglib-2.0.so.0 for Chromium. GitHub Actions E2E Tests for this head passed.

Current blocker: GitHub Actions and CodeQL are passing, and review threads are resolved, but the external CodeRabbit status context is still pending, so GitHub still reports mergeStateStatus: UNSTABLE.

@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)
src/components/pro/checkout-client.tsx (1)

109-131: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

resolvePaymentSession skips the legacy singular fallback whenever payment_sessions is non-empty, even if no match is found.

The first branch (payment_collection.payment_sessions) correctly only returns when a match is actually found (Line 116-118, if (collectionSession) { return collectionSession }). The second branch instead gates on array length, not on whether .find() actually matched:

if (cart.payment_sessions?.length) {
    return cart.payment_sessions.find(
      (session) => session.provider_id === providerId
    )
  }

If cart.payment_sessions is non-empty but contains no session for providerId, this returns undefined immediately — the subsequent cart.payment_session (singular) fallback check on Line 126 is never reached, even though it might hold the matching session for that provider. This directly affects PayPal/BTCPay checkout-link resolution (paypalSession/btcpaySession at Lines 474-479), and none of the current unit tests combine a populated payment_sessions array with a payment_session singular field, so this gap isn't caught by the suite.

🐛 Suggested fix
   if (cart.payment_sessions?.length) {
-    return cart.payment_sessions.find(
+    const legacySession = cart.payment_sessions.find(
       (session) => session.provider_id === providerId
     )
+    if (legacySession) {
+      return legacySession
+    }
   }
 
   if (cart.payment_session?.provider_id === providerId) {
     return cart.payment_session
   }
 
   return undefined
🤖 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 `@src/components/pro/checkout-client.tsx` around lines 109 - 131,
`resolvePaymentSession` returns too early from the `payment_sessions` array
branch and skips the legacy `payment_session` fallback when no provider match is
found. Update `resolvePaymentSession` in `checkout-client.tsx` so it only
returns from `cart.payment_sessions` when `.find()` actually matches, then falls
through to the singular `cart.payment_session` check. Make sure the
`paypalSession` and `btcpaySession` lookup paths still resolve correctly when
only the legacy singular session matches.
🧹 Nitpick comments (8)
src/components/pro/checkout-client.tsx (1)

90-94: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

DEFAULT_PAYMENT_METHOD silently falls back to 'btcpay' even when no provider is enabled.

If isStripeEnabled, isPayPalEnabled, and isBTCPayEnabled are all false (misconfiguration), this still resolves to 'btcpay' and initializeCheckout will attempt createPaymentSession for a provider that isn't actually configured, producing a confusing "Failed to initialize payment" error instead of surfacing the real "no payment methods configured" state that PaymentStep already handles gracefully (Lines 114-120 of payment-step.tsx).

🤖 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 `@src/components/pro/checkout-client.tsx` around lines 90 - 94, The
DEFAULT_PAYMENT_METHOD fallback in checkout-client.tsx is masking the
misconfiguration case by resolving to 'btcpay' even when no payment provider is
enabled. Update the PaymentMethod selection logic near DEFAULT_PAYMENT_METHOD so
it only chooses a provider that is actually enabled, and make the no-provider
case explicit instead of defaulting to BTCPay. Then ensure initializeCheckout
uses that signal to avoid calling createPaymentSession for an unavailable
provider and let the existing PaymentStep “no payment methods configured”
handling remain the user-facing path.
src/app/[locale]/(main)/pro/checkout/page.tsx (1)

138-155: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Suspense fallback skeleton no longer matches the new checkout layout.

The fallback still renders a 2-panel md:grid-cols-2 skeleton left over from the old two-column design, while the actual loaded content is now a 3-step list plus a sticky summary in a [1.6fr_1fr] grid (per checkout-client.tsx Line 519). This is a brief, self-correcting visual mismatch during the loading flash, but worth aligning for polish.

🤖 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 `@src/app/`[locale]/(main)/pro/checkout/page.tsx around lines 138 - 155, The
Suspense fallback in CheckoutContent no longer matches the current checkout
layout. Update the fallback skeleton inside the checkout page’s Suspense block
to mirror the new structure from CheckoutContent/checkout-client.tsx: a 3-step
list area plus a sticky summary column in the same [1.6fr_1fr] grid. Remove the
old md:grid-cols-2 two-panel skeleton and align the Skeleton placeholders with
the new checkout sections so the loading state matches the rendered UI.
src/components/pro/checkout/step-shell.tsx (1)

63-71: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

"Edit" buttons lack contextual accessible names.

editLabel is rendered as the sole visible/accessible text for the button (Line 69). Since CheckoutClient passes editLabel="Edit" for both the Account and Billing steps, a screen reader user navigating by buttons will hear two indistinguishable "Edit" controls with no indication of what each edits.

♻️ Suggested fix
         <button
           type="button"
           onClick={onEdit}
+          aria-label={`Edit ${title}`}
           className="text-sm text-muted-foreground underline underline-offset-4"
         >
           {editLabel}
         </button>
🤖 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 `@src/components/pro/checkout/step-shell.tsx` around lines 63 - 71, The
StepShell edit button uses only editLabel, so both Account and Billing render as
indistinguishable “Edit” controls. Update the button in StepShell to include a
contextual accessible name derived from the step or section title while keeping
the visible label unchanged, using the existing onEdit/editLabel props and the
step context passed into this component.
src/components/pro/checkout/express-checkout.tsx (1)

44-117: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Keep a local in-flight guard around handleConfirm. The ExpressCheckoutElement confirm flow is intended as a single-trigger event, but Stripe does not promise your handler won’t be re-entered by the UI or app state, and the docs still recommend preventing duplicate submissions yourself. This checkout path should mirror the existing isProcessing protection used in the card flow.

🤖 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 `@src/components/pro/checkout/express-checkout.tsx` around lines 44 - 117, The
`handleConfirm` flow in `ExpressCheckoutElement` can be re-entered, so add a
local in-flight guard like the `isProcessing` protection used in the card
checkout path. Track a boolean state or ref in `ExpressCheckout` and
short-circuit `handleConfirm` when a confirm is already running, then set/reset
it around the `stripe.confirmPayment` and `completeProviderConfirmedCheckout`
work so duplicate submissions are prevented.
e2e/mocks/server.mjs (1)

319-338: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Customer creation mutates the shared fixture object for fixture personas.

personaForRequest returns a direct reference into fixtures.personas[base] for non-dynamic tokens. If this route is ever hit by a fixture persona whose .customer is still null (currently unlikely since named fixtures already carry a customer), auth.persona.customer = {...} permanently mutates the shared, process-wide fixture object rather than a per-test copy — leaking state into every later test that resolves the same fixture persona.

Cheap to future-proof: only mutate a stored copy for dynamic personas, or deep-clone fixture personas before returning them from personaForRequest.

🛡️ Suggested defensive fix
 function personaForRequest(req) {
   const auth = req.headers.authorization || ''
   const token = auth.startsWith('Bearer ') ? auth.slice('Bearer '.length) : ''
   if (!token) return null

   const dynamic = dynamicPersonas.get(token)
   if (dynamic) return { persona: dynamic, suffix: null }

   const { base, suffix } = splitSuffix(token)
-  const persona = fixtures.personas[base]
+  const persona = fixtures.personas[base]
   if (!persona) return null

-  return { persona, suffix }
+  // Never let request handlers mutate the shared fixture singleton.
+  return { persona: structuredClone(persona), suffix }
 }
🤖 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 `@e2e/mocks/server.mjs` around lines 319 - 338, The customer creation handler
in the server mock mutates the shared fixture persona returned by
personaForRequest, which can leak state across tests. Update personaForRequest
and/or the /store/customers POST handling so only dynamic personas are mutated,
or return a cloned persona for fixture-backed identities before assigning
auth.persona.customer. Keep the fix localized around personaForRequest,
auth.persona.customer, and the customer creation route.
e2e/pro-checkout-integration.spec.ts (1)

52-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the new completeBillingStep helper instead of re-duplicating the fill sequence.

Lines 64-74 duplicate completeBillingStep from e2e/helpers/checkout.ts field-for-field (same locators, same "Continue to payment" click, same step-3 wait). None of this file's assertions depend on the specific name/address values entered, so the helper can be reused directly.

♻️ Proposed refactor
-import { test, expect } from '`@playwright/test`'
+import { test, expect } from '`@playwright/test`'
+import { completeBillingStep } from './helpers/checkout'
     await expect(page.getByTestId('checkout-steps')).toBeVisible({
       timeout: 30000,
     })
 
-    // Billing address step
-    await expect(page.getByTestId('billing-step-form')).toBeVisible({
-      timeout: 15000,
-    })
-    await page.getByLabel('First name').fill('E2E')
-    await page.getByLabel('Last name').fill('Tester')
-    await page.getByLabel('Address').fill('1 Integration Way')
-    await page.getByLabel('City').fill('Sydney')
-    await page.getByLabel('Postal code').fill('2000')
-    await page.getByLabel('Country').selectOption('au')
-    await page.getByRole('button', { name: /continue to payment/i }).click()
+    // Billing address step
+    await completeBillingStep(page)
🤖 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 `@e2e/pro-checkout-integration.spec.ts` around lines 52 - 81, The billing-step
interaction in this checkout test duplicates the existing completeBillingStep
helper logic instead of reusing it. Replace the repeated field fills and
continue-click flow in the e2e/pro-checkout-integration.spec.ts scenario with a
call to completeBillingStep from e2e/helpers/checkout.ts, and keep only the
assertions that are specific to this test, such as the payment-method
verification.
e2e/pro-checkout-journeys.spec.ts (2)

129-186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert registration succeeded before exercising the sign-in-fallback flow.

Lines 135-137 and 166-168 seed the "existing account" state via page.request.post('/api/auth/register', ...) without checking the response, unlike registerViaApi (lines 74-83) which does. If seeding silently fails, the later account-exists-notice assertion fails with a misleading root cause.

♻️ Proposed fix
-    await page.request.post('/api/auth/register', {
-      data: { email, password: PASSWORD },
-    })
+    const seed = await page.request.post('/api/auth/register', {
+      data: { email, password: PASSWORD },
+    })
+    expect(seed.status()).toBe(200)

(apply the same change at the other seeding call site)

🤖 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 `@e2e/pro-checkout-journeys.spec.ts` around lines 129 - 186, The two
account-seeding calls in the checkout journey tests do not verify that
registration succeeded before testing the sign-in fallback flow. Update the
setup in the affected specs to assert the `/api/auth/register` response is
successful, following the same pattern used by `registerViaApi`, so failures are
caught at the seeding step rather than later in the `account-exists-notice` or
sign-in assertions.

248-260: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Centralize the checkout trigger literals
fail-session+, order-pending+, fail-complete+, PURC-HASE-FLOW-7777, and lic-e2e-purchase are duplicated across e2e/pro-checkout-journeys.spec.ts, e2e/mocks/server.mjs, and e2e/mocks/fixtures.json. Move them to shared e2e constants so the mock/test contract can’t drift.

🤖 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 `@e2e/pro-checkout-journeys.spec.ts` around lines 248 - 260, Centralize the
checkout trigger literals used by this test flow so the mock and spec stay in
sync; the duplicated values like fail-session+, order-pending+, fail-complete+,
PURC-HASE-FLOW-7777, and lic-e2e-purchase should be moved into shared e2e
constants. Update the references in pro-checkout-journeys.spec.ts,
mocks/server.mjs, and mocks/fixtures.json to consume the shared constants, and
keep the test behavior in the payment-session failure case wired through those
symbols rather than inline strings.
🤖 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 `@e2e/mocks/server.mjs`:
- Around line 535-568: The cart completion mock in the
/store/carts/{id}/complete handler is not idempotent because it always calls
completeCartIntoOrder(cart), which can mint duplicate orders on retries. Update
this path to short-circuit when the cart already has an order_id, matching the
retry guard used in the BTCPay pay flow, and return the existing completed
result instead of creating a new order.

In `@src/components/pro/checkout-client.tsx`:
- Around line 378-414: handleBillingSubmit currently treats billing save and
payment-session refresh as one failure path, so a createPaymentSession error is
surfaced as a billing-save error. Split the two phases in handleBillingSubmit so
the PATCH to /api/store/cart and the createPaymentSession call are handled
separately, and wrap the payment refresh failure with a distinct message before
it bubbles to BillingStep. Keep the success path setting cart,
paymentCollectionId, clientSecret, and step only after both phases succeed.

---

Outside diff comments:
In `@src/components/pro/checkout-client.tsx`:
- Around line 109-131: `resolvePaymentSession` returns too early from the
`payment_sessions` array branch and skips the legacy `payment_session` fallback
when no provider match is found. Update `resolvePaymentSession` in
`checkout-client.tsx` so it only returns from `cart.payment_sessions` when
`.find()` actually matches, then falls through to the singular
`cart.payment_session` check. Make sure the `paypalSession` and `btcpaySession`
lookup paths still resolve correctly when only the legacy singular session
matches.

---

Nitpick comments:
In `@e2e/mocks/server.mjs`:
- Around line 319-338: The customer creation handler in the server mock mutates
the shared fixture persona returned by personaForRequest, which can leak state
across tests. Update personaForRequest and/or the /store/customers POST handling
so only dynamic personas are mutated, or return a cloned persona for
fixture-backed identities before assigning auth.persona.customer. Keep the fix
localized around personaForRequest, auth.persona.customer, and the customer
creation route.

In `@e2e/pro-checkout-integration.spec.ts`:
- Around line 52-81: The billing-step interaction in this checkout test
duplicates the existing completeBillingStep helper logic instead of reusing it.
Replace the repeated field fills and continue-click flow in the
e2e/pro-checkout-integration.spec.ts scenario with a call to completeBillingStep
from e2e/helpers/checkout.ts, and keep only the assertions that are specific to
this test, such as the payment-method verification.

In `@e2e/pro-checkout-journeys.spec.ts`:
- Around line 129-186: The two account-seeding calls in the checkout journey
tests do not verify that registration succeeded before testing the sign-in
fallback flow. Update the setup in the affected specs to assert the
`/api/auth/register` response is successful, following the same pattern used by
`registerViaApi`, so failures are caught at the seeding step rather than later
in the `account-exists-notice` or sign-in assertions.
- Around line 248-260: Centralize the checkout trigger literals used by this
test flow so the mock and spec stay in sync; the duplicated values like
fail-session+, order-pending+, fail-complete+, PURC-HASE-FLOW-7777, and
lic-e2e-purchase should be moved into shared e2e constants. Update the
references in pro-checkout-journeys.spec.ts, mocks/server.mjs, and
mocks/fixtures.json to consume the shared constants, and keep the test behavior
in the payment-session failure case wired through those symbols rather than
inline strings.

In `@src/app/`[locale]/(main)/pro/checkout/page.tsx:
- Around line 138-155: The Suspense fallback in CheckoutContent no longer
matches the current checkout layout. Update the fallback skeleton inside the
checkout page’s Suspense block to mirror the new structure from
CheckoutContent/checkout-client.tsx: a 3-step list area plus a sticky summary
column in the same [1.6fr_1fr] grid. Remove the old md:grid-cols-2 two-panel
skeleton and align the Skeleton placeholders with the new checkout sections so
the loading state matches the rendered UI.

In `@src/components/pro/checkout-client.tsx`:
- Around line 90-94: The DEFAULT_PAYMENT_METHOD fallback in checkout-client.tsx
is masking the misconfiguration case by resolving to 'btcpay' even when no
payment provider is enabled. Update the PaymentMethod selection logic near
DEFAULT_PAYMENT_METHOD so it only chooses a provider that is actually enabled,
and make the no-provider case explicit instead of defaulting to BTCPay. Then
ensure initializeCheckout uses that signal to avoid calling createPaymentSession
for an unavailable provider and let the existing PaymentStep “no payment methods
configured” handling remain the user-facing path.

In `@src/components/pro/checkout/express-checkout.tsx`:
- Around line 44-117: The `handleConfirm` flow in `ExpressCheckoutElement` can
be re-entered, so add a local in-flight guard like the `isProcessing` protection
used in the card checkout path. Track a boolean state or ref in
`ExpressCheckout` and short-circuit `handleConfirm` when a confirm is already
running, then set/reset it around the `stripe.confirmPayment` and
`completeProviderConfirmedCheckout` work so duplicate submissions are prevented.

In `@src/components/pro/checkout/step-shell.tsx`:
- Around line 63-71: The StepShell edit button uses only editLabel, so both
Account and Billing render as indistinguishable “Edit” controls. Update the
button in StepShell to include a contextual accessible name derived from the
step or section title while keeping the visible label unchanged, using the
existing onEdit/editLabel props and the step context passed into this component.
🪄 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: CHILL

Plan: Pro Plus

Run ID: 149d547f-13c4-4976-a5fb-39efbb8192f4

📥 Commits

Reviewing files that changed from the base of the PR and between ae25b46 and f4168af.

📒 Files selected for processing (17)
  • e2e/auth.spec.ts
  • e2e/helpers/checkout.ts
  • e2e/mocks/server.mjs
  • e2e/pro-checkout-integration.spec.ts
  • e2e/pro-checkout-journeys.spec.ts
  • e2e/pro-checkout.spec.ts
  • playwright.config.ts
  • src/app/[locale]/(main)/pro/checkout/page.tsx
  • src/components/pro/checkout-client.test.tsx
  • src/components/pro/checkout-client.tsx
  • src/components/pro/checkout/account-step.tsx
  • src/components/pro/checkout/billing-step.tsx
  • src/components/pro/checkout/express-checkout.tsx
  • src/components/pro/checkout/payment-step.tsx
  • src/components/pro/checkout/step-shell.tsx
  • src/middleware.test.ts
  • src/middleware.ts

Comment thread e2e/mocks/server.mjs
Comment thread src/components/pro/checkout-client.tsx
@wcpos-bot

wcpos-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Round 3 review-fix triage before code changes:

Thread Author/tool File/line Issue summary Decision Reason Commit
discussion_r3509761999 CodeRabbit e2e/mocks/server.mjs:568 /store/carts/{id}/complete is not idempotent and can mint duplicate orders on retry. Fix Valid mock data-integrity gap; short-circuit already-completed carts and return the existing order. Pending
discussion_r3509762009 CodeRabbit src/components/pro/checkout-client.tsx:414 Billing PATCH success and payment-session refresh failure share the same billing-save error path. Fix Valid checkout UX/correctness gap; payment refresh needs a distinct error while success state updates stay after both phases. Pending

Review-body outside-diff finding:

Source File/line Issue summary Decision Reason Commit
CodeRabbit review 4613665658 src/components/pro/checkout-client.tsx:109 resolvePaymentSession returns too early from cart.payment_sessions and skips cart.payment_session fallback when no provider match is found. Fix Valid legacy fallback gap for PayPal/BTCPay session resolution. Pending

Skipped threads: none among the fresh unresolved inline thread inventory.

@greptile-apps greptile-apps 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.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@wcpos-bot

wcpos-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Round 3 review-fix status after fresh unresolved-thread inventory: unresolved inline review threads remaining: 0.

Thread File Issue Decision Commit
discussion_r3509761999 server.mjs /store/carts/{id}/complete was not idempotent and could mint duplicate orders on retry. Fixed and resolved f1f3bac
discussion_r3509762009 checkout-client.tsx Payment-session refresh errors after successful billing save surfaced as billing-save failures. Fixed and resolved f1f3bac
CodeRabbit review 4613665658 checkout-client.tsx resolvePaymentSession returned too early from payment_sessions and skipped the singular payment_session fallback when no provider matched. Fixed f1f3bac

Excluded threads:

Thread Reason for skipping
None Fresh GraphQL inventory returned 0 unresolved inline review threads after fixes and resolution.

Validation:

  • pnpm install --frozen-lockfile passed.
  • pnpm exec vitest run src/components/pro/checkout-client.test.tsx passed: 25 tests.
  • pnpm lint passed with 4 existing warnings in src/services/core/business/electron-service.ts.
  • pnpm exec tsc --noEmit passed.
  • pnpm test:unit passed: 134 files / 918 tests.
  • pnpm exec playwright test e2e/pro-checkout.spec.ts --project=chromium -g "Mock checkout backend" passed: 3 tests.
  • pnpm test:e2e --project=chromium is blocked in this runner because Chromium cannot load libglib-2.0.so.0; 14 request-level tests passed before browser-launch failures, 59 browser tests failed at launch with exit code 127.

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

🧹 Nitpick comments (1)
e2e/pro-checkout.spec.ts (1)

142-188: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Idempotency test looks solid.

Retry-completion coverage against the mock backend correctly asserts stable id/display_id across duplicate complete calls. One optional addition: also assert retriedComplete doesn't create a second cart/order side effect (e.g., checking cart status or a distinct completion count) if the mock server exposes that, to catch regressions where IDs match but duplicate state is created underneath.

🤖 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 `@e2e/pro-checkout.spec.ts` around lines 142 - 188, The retry-completion test
in e2e/pro-checkout.spec.ts already verifies the same order id and display_id,
but it should also cover the absence of duplicate side effects. Update the test
around `retriedComplete` in `returns the same order when cart completion is
retried` to assert an additional observable state from the mock backend, such as
unchanged cart status or a completion counter, if available, so the `complete`
endpoint’s idempotency is validated beyond matching order fields.
🤖 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.

Nitpick comments:
In `@e2e/pro-checkout.spec.ts`:
- Around line 142-188: The retry-completion test in e2e/pro-checkout.spec.ts
already verifies the same order id and display_id, but it should also cover the
absence of duplicate side effects. Update the test around `retriedComplete` in
`returns the same order when cart completion is retried` to assert an additional
observable state from the mock backend, such as unchanged cart status or a
completion counter, if available, so the `complete` endpoint’s idempotency is
validated beyond matching order fields.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: df98e44d-16f5-4e14-91b0-4ab793ec1929

📥 Commits

Reviewing files that changed from the base of the PR and between f4168af and f1f3bac.

📒 Files selected for processing (5)
  • e2e/mocks/server.mjs
  • e2e/pro-checkout.spec.ts
  • src/components/pro/checkout-client.test.tsx
  • src/components/pro/checkout-client.tsx
  • src/components/pro/checkout/billing-step.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/components/pro/checkout/billing-step.tsx
  • e2e/mocks/server.mjs
  • src/components/pro/checkout-client.test.tsx
  • src/components/pro/checkout-client.tsx

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-m6kf0npm9-wcpos.vercel.app

@greptile-apps greptile-apps 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.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@wcpos-bot

wcpos-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review-body fix pushed in 5242aa6.

Fresh post-push unresolved inline review-thread inventory: 0.

Thread File Issue Decision Commit
CodeRabbit review 4613705750 pro-checkout.spec.ts Retry-completion regression only asserted stable order id/display_id; it needed an observable absence of duplicate side effects. Fixed: the test now attaches the cart to a dynamic persona and verifies /store/orders still reports exactly one order after retrying completion. 5242aa6

Excluded threads:

Thread Reason for skipping
None Fresh GraphQL inventory returned 0 unresolved inline review threads.

Validation:

  • pnpm install --frozen-lockfile passed.
  • pnpm exec playwright test e2e/pro-checkout.spec.ts --project=chromium -g "returns the same order when cart completion is retried" passed: 1 test.
  • pnpm lint passed with 4 existing warnings in src/services/core/business/electron-service.ts.
  • pnpm exec tsc --noEmit passed.
  • pnpm test:unit passed: 134 files / 918 tests.
  • Local pnpm test:e2e --project=chromium was attempted and failed in this runner because Chromium cannot load libglib-2.0.so.0; 14 request-level tests passed before 59 browser-launch specs failed with exit code 1.
  • GitHub CI for head 5242aa6 passed: Test, E2E Tests, CodeQL, Deploy Preview, and CodeRabbit all green.

Final PR state after polling: mergeable, mergeStateStatus: CLEAN, unresolved inline review threads: 0.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-l1mh585oi-wcpos.vercel.app

@kilbot

kilbot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Post-creation review (partial): the full 3-agent adversarial pass hit the session token limit mid-run (resumes after 04:10); I completed the highest-risk checks inline and fixed two findings in the latest commit:

  • Restored protective warning visibility: a payment_uncertain state restored from sessionStorage only rendered inside step 3 — a customer on the billing step would not see the do-not-retry warning until reaching payment. The notice now renders above the steps from first paint, pinned by a unit test.
  • Wallet double-submit guard: ExpressCheckoutRow now carries the same in-flight guard as the card form (double-confirm → potential double charge).

Also verified inline (no change needed): safety-restore ordering gates cart init correctly (a restored order_pending blocks init before any cart is created); cart email is set at creation from the authenticated customer (the dropped PATCH was redundant); /login?redirect= goes through sanitizeRedirectPath; register/login APIs have the same (lack of) rate limiting as the existing register page — no parity gap introduced, though adding rate limiting to auth APIs is a reasonable pre-launch hardening item.

After validation: 915 unit / 71/71 e2e still green. A full multi-angle review pass will run once the token window resets.

…e-submit guard

Inline review findings: a restored payment_uncertain warning rendered
only inside the payment step, so a customer on billing wouldn't see it
until step 3 — the notice now renders above the steps from first paint
(unit test pins visible-before-billing). The Express Checkout wallet
confirm path gains the same in-flight guard the card form has.

@greptile-apps greptile-apps 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.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@kilbot

kilbot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Overnight status (final for this window): branch now carries the full v2 + harness + both review rounds (mine inline + the agents bot's session-refresh/idempotency work, rebased together). Combined validation: 918 unit tests / 73 e2e passing, tsc + eslint clean.

Remaining before launch (needs credentials or a human):

  1. Run INCLUDE_INTEGRATION=1 pnpm test:e2e --grep @integration against staging with Stripe test keys (spec already updated for the new UI).
  2. PayPal sandbox manual run-through (select PayPal row → approve popup → success).
  3. One live BTCPay invoice end-to-end.
  4. Wallet test on a real device (Apple/Google Pay, Stripe test mode).
  5. Confirm production Vercel envs: NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, NEXT_PUBLIC_PAYPAL_CLIENT_ID, NEXT_PUBLIC_BTCPAY_ENABLED.
  6. A full multi-angle adversarial review pass was cut short by the session token limit — resumes automatically after the window resets. Recommend not merging before that completes.

Optional hardening flagged during review: rate limiting on /api/auth/register and /api/auth/login (pre-existing gap, more exposed now that checkout links to them).

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-499ioe6mf-wcpos.vercel.app

…aces, cart binding

Full multi-angle review of the checkout diff surfaced and fixes:

LAUNCH-BLOCKING: inline registration persisted Medusa's registration
token, whose JWT has an empty actor_id until re-auth — on real Medusa
every brand-new customer's session was dead at cart creation while the
mocked suite passed (the mock accepted registration tokens as
sessions). register() now exchanges for a session token via login (the
same refresh-after-linking invariant the OAuth flow enforces), and the
mock now mirrors real token semantics (registration tokens can only
create the customer; persona reads require a login-minted session
token) so this class of bug can never pass CI green again.

Races (two finders independently): handleBillingSubmit read the payment
collection id from React state that hasn't rendered when billing beats
the background init — minting a SECOND payment collection (customer
pays intent A, completion authorizes collection B → paid-but-no-order).
The init promise now carries the full init result. Session mutations
are serialized (billing refresh + method switch share the isProcessing
lock), provider content shows a preparing state during mutations (no
paying a superseded BTCPay invoice), billing Edit and method rows lock
while a provider confirmation may be charging (onProcessingChange from
CheckoutForm + ExpressCheckoutRow), and Stripe Elements is keyed by
client secret so a refreshed session can't leave the form confirming a
stale intent.

Hardening: cart routes now bind carts to the caller (cart.email must
match the session customer on GET/PATCH/line-items/payment-sessions/
complete); PATCH whitelists billing_address instead of spreading
arbitrary body fields into Medusa; login/register reject cross-origin
browser POSTs (login-CSRF); the account step's sign-in failure copy
points OAuth-only customers at the OAuth path instead of dead-ending
them at a password they don't have.

Tests updated to pin the new contracts: register token exchange,
cart-ownership fixtures, retry-idempotency spec now performs the
login exchange. 918 unit / 73 e2e green.

@greptile-apps greptile-apps 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.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@kilbot

kilbot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Full adversarial review complete (3 finder angles + verification, applied in 7f61b29). All validation green after fixes: 918 unit / 73 e2e, tsc + eslint clean.

The big one (would have broken launch)

Inline registration produced a dead session on real Medusa. register() persisted the registration token, whose JWT has an empty actor_id until re-authentication (the documented refresh-after-linking invariant the OAuth flow already honors). On production Medusa, every brand-new customer would have hit "Unable to initialize checkout" at the cart step — while the mocked e2e suite stayed green because the mock accepted registration tokens as sessions. Fixed by exchanging for a session token via login inside register(), and the mock now mirrors real token semantics (registration tokens may only create the customer; all persona reads require a login-minted session token) so this failure class can't silently pass CI again. This also strengthens the existing /register page flow.

Money-path races (confirmed by two independent finders)

  • Billing submitted before background init rendered its state → the session refresh saw a null payment-collection id → second payment collection minted → customer pays intent A while completion authorizes collection B → paid-but-no-order. The init promise now carries the collection id.
  • Session mutations (billing refresh, method switch) now share one lock; provider row content shows a "Preparing payment" state during mutations (no clicking a superseded BTCPay invoice); billing Edit and method switching lock while a card/wallet confirmation may be charging; Stripe Elements is keyed by client secret so a refreshed session remounts instead of confirming a stale intent.

Hardening

  • Cart ownership binding: GET/PATCH/line-items/payment-sessions/complete now verify the cart carries the session customer's email (previously any authenticated user could act on any cart id; GET had no auth at all — pre-existing, now closed).
  • PATCH whitelist: only billing_address passes through (the body was previously spread verbatim into Medusa — region/metadata/experiment attribution were writable).
  • Login-CSRF: /api/auth/login and /register reject cross-origin browser POSTs (Origin check).
  • OAuth dead-end: sign-in failures in the account step now point Google/GitHub/Discord customers at the OAuth path instead of telling them to check a password they don't have.

Verified-no-change-needed / follow-ups (non-blocking)

  • Payment-collection amount isn't refreshed on session re-create — irrelevant while the store is single-region USD with no tax variation; revisit if taxes/regions arrive.
  • The duplicate-account classification rests on Medusa's error message regex — the staging @integration run will exercise the real string; consider keying on an error code later.
  • OAuth sign-in from a localized checkout returns to the default-locale checkout (pre-existing login-page behavior).
  • Fixture-persona purchases don't attach orders in the mock (deliberate: fixtures are shared across parallel workers); the license-delivery invariant is covered via dynamically-registered accounts.

With this, the mocked matrix + safety machinery are as battle-tested as they can be without real keys. The credential-dependent items from the earlier comment remain the pre-launch checklist.

@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

🧹 Nitpick comments (1)
src/lib/medusa-auth.test.ts (1)

90-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for a Step 3 (login-exchange) failure.

The happy path is now well covered, but there's no test for login() failing after the customer is already created in Step 2 — the new edge case this three-step flow introduces. Worth a test asserting register() still throws/handles this distinctly (see companion comment on medusa-auth.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 `@src/lib/medusa-auth.test.ts` around lines 90 - 141, Add a test for the new
Step 3 login-exchange failure path in the register() flow. In
src/lib/medusa-auth.test.ts, extend the register() coverage to mock a successful
auth identity creation and customer creation, then make the third fetch call for
the login() exchange fail. Assert that register() throws or surfaces the login
failure distinctly, and verify the flow stops after the failed login attempt
rather than returning a session token.
🤖 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 `@src/app/api/store/cart/payment-sessions/route.ts`:
- Around line 66-70: The payment session route is trusting the caller’s
paymentCollectionId instead of the cart’s own payment collection. In the
payment-sessions route, update the logic around createPaymentSession to compare
the provided paymentCollectionId against currentCart.payment_collection?.id, or
derive the ID directly from currentCart before using it. Keep the existing
cart/email validation intact, and reject mismatched or stale collection IDs
before creating the session.

In `@src/app/api/store/cart/route.ts`:
- Around line 118-119: The PATCH handler in cart route should validate the
parsed request body before destructuring, since request.json() can fail or
return null and non-string cartId values can reach getCart/updateCart. Update
the PATCH flow in the route handler to mirror the safer object/type guard used
by the line-items route, then only read cartId after confirming the body is a
non-null object with a string cartId and return a client error otherwise; apply
the same validation pattern to the related cart update path referenced by the
comment.

In `@src/lib/medusa-auth.ts`:
- Around line 209-217: The registration flow in medusa-auth’s customer creation
path returns a plain failure when the Step 3 login exchange throws, leaving an
already-created customer behind. Wrap the `login(email, password)` call in the
registration flow so failures are converted into a distinct typed error or
actionable message that callers can use to route to the sign-in fallback, while
keeping the existing `AccountExistsError` handling unchanged.

---

Nitpick comments:
In `@src/lib/medusa-auth.test.ts`:
- Around line 90-141: Add a test for the new Step 3 login-exchange failure path
in the register() flow. In src/lib/medusa-auth.test.ts, extend the register()
coverage to mock a successful auth identity creation and customer creation, then
make the third fetch call for the login() exchange fail. Assert that register()
throws or surfaces the login failure distinctly, and verify the flow stops after
the failed login attempt rather than returning a session token.
🪄 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: CHILL

Plan: Pro Plus

Run ID: 2c43aa13-ac80-4a28-b411-9fc83c9a26a2

📥 Commits

Reviewing files that changed from the base of the PR and between b5d54dd and 7f61b29.

📒 Files selected for processing (19)
  • e2e/mocks/server.mjs
  • e2e/pro-checkout.spec.ts
  • src/app/api/auth/login/route.ts
  • src/app/api/auth/register/route.ts
  • src/app/api/store/cart/complete/route.test.ts
  • src/app/api/store/cart/complete/route.ts
  • src/app/api/store/cart/line-items/route.test.ts
  • src/app/api/store/cart/line-items/route.ts
  • src/app/api/store/cart/payment-sessions/route.ts
  • src/app/api/store/cart/route.ts
  • src/components/pro/checkout-client.tsx
  • src/components/pro/checkout-form.tsx
  • src/components/pro/checkout/account-step.tsx
  • src/components/pro/checkout/express-checkout.tsx
  • src/components/pro/checkout/payment-step.tsx
  • src/components/pro/stripe-provider.tsx
  • src/lib/api/same-origin.ts
  • src/lib/medusa-auth.test.ts
  • src/lib/medusa-auth.ts
✅ Files skipped from review due to trivial changes (1)
  • src/lib/api/same-origin.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/components/pro/checkout/express-checkout.tsx
  • e2e/pro-checkout.spec.ts
  • e2e/mocks/server.mjs
  • src/components/pro/checkout/payment-step.tsx

Comment thread src/app/api/store/cart/payment-sessions/route.ts
Comment thread src/app/api/store/cart/route.ts Outdated
Comment thread src/lib/medusa-auth.ts
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-l7lmnyw5v-wcpos.vercel.app

@kilbot

kilbot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

/fix triage — round 1

CI is green; 4 earlier threads already fixed+resolved by the repo bot. Remaining findings:

# Source File Class Category Decision Outcome
1 CodeRabbit payment-sessions/route.ts Logic Major (security) Fix Bind caller-supplied paymentCollectionId to the cart's own collection
2 CodeRabbit cart/route.ts Mechanical Minor Fix Validate PATCH body before destructuring (mirror line-items route)
3 CodeRabbit medusa-auth.ts Logic Minor Fix Step-3 login failure after customer creation → actionable "account created, please sign in" error
4 CodeRabbit (nit) checkout-client.tsx Logic Trivial Fix Don't init a payment session when no provider is enabled; let PaymentStep's no-methods state surface
5 CodeRabbit (nit) checkout/page.tsx Mechanical Trivial Fix Align Suspense fallback skeleton with the 3-step + summary layout
6 CodeRabbit (nit) step-shell.tsx Mechanical Trivial (a11y) Fix Distinguish the two "Edit" buttons with contextual aria-labels
7 CodeRabbit (nit) express-checkout.tsx Trivial Skip Already addressed in b5d54dd (isConfirming guard)
8 CodeRabbit (nit) mocks/server.mjs Trivial Skip Already addressed in 7f61b29 (/store/customers only accepts dynamic registration tokens; fixture tokens 401)
9 CodeRabbit (nit) pro-checkout-integration.spec.ts Mechanical Trivial Fix Reuse completeBillingStep helper instead of duplicating field fills
10 CodeRabbit (nit) pro-checkout-journeys.spec.ts Mechanical Trivial Fix Assert the account-seeding register responses
11 CodeRabbit (nit) mocks + journeys spec Mechanical Trivial Fix Centralize failure-injection/license magic strings in a shared e2e constants module

Greptile: no findings (trial credit limit). Codex: both inline findings previously fixed in f4168af.

kilbot added 5 commits July 2, 2026 14:24
Review: a caller-supplied paymentCollectionId was passed straight to
createPaymentSession — a stale or mismatched id could attach sessions
to the wrong collection. The route now prefers the cart's own
collection and rejects mismatched ids (MedusaCart type gains the v2
payment_collection field).
Review: request.json() failures / null bodies surfaced as 500s and
non-string cartId could reach getCart. Mirrors the line-items guard.
… fails

Review: if step 3 (login) fails after the customer exists, the generic
'registration failed' error invited a confusing retry into
ACCOUNT_EXISTS. The customer is now told the account was created and to
sign in.
Review nit: with no NEXT_PUBLIC payment env at all, init attempted a
session for an unconfigured provider and failed with a misleading
error; PaymentStep's explicit no-methods state now surfaces instead
(both at init and after a billing save).
…c hygiene

- Checkout Suspense fallback mirrors the 3-step + summary layout
- Step Edit buttons get contextual aria-labels (two identical 'Edit'
  controls were indistinguishable to screen readers)
- Integration spec reuses the shared completeBillingStep helper
- Journeys spec asserts its account-seeding register responses
- Failure-injection prefixes + purchase-license literals move to a
  shared e2e/mocks/constants.mjs consumed by both the mock server and
  the specs (the strings are the mock<->spec contract)

@greptile-apps greptile-apps 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.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@kilbot

kilbot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

/fix round 1 — complete

All 11 triage items closed (commits 83a7ea64bfb781):

# Finding Outcome
1 Payment-collection/cart binding ✅ Fixed 83a7ea6 — cart is source of truth; mismatched ids rejected 400
2 PATCH body validation ✅ Fixed f80056e
3 Post-registration login-exchange failure message ✅ Fixed 82c8a71
4 No-provider misconfiguration path ✅ Fixed a3164b3 (init + billing-refresh both skip; PaymentStep's no-methods state surfaces)
5 Suspense skeleton parity ✅ Fixed 4bfb781
6 Edit-button a11y names ✅ Fixed 4bfb781 (aria-label="Edit <step>")
7 Express-checkout in-flight guard ⏭️ Already in b5d54dd
8 Mock fixture-persona mutation ⏭️ Already in 7f61b29 (fixture tokens can't hit /store/customers)
9 Integration spec helper reuse ✅ Fixed 4bfb781
10 Seeding register assertions ✅ Fixed 4bfb781
11 Shared mock↔spec constants ✅ Fixed 4bfb781 (e2e/mocks/constants.mjs)

All three open CodeRabbit threads replied + resolved. Validation after fixes: 918 unit / 73 e2e passing, tsc + eslint clean. Waiting on CI for the new commits; after that the PR needs only human approval (plus the credential-dependent pre-launch checklist from earlier comments).

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-1fusynrwl-wcpos.vercel.app

@kilbot kilbot merged commit fa1b3b1 into main Jul 2, 2026
9 checks passed
@kilbot kilbot deleted the worktree-checkout-v2 branch July 2, 2026 12:35
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