Skip to content

Make payment section provider agnostic + support manual payment metho…#129

Merged
damianlegawiec merged 30 commits intomainfrom
feature/payment-provider-abstraction
Apr 27, 2026
Merged

Make payment section provider agnostic + support manual payment metho…#129
damianlegawiec merged 30 commits intomainfrom
feature/payment-provider-abstraction

Conversation

@damianlegawiec
Copy link
Copy Markdown
Member

@damianlegawiec damianlegawiec commented Apr 15, 2026

…ds (non session ones)

Fixes #64

Summary by CodeRabbit

  • New Features

    • Added Adyen and PayPal client payment UIs, improved multi-method selection, session-based flows, saved-card support, and CTA that toggles between “Pay now” and “Place order”.
  • Localization

    • New checkout/payment translations added for German, English, Spanish, French, and Polish.
  • Bug Fixes / UX

    • Clearer messaging for payment creation failures, PayPal approval/cancellation states, unsupported gateways, and zero-amount/no-payment flows.
  • Tests

    • Updated payment session test to validate external data propagation.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 15, 2026

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

Project Deployment Actions Updated (UTC)
storefront Ready Ready Preview, Comment Apr 24, 2026 9:02am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

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

Adds multi-gateway payment support: new Adyen and PayPal client components, Stripe conditional initialization, PaymentSection refactor to support session vs direct payments, server-side direct-payment action and external_data plumbing, updated tests, and new checkout translation keys.

Changes

Cohort / File(s) Summary
Translations
messages/de.json, messages/en.json, messages/es.json, messages/fr.json, messages/pl.json
Added new checkout translation keys for payment flows and PayPal statuses: selectPaymentMethod, manualPaymentInfo, failedToCreatePayment, placeOrder, noPaymentRequired, unsupportedGateway, paypalApproveFirst, paypalApproved, paypalCancelled, paypalError.
Dependencies
package.json
Added @adyen/adyen-web@^6.33.0 and @paypal/react-paypal-js@^9.1.1.
Adyen client
src/components/checkout/AdyenPaymentForm.tsx
New client component that lazy-loads @adyen/adyen-web, mounts Drop-in, exposes handle (confirmPayment, fetchUpdates), reports approval/errors, and cleans up on unmount.
PayPal client
src/components/checkout/PayPalPaymentForm.tsx
New client component that lazy-loads PayPal SDK, renders PayPalButtons, exposes handle (confirmPayment, fetchUpdates), manages approve/cancel/error states, and returns null when PayPal is not configured.
PaymentSection refactor
src/components/checkout/PaymentSection.tsx, src/components/checkout/index.ts
Major refactor: replaced forwardRef export with named function + optional ref prop; introduced PaymentCompleteResult union (session vs direct); added onSessionMethodChange; unified gateway handles (Stripe/Adyen/PayPal), external_data usage, zero-amount bypass, per-method sub-UIs, and new submission/approval flows. Re-exported PaymentCompleteResult.
Express checkout / Stripe
src/components/checkout/ExpressCheckoutButton.tsx, src/lib/utils/stripe.ts, src/app/.../cart/page.tsx
Added isStripeConfigured export; stripePromise resolves to null when key missing; ExpressCheckoutButton wraps previous impl and returns null when Stripe not configured (notifying availability); removed dynamic import loading placeholder.
Gateway utilities & config
src/lib/utils/payment-gateway.ts
New module exporting GatewayId type, resolveGatewayId(paymentMethodType), and PayPal/Adyen config flags/keys plus adyenEnvironment.
Server/data changes
src/lib/data/payment.ts, src/lib/data/express-checkout-flow.ts
createCheckoutPaymentSession signature changed to accept externalData?: Record<string, unknown>; added exported createDirectPayment(cartId, paymentMethodId); completeCheckoutPaymentSession accepts optional params (including session_result/external_data); express-checkout flow updated to pass external_data.
Tests
src/lib/data/__tests__/payment.test.ts, src/app/.../confirm-payment/__tests__/page.test.tsx
Updated tests to expect external_data object in session creation and to assert confirmPaymentAndCompleteCart receives optional sessionResult from query params.
Checkout page integration
src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx, src/app/[country]/[locale]/(checkout)/confirm-payment/[id]/page.tsx
Page updated to accept PaymentCompleteResult from PaymentSection, track isSessionPayment, conditionally call completeCheckoutPaymentSession when session payments occur, forward sessionResult from query to completion, and toggle primary CTA label between payNow and placeOrder.

Sequence Diagram

sequenceDiagram
    actor User
    participant Page as Checkout Page
    participant PaymentSection as PaymentSection
    participant GatewayForm as Gateway Form (Stripe/Adyen/PayPal)
    participant PaymentAPI as Payment API
    participant OrderAPI as Order Completion

    User->>Page: open checkout
    Page->>PaymentSection: render cart & payment methods
    User->>PaymentSection: select payment method
    PaymentSection->>PaymentSection: resolveGatewayId() / determine session-based
    alt Session-based
        PaymentSection->>PaymentAPI: createCheckoutPaymentSession(cartId, methodId, externalData)
        PaymentAPI-->>PaymentSection: { sessionId, sessionData }
        PaymentSection->>GatewayForm: init with sessionData/sessionId
        GatewayForm-->>PaymentSection: onReady (handle)
        User->>GatewayForm: submit payment
        GatewayForm->>PaymentAPI: confirm/process payment
        PaymentAPI-->>GatewayForm: confirmed
        GatewayForm-->>PaymentSection: onPaymentCompleted
        PaymentSection->>Page: onPaymentComplete({type:"session", sessionId, sessionResult?})
        Page->>OrderAPI: completeCheckoutPaymentSession(cartId, sessionId, { session_result?: sessionResult })
    else Direct/manual
        PaymentSection->>User: show manual instructions
        User->>PaymentSection: confirm order
        PaymentSection->>PaymentAPI: createDirectPayment(cartId, methodId)
        PaymentAPI-->>PaymentSection: { payment }
        PaymentSection->>Page: onPaymentComplete({type:"direct"})
        Page->>OrderAPI: complete order (no session)
    else Zero-amount
        PaymentSection->>Page: onPaymentComplete({type:"direct"})
    end
    OrderAPI-->>Page: order confirmation
    Page->>User: show confirmation
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through SDKs with nimble feet,

Adyen, PayPal, Stripe — a multi-gateway treat,
Sessions or direct paths, translations all in line,
Buttons swap, tests pass, and checkout feels fine,
A carrot-coded commit — nibble, merge, and dine!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main changes: making the payment section provider-agnostic and adding support for manual payment methods. It is specific and directly related to the changeset.
Linked Issues check ✅ Passed The PR successfully implements the requirements from issue #64: payment provider abstraction (Stripe, Adyen, PayPal), manual payment method support, and multi-gateway handling. All coding objectives are met.
Out of Scope Changes check ✅ Passed All changes are in scope for the objectives. International i18n additions, dependency updates, payment provider implementations, and form handling are all directly aligned with supporting multiple gateways and manual payments.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/payment-provider-abstraction

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/components/checkout/AdyenPaymentForm.tsx (1)

28-36: Consider using a more specific type for the drop-in ref.

While the comment explains the reasoning, using any weakens type safety. Consider using unknown or a minimal interface.

♻️ Suggested alternative
-  // Adyen SDK types use Preact internally — use a loose ref to avoid
-  // coupling to their internal component hierarchy.
-  const dropinRef = useRef<any>(null);
+  // Adyen SDK types use Preact internally — use a minimal interface
+  // to avoid coupling to their internal component hierarchy.
+  const dropinRef = useRef<{ submit: () => void; unmount: () => void } | null>(
+    null,
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/checkout/AdyenPaymentForm.tsx` around lines 28 - 36, The
dropinRef currently uses an unsafe any type; replace it with a narrower type
(e.g., useRef<unknown>(null) or define a minimal interface matching the Adyen
Dropin methods you call such as mount/destroy/submit and useRef<AdyenDropin |
null>(null)) so you retain type safety while avoiding coupling to Preact
internals; update references to dropinRef throughout the component (e.g., where
you call methods on the drop-in) to match the new type/interface and adjust
resolvePaymentRef and related handlers if necessary.
src/components/checkout/PaymentSection.tsx (1)

300-350: Consider extracting the shared initialization logic.

The initialization sequence in handleMethodSelect (lines 310-338) duplicates the logic in the mount effect (lines 229-256). This could lead to drift if one is updated without the other.

♻️ Suggested refactor

Extract the shared logic into a reusable function:

const initializeSessionMethod = useCallback(
  async (method: PaymentMethod) => {
    setLoading(true);
    let cardId: string | null = null;

    if (isAuthenticated) {
      try {
        const result = await getCreditCards();
        const gatewayCards = result.data.filter(
          (card) => card.gateway_payment_profile_id,
        );
        setSavedCards(gatewayCards);
        if (gatewayCards.length > 0) {
          const defaultCard =
            gatewayCards.find((c) => c.default) || gatewayCards[0];
          cardId = defaultCard.gateway_payment_profile_id;
          setSelectedCardId(cardId);
        }
      } catch {
        // proceed without saved cards
      }
    }

    selectedCardRef.current = cardId;
    lastTotalRef.current = cart.total;
    await createSession(cardId, method);
  },
  [isAuthenticated, cart.total, createSession],
);

Then use it in both places:

  • Mount effect: initializeSessionMethod(selectedMethod)
  • handleMethodSelect: initializeSessionMethod(newMethod)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/checkout/PaymentSection.tsx` around lines 300 - 350,
handleMethodSelect duplicates the session initialization logic found in the
mount effect; extract that shared sequence into a single reusable async function
(e.g., initializeSessionMethod) implemented with useCallback and proper deps
(isAuthenticated, cart.total, createSession) that performs loading state,
fetches and filters getCreditCards, sets saved cards and selectedCardId, updates
selectedCardRef/lastTotalRef, and calls createSession; then call
initializeSessionMethod(selectedMethod) from the mount effect and
initializeSessionMethod(newMethod) from handleMethodSelect (replace the inline
init/else branches) to avoid drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/utils/payment-gateway.ts`:
- Around line 48-50: The exported adyenEnvironment constant currently unsafely
casts NEXT_PUBLIC_ADYEN_ENVIRONMENT with as "test" | "live"; replace this with a
small validation step that reads process.env.NEXT_PUBLIC_ADYEN_ENVIRONMENT,
checks whether it equals "test" or "live" (e.g., via a helper isValidAdyenEnv or
a switch), and then export either the validated value or a safe default ("test")
or throw a clear error; update the exported symbol adyenEnvironment to use the
validated result so only "test" or "live" can be passed to the Adyen SDK.

---

Nitpick comments:
In `@src/components/checkout/AdyenPaymentForm.tsx`:
- Around line 28-36: The dropinRef currently uses an unsafe any type; replace it
with a narrower type (e.g., useRef<unknown>(null) or define a minimal interface
matching the Adyen Dropin methods you call such as mount/destroy/submit and
useRef<AdyenDropin | null>(null)) so you retain type safety while avoiding
coupling to Preact internals; update references to dropinRef throughout the
component (e.g., where you call methods on the drop-in) to match the new
type/interface and adjust resolvePaymentRef and related handlers if necessary.

In `@src/components/checkout/PaymentSection.tsx`:
- Around line 300-350: handleMethodSelect duplicates the session initialization
logic found in the mount effect; extract that shared sequence into a single
reusable async function (e.g., initializeSessionMethod) implemented with
useCallback and proper deps (isAuthenticated, cart.total, createSession) that
performs loading state, fetches and filters getCreditCards, sets saved cards and
selectedCardId, updates selectedCardRef/lastTotalRef, and calls createSession;
then call initializeSessionMethod(selectedMethod) from the mount effect and
initializeSessionMethod(newMethod) from handleMethodSelect (replace the inline
init/else branches) to avoid drift.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c5a80e80-ed4b-4208-8b37-a3c54cd6c36f

📥 Commits

Reviewing files that changed from the base of the PR and between 8f5024d and fff2408.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (16)
  • messages/de.json
  • messages/en.json
  • messages/es.json
  • messages/fr.json
  • messages/pl.json
  • package.json
  • src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx
  • src/components/checkout/AdyenPaymentForm.tsx
  • src/components/checkout/ExpressCheckoutButton.tsx
  • src/components/checkout/PaymentSection.tsx
  • src/components/checkout/index.ts
  • src/lib/data/__tests__/payment.test.ts
  • src/lib/data/express-checkout-flow.ts
  • src/lib/data/payment.ts
  • src/lib/utils/payment-gateway.ts
  • src/lib/utils/stripe.ts

Comment thread src/lib/utils/payment-gateway.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/checkout/PaymentSection.tsx (2)

368-497: ⚠️ Potential issue | 🟠 Major

Add an in-flight guard around imperative submit.

submit() ignores the existing processing prop before starting backend calls. A double click or repeated imperative call can create duplicate direct payments or concurrently complete the same session. Keep a local processingRef in sync and flip it before the first await. Based on learnings, checkout backend calls should be guarded with processing state to prevent redundant saves/calls during processing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/checkout/PaymentSection.tsx` around lines 368 - 497, The
submit() imperative handler doesn't guard against concurrent calls; add a local
ref (e.g., processingRef via useRef<boolean>(false)) kept in sync with
setProcessing and check processingRef.current at the very start of submit to
early-return an error (or { error: t("processing") }) if already true, set
processingRef.current = true immediately before any awaited work begins, and
ensure you set it back to false whenever you call setProcessing(false) or on all
catch/early-return paths (also update it when you call setProcessing(true) so
useImperativeHandle's submit, setProcessing, and processingRef remain
consistent).

246-259: ⚠️ Potential issue | 🔴 Critical

Scope saved-card readiness to Stripe only.

Saved cards are selected for every session gateway. For PayPal/Adyen, selectedCardId can be non-null while gatewayHandleRef.current is still null; then Line 435 passes and Line 460 can dereference null. Restrict saved-card loading/selection to Stripe, or make the submit guard gateway-aware.

🐛 Harden the submit branch
             const clientSecret = sessionExternalData.client_secret as
               | string
               | undefined;
+            const isStripeSavedCard =
+              resolveGatewayId(selectedMethod.type) === "stripe" &&
+              selectedCardId !== null &&
+              typeof clientSecret === "string";
 
-            if (selectedCardId && clientSecret) {
+            if (!isStripeSavedCard && !gatewayHandleRef.current) {
+              setProcessing(false);
+              return { error: t("failedToInitPayment") };
+            }
+
+            if (isStripeSavedCard) {
               // Stripe saved card flow
               const result = await confirmWithSavedCard(
                 clientSecret,
                 selectedCardId,
                 returnUrl,
               );
               error = result.error;
             } else {
               // New payment via gateway handle (Stripe PaymentElement or Adyen Drop-in)
               const result =
-                await gatewayHandleRef.current!.confirmPayment(returnUrl);
+                await gatewayHandleRef.current.confirmPayment(returnUrl);
               error = result.error;
             }

Also applies to: 328-340, 428-461

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/checkout/PaymentSection.tsx` around lines 246 - 259, The
saved-card loading/selection must be restricted to Stripe to avoid selecting a
card when gatewayHandleRef.current is null or non-Stripe; update the
effect/logic that calls getCreditCards (the block using getCreditCards,
setSavedCards and setSelectedCardId) to first verify the current gateway is
Stripe (e.g., gatewayHandleRef.current?.gateway === 'stripe' or use your app's
Stripe identifier) and only then fetch and set gatewayCards/initialCardId;
likewise, harden the submit handler (the function that reads selectedCardId and
uses gatewayHandleRef, e.g., submitPayment/onSubmit) to check
gatewayHandleRef.current exists and matches Stripe before using selectedCardId,
or else ignore saved-card flow for non-Stripe gateways—apply the same
gateway-aware guard wherever getCreditCards and selectedCardId are used (the
other saved-card loading blocks and submit branch).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/checkout/PaymentSection.tsx`:
- Around line 106-121: The parent is being notified that the method is
session-based even for zero-amount orders; derive an effective session flag
(e.g., const isSessionEffective = (selectedMethod?.session_required ?? false) &&
!isZeroAmount) and use that instead of isSessionBased in the
prevIsSessionRef/useEffect/onSessionMethodChangeRef flow so free orders are
treated as direct; update references to isSessionBased in the useEffect block
(prevIsSessionRef, onSessionMethodChangeRef.current call) to use the new
isSessionEffective value so the parent receives false for zero-amount carts.
- Around line 354-361: When switching to a direct method, first invalidate the
in-flight session guard used by createSession (e.g. increment the session
request nonce / call the same abort/invalidate function that createSession
checks) so any pending createSession resolution will bail; after advancing that
stale-request guard, then clear state with setSessionExternalData(null),
setPaymentSessionId(null), setGatewayError(null), setLoading(false) and set
gatewayHandleRef.current = null. This ensures a late-resolving createSession
cannot repopulate sessionExternalData/paymentSessionId for an unselected
gateway.

In `@src/components/checkout/PayPalPaymentForm.tsx`:
- Around line 34-45: confirmPayment currently returns a Promise that only
resolves when resolvePaymentRef is invoked by the PayPalButtons onApprove, which
leaves the checkout submit pending if the user clicks the main CTA before
approving; update confirmPayment (the useCallback) to immediately return a
resolved object with a clear error (e.g. { error: "Please approve the PayPal
payment in the PayPal dialog" }) when approvedRef.current is false instead of
creating a never-resolving promise, or alternatively programmatically trigger
the PayPal approval flow from the submit handler; modify the logic around
approvedRef, resolvePaymentRef and the promise creation inside confirmPayment so
the submit path always receives a synchronous error rather than waiting
indefinitely.
- Around line 109-136: The PayPal-facing messages in PayPalPaymentForm (the
onError handler fallback message, the onCancel message, and the approvedForUI
paragraph) are hard-coded English strings; update PayPalPaymentForm to use the
app's localization (e.g., the existing translation hook/function used elsewhere
such as t or useTranslation) to produce localized strings instead of literal
text: replace the fallback error "An error occurred with PayPal. Please try
again.", the cancel text "PayPal payment was cancelled.", and the approval text
"PayPal payment approved. Completing your order..." with calls to the
translation function (pass any dynamic parts like error.message into the
translator if needed), and ensure AlertDescription and resolvePaymentRef.current
use the localized messages.
- Around line 60-68: Move the dynamic import out of the render path into a
useEffect that runs when relevant dependencies (e.g., session or gateway props)
change, and ensure you cancel stale work on unmount: in useEffect check if
PayPalSDK already exists before importing, perform
import("@paypal/react-paypal-js").then(mod => { if (!mounted) return;
setPayPalSDK({...}); if (mounted) onReady({ confirmPayment, fetchUpdates }); }),
and in the effect cleanup set a mounted flag (or use an AbortController) to
prevent calling onReady or setPayPalSDK after unmount; this prevents duplicate
imports/onReady calls and stale gateway handles in PaymentSection.

---

Outside diff comments:
In `@src/components/checkout/PaymentSection.tsx`:
- Around line 368-497: The submit() imperative handler doesn't guard against
concurrent calls; add a local ref (e.g., processingRef via
useRef<boolean>(false)) kept in sync with setProcessing and check
processingRef.current at the very start of submit to early-return an error (or {
error: t("processing") }) if already true, set processingRef.current = true
immediately before any awaited work begins, and ensure you set it back to false
whenever you call setProcessing(false) or on all catch/early-return paths (also
update it when you call setProcessing(true) so useImperativeHandle's submit,
setProcessing, and processingRef remain consistent).
- Around line 246-259: The saved-card loading/selection must be restricted to
Stripe to avoid selecting a card when gatewayHandleRef.current is null or
non-Stripe; update the effect/logic that calls getCreditCards (the block using
getCreditCards, setSavedCards and setSelectedCardId) to first verify the current
gateway is Stripe (e.g., gatewayHandleRef.current?.gateway === 'stripe' or use
your app's Stripe identifier) and only then fetch and set
gatewayCards/initialCardId; likewise, harden the submit handler (the function
that reads selectedCardId and uses gatewayHandleRef, e.g.,
submitPayment/onSubmit) to check gatewayHandleRef.current exists and matches
Stripe before using selectedCardId, or else ignore saved-card flow for
non-Stripe gateways—apply the same gateway-aware guard wherever getCreditCards
and selectedCardId are used (the other saved-card loading blocks and submit
branch).
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 37bb01de-affd-41d2-9fe5-8b201526f8b7

📥 Commits

Reviewing files that changed from the base of the PR and between fff2408 and 9517956.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • package.json
  • src/components/checkout/PayPalPaymentForm.tsx
  • src/components/checkout/PaymentSection.tsx
  • src/lib/utils/payment-gateway.ts
✅ Files skipped from review due to trivial changes (1)
  • package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/utils/payment-gateway.ts

Comment thread src/components/checkout/PaymentSection.tsx Outdated
Comment thread src/components/checkout/PaymentSection.tsx
Comment thread src/components/checkout/PayPalPaymentForm.tsx Outdated
Comment thread src/components/checkout/PayPalPaymentForm.tsx Outdated
Comment thread src/components/checkout/PayPalPaymentForm.tsx
- PayPal: return error instead of hanging promise when not yet approved
- PayPal: move SDK loading to useEffect with cancellation
- PayPal: localize all user-facing strings (5 locales)
- Adyen: validate environment value instead of unsafe type cast
- PaymentSection: treat zero-amount orders as non-session for button text
- PaymentSection: invalidate in-flight sessions when switching to direct method

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The PayPal flow is user-initiated via the PayPal button, not via the
checkout page's "Place Order" button. After the user approves in the
PayPal popup, onApproved fires and PaymentSection directly updates the
billing address and completes the payment session + order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/components/checkout/PayPalPaymentForm.tsx (1)

25-30: Add explicit return types to component functions.

These component functions lack explicit return type annotations, which violates the coding guideline: "Use strict TypeScript type checking. Always define explicit return types for functions." PayPalPaymentFormInner should return JSX.Element, and PayPalPaymentForm should return JSX.Element | null since it conditionally returns null.

♻️ Proposed typing cleanup
-import { useCallback, useEffect, useRef, useState } from "react";
+import { type JSX, useCallback, useEffect, useRef, useState } from "react";
@@
 function PayPalPaymentFormInner({
   paypalOrderId,
   currency,
   onReady,
   onApproved,
-}: PayPalPaymentFormProps) {
+}: PayPalPaymentFormProps): JSX.Element {
@@
-export function PayPalPaymentForm(props: PayPalPaymentFormProps) {
+export function PayPalPaymentForm(
+  props: PayPalPaymentFormProps,
+): JSX.Element | null {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/checkout/PayPalPaymentForm.tsx` around lines 25 - 30, The
component functions lack explicit TypeScript return types: update
PayPalPaymentFormInner to declare its return type as JSX.Element and update
PayPalPaymentForm to declare its return type as JSX.Element | null (since it
conditionally returns null); locate the function signatures for
PayPalPaymentFormInner and PayPalPaymentForm in the file and add the respective
return type annotations to their definitions to satisfy strict typing rules.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/checkout/PaymentSection.tsx`:
- Around line 99-105: selectedMethodId is only initialized once so when
paymentMethods changes the stored id can become invalid and selectedMethod
becomes undefined; add a useEffect watching paymentMethods that checks if the
current selectedMethodId still exists in paymentMethods and if not calls
setSelectedMethodId to either the first paymentMethods[0].id or "" (or leave
current id if still present). Update the logic that derives selectedMethod
(paymentMethods.find((pm) => pm.id === selectedMethodId)) to rely on this effect
so the radio group always has a valid selected value; use the existing
identifiers selectedMethodId, setSelectedMethodId, selectedMethod and
paymentMethods to locate and implement the fix.
- Around line 372-376: Add a local in-flight guard ref (e.g., processingRef and
completionInFlightRef) and use it inside handlePayPalApproved and the submit
function to prevent re-entrancy: at the very top of handlePayPalApproved and
submit check if processingRef.current or completionInFlightRef.current is true
and return early; otherwise set them true immediately (aligned with
setProcessing(true)) and clear them on all exit paths (including before every
early error return in submit and on success/finally). Ensure the ref state is
mounted-guarded (check mounted or use a mount ref) so it stays aligned with the
processing prop and is reset appropriately when the component unmounts.
- Around line 473-505: The saved-card branch must only run for Stripe session
flows and you must never dereference gatewayHandleRef.current when it's null;
update the condition around confirmWithSavedCard to require that the
session/gateway is Stripe (e.g., check selectedMethod identifier or
sessionExternalData indicating Stripe) in addition to selectedCardId and
clientSecret so you don't take the saved-card path for PayPal/Adyen sessions,
and before calling gatewayHandleRef.current.confirmPayment ensure
gatewayHandleRef.current exists and return a handled error if it does not.
Reference symbols to change: selectedCardId, clientSecret, sessionExternalData
(or selectedMethod id/gateway), confirmWithSavedCard, and
gatewayHandleRef.current.confirmPayment.

In `@src/components/checkout/PayPalPaymentForm.tsx`:
- Around line 64-84: The dynamic import in the useEffect that loads PayPal SDK
(checking PayPalSDK, calling setPayPalSDK) does not handle promise rejection and
can leave the component hanging; update the
import("@paypal/react-paypal-js").then(...) chain to add a .catch handler that
sets an error state (e.g., setPayPalError or setPayPalLoadFailed) and/or calls
onReadyRef.current with a failure object so the parent knows the component
failed to initialize, ensure the cancelled flag is checked in the catch as well,
and keep existing cleanup logic so confirmPaymentRef and fetchUpdatesRef are not
referenced if loading failed.

---

Nitpick comments:
In `@src/components/checkout/PayPalPaymentForm.tsx`:
- Around line 25-30: The component functions lack explicit TypeScript return
types: update PayPalPaymentFormInner to declare its return type as JSX.Element
and update PayPalPaymentForm to declare its return type as JSX.Element | null
(since it conditionally returns null); locate the function signatures for
PayPalPaymentFormInner and PayPalPaymentForm in the file and add the respective
return type annotations to their definitions to satisfy strict typing rules.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a1c7c304-8389-48c2-a0c0-d1994d180a97

📥 Commits

Reviewing files that changed from the base of the PR and between 9517956 and add58bb.

📒 Files selected for processing (8)
  • messages/de.json
  • messages/en.json
  • messages/es.json
  • messages/fr.json
  • messages/pl.json
  • src/components/checkout/PayPalPaymentForm.tsx
  • src/components/checkout/PaymentSection.tsx
  • src/lib/utils/payment-gateway.ts
✅ Files skipped from review due to trivial changes (3)
  • messages/de.json
  • messages/pl.json
  • src/lib/utils/payment-gateway.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • messages/en.json
  • messages/es.json

Comment thread src/components/checkout/PaymentSection.tsx Outdated
Comment thread src/components/checkout/PaymentSection.tsx Outdated
Comment thread src/components/checkout/PaymentSection.tsx Outdated
Comment thread src/components/checkout/PayPalPaymentForm.tsx
… error

- Critical: gate saved-card confirmation to Stripe only, preventing null
  dereference crash when PayPal/Adyen is selected with authenticated user
- Major: fall back to first payment method when selectedMethodId becomes
  stale after cart.payment_methods changes
- Major: add completionInFlightRef guard to prevent re-entrant submit/
  PayPal approval from double-completing the order
- Minor: handle PayPal SDK import failure with error message instead of
  infinite spinner

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ternal_data

Spree's Adyen gateway stores the Adyen session ID in PaymentSession.external_id,
not inside external_data. The external_data only contains session_data, channel,
and return_url. Include external_id as _external_id in the stored session data
so the Adyen form can access it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adyen Web v6 requires explicit registration of payment method components
(Card, GooglePay, ApplePay, PayPal, Klarna, Bancontact, Redirect) via the
paymentMethodComponents config. Without them the Drop-in mounts but renders
nothing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ister()

paymentMethodComponents on the Dropin constructor alone wasn't sufficient.
Register components globally via AdyenCheckout.register() before creating
the checkout instance, which is the v6 recommended approach.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Next.js doesn't process dynamic import() of CSS from node_modules.
Move adyen.css to a static top-level import so it's bundled correctly.
Also add debug logging for Adyen init flow (temporary).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
src/components/checkout/PaymentSection.tsx (1)

99-107: ⚠️ Potential issue | 🟠 Major

Synchronize stale payment-method fallback with session lifecycle.

The render fallback on Lines 104-107 can switch the effective method when cart.payment_methods changes, but it does not run handleMethodSelect, recreate a session, or clear stale session state. If the previous method’s session is still in state, the UI/submit path can use old sessionExternalData for the newly effective method.

🐛 Suggested direction

Centralize method-change side effects so both user selection and paymentMethods refreshes go through the same path:

-  const selectedMethod: PaymentMethod | undefined =
-    paymentMethods.find((pm) => pm.id === selectedMethodId) ??
-    paymentMethods[0];
+  const selectedMethod: PaymentMethod | undefined = paymentMethods.find(
+    (pm) => pm.id === selectedMethodId,
+  );
   const effectiveSelectedMethodId = selectedMethod?.id ?? "";

Then add a small sync effect after handleMethodSelect is available:

+  useEffect(() => {
+    if (paymentMethods.length === 0) {
+      if (selectedMethodId !== "") setSelectedMethodId("");
+      sessionRequestIdRef.current += 1;
+      setSessionExternalData(null);
+      setPaymentSessionId(null);
+      setGatewayError(null);
+      gatewayHandleRef.current = null;
+      setLoading(false);
+      return;
+    }
+
+    if (!paymentMethods.some((pm) => pm.id === selectedMethodId)) {
+      handleMethodSelect(paymentMethods[0].id);
+    }
+  }, [paymentMethods, selectedMethodId, handleMethodSelect]);

This requires wrapping handleMethodSelect in useCallback so the effect does not fire on every render.

Also applies to: 242-299, 322-373

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/checkout/PaymentSection.tsx` around lines 99 - 107, The
fallback that computes selectedMethod and effectiveSelectedMethodId can change
when paymentMethods updates but doesn't run the selection side-effects (session
recreation / clearing sessionExternalData); update the flow so method changes
always go through a single handler: wrap handleMethodSelect in useCallback and
replace any implicit fallback assignment logic (selectedMethodId /
setSelectedMethodId) with a useEffect that watches paymentMethods and calls
handleMethodSelect(paymentMethods[0]?.id ?? "") when the previously selected id
becomes stale; ensure handleMethodSelect performs recreating the payment session
and clearing any sessionExternalData so the UI/submit path never retains stale
session state (affecting selectedMethodId, selectedMethod,
effectiveSelectedMethodId).
src/components/checkout/PayPalPaymentForm.tsx (1)

81-104: ⚠️ Potential issue | 🟠 Major

Render the PayPal load error instead of the spinner.

The catch block sets error, but Lines 99-104 return the loading spinner whenever PayPalSDK is still null, so SDK load failures remain visually indistinguishable from loading.

🐛 Proposed fix
   if (!PayPalSDK) {
+    if (error) {
+      return (
+        <Alert variant="destructive" className="mt-3">
+          <CircleAlert />
+          <AlertDescription>{error}</AlertDescription>
+        </Alert>
+      );
+    }
+
     return (
       <div className="flex items-center justify-center py-8">
         <div className="w-5 h-5 border-2 border-gray-300 border-t-gray-600 rounded-full animate-spin" />
       </div>
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/checkout/PayPalPaymentForm.tsx` around lines 81 - 104, The UI
currently always shows the loading spinner when PayPalSDK is null, hiding load
failures set via setError; update the render logic to check the error state
first (the variable updated in the catch that calls setError) and return a clear
error UI when error is truthy (e.g., show the error message or a retry hint),
otherwise keep the existing spinner when PayPalSDK is still null; locate the
relevant render branch using PayPalSDK and setError (and the error state
variable) and adjust the conditional order so error is rendered before the
spinner.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/checkout/AdyenPaymentForm.tsx`:
- Around line 39-165: The AdyenPaymentForm currently uses hardcoded user-facing
strings (e.g., "Adyen has not loaded yet", "An error occurred during payment.",
"Payment ... failed. Please try again.", "Failed to initialize payment form.")
which bypass next-intl; update the component to use checkout translation keys
instead. In AdyenPaymentForm, import/use the existing translation helper (e.g.,
useTranslations('checkout') or accept the same t function used by
PaymentSection), then replace all hardcoded messages in confirmPayment,
onPaymentCompleted, onPaymentFailed, onError, and the init catch block with
calls to the translation keys (e.g., t('adyen.notLoaded'),
t('adyen.genericError'), t('adyen.paymentFailed'), t('adyen.initFailed')) and
pass any dynamic parts (like resultCode) to the translator; ensure
resolvePaymentRef.current and setError receive the translated strings.
- Around line 36-180: The effect that initializes Adyen Drop-in should not
include the local mounted state because setMounted(true) inside init triggers
the effect cleanup which unmounts the Drop-in; remove the mounted state and all
uses of setMounted and the mounted variable, and remove mounted from the
useEffect dependency array so the effect depends only on sessionId, sessionData,
onReady, confirmPayment, and fetchUpdates; ensure dropinRef.current is still
mounted/unmounted in the cleanup and that onReady({ confirmPayment, fetchUpdates
}) is called after dropinRef.current is set.

---

Duplicate comments:
In `@src/components/checkout/PaymentSection.tsx`:
- Around line 99-107: The fallback that computes selectedMethod and
effectiveSelectedMethodId can change when paymentMethods updates but doesn't run
the selection side-effects (session recreation / clearing sessionExternalData);
update the flow so method changes always go through a single handler: wrap
handleMethodSelect in useCallback and replace any implicit fallback assignment
logic (selectedMethodId / setSelectedMethodId) with a useEffect that watches
paymentMethods and calls handleMethodSelect(paymentMethods[0]?.id ?? "") when
the previously selected id becomes stale; ensure handleMethodSelect performs
recreating the payment session and clearing any sessionExternalData so the
UI/submit path never retains stale session state (affecting selectedMethodId,
selectedMethod, effectiveSelectedMethodId).

In `@src/components/checkout/PayPalPaymentForm.tsx`:
- Around line 81-104: The UI currently always shows the loading spinner when
PayPalSDK is null, hiding load failures set via setError; update the render
logic to check the error state first (the variable updated in the catch that
calls setError) and return a clear error UI when error is truthy (e.g., show the
error message or a retry hint), otherwise keep the existing spinner when
PayPalSDK is still null; locate the relevant render branch using PayPalSDK and
setError (and the error state variable) and adjust the conditional order so
error is rendered before the spinner.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e2c88f1f-750f-44de-a5dc-e9988bb6a906

📥 Commits

Reviewing files that changed from the base of the PR and between add58bb and 6e322d8.

📒 Files selected for processing (3)
  • src/components/checkout/AdyenPaymentForm.tsx
  • src/components/checkout/PayPalPaymentForm.tsx
  • src/components/checkout/PaymentSection.tsx

Comment thread src/components/checkout/AdyenPaymentForm.tsx Outdated
Comment on lines +39 to +165
async (_returnUrl: string): Promise<{ error?: string }> => {
if (!dropinRef.current) {
return { error: "Adyen has not loaded yet" };
}

// The Drop-in handles its own submit flow. We trigger it
// programmatically and wait for onPaymentCompleted/onPaymentFailed.
return new Promise<{ error?: string }>((resolve) => {
resolvePaymentRef.current = resolve;
try {
dropinRef.current.submit();
} catch (err) {
resolve({
error:
err instanceof Error
? err.message
: "An error occurred during payment.",
});
resolvePaymentRef.current = null;
}
});
},
[],
);

const fetchUpdates = useCallback(async () => {
// Adyen sessions auto-update; no manual fetch needed
}, []);

useEffect(() => {
if (mounted) return;
if (!containerRef.current) return;

let cancelled = false;

async function init() {
try {
const adyen = await import("@adyen/adyen-web");
const {
AdyenCheckout,
Dropin,
Card,
GooglePay,
ApplePay,
PayPal,
Klarna,
Bancontact,
Redirect,
} = adyen;
await import("@adyen/adyen-web/styles/adyen.css");

if (cancelled || !containerRef.current) return;

// Register payment method components globally so Drop-in can use them
AdyenCheckout.register(
Card,
GooglePay,
ApplePay,
PayPal,
Klarna,
Bancontact,
Redirect,
);

const checkout = await AdyenCheckout({
clientKey: adyenClientKey,
environment: adyenEnvironment,
session: {
id: sessionId,
sessionData,
},
showPayButton: false,
onPaymentCompleted: (result) => {
if (
result.resultCode === "Authorised" ||
result.resultCode === "Pending" ||
result.resultCode === "Received"
) {
resolvePaymentRef.current?.({});
} else {
const msg = `Payment ${result.resultCode?.toLowerCase() || "failed"}. Please try again.`;
setError(msg);
resolvePaymentRef.current?.({ error: msg });
}
resolvePaymentRef.current = null;
},
onPaymentFailed: () => {
const msg = "An error occurred during payment.";
setError(msg);
resolvePaymentRef.current?.({ error: msg });
resolvePaymentRef.current = null;
},
onError: (err) => {
const msg = err?.message || "An error occurred during payment.";
setError(msg);
resolvePaymentRef.current?.({ error: msg });
resolvePaymentRef.current = null;
},
});

if (cancelled || !containerRef.current) return;

const dropin = new Dropin(checkout, {
openFirstPaymentMethod: true,
paymentMethodComponents: [
Card,
GooglePay,
ApplePay,
PayPal,
Klarna,
Bancontact,
Redirect,
],
});

dropin.mount(containerRef.current);
dropinRef.current = dropin;
setMounted(true);

onReady({ confirmPayment, fetchUpdates });
} catch (err) {
if (!cancelled) {
setError(
err instanceof Error
? err.message
: "Failed to initialize payment form.",
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Localize Adyen-facing error messages.

These strings are shown in checkout UI but bypass next-intl, unlike the PayPal form and the rest of PaymentSection. Please route them through checkout translation keys.

🌐 Suggested direction
+import { useTranslations } from "next-intl";
 import { useCallback, useEffect, useRef, useState } from "react";
@@
 }: AdyenPaymentFormProps) {
+  const t = useTranslations("checkout");
   const containerRef = useRef<HTMLDivElement>(null);
@@
       if (!dropinRef.current) {
-        return { error: "Adyen has not loaded yet" };
+        return { error: t("failedToInitPayment") };
       }
@@
-                : "An error occurred during payment.",
+                : t("paymentError"),
@@
-              const msg = `Payment ${result.resultCode?.toLowerCase() || "failed"}. Please try again.`;
+              const msg = t("paymentError");
@@
-            const msg = "An error occurred during payment.";
+            const msg = t("paymentError");
@@
-            const msg = err?.message || "An error occurred during payment.";
+            const msg = err?.message || t("paymentError");
@@
-              : "Failed to initialize payment form.",
+              : t("failedToInitPayment"),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/checkout/AdyenPaymentForm.tsx` around lines 39 - 165, The
AdyenPaymentForm currently uses hardcoded user-facing strings (e.g., "Adyen has
not loaded yet", "An error occurred during payment.", "Payment ... failed.
Please try again.", "Failed to initialize payment form.") which bypass
next-intl; update the component to use checkout translation keys instead. In
AdyenPaymentForm, import/use the existing translation helper (e.g.,
useTranslations('checkout') or accept the same t function used by
PaymentSection), then replace all hardcoded messages in confirmPayment,
onPaymentCompleted, onPaymentFailed, onError, and the init catch block with
calls to the translation keys (e.g., t('adyen.notLoaded'),
t('adyen.genericError'), t('adyen.paymentFailed'), t('adyen.initFailed')) and
pass any dynamic parts (like resultCode) to the translator; ensure
resolvePaymentRef.current and setError receive the translated strings.

The Adyen SDK uses Preact internally. When mounting directly into a React
ref div, Preact's render() silently fails because React owns that DOM node.
Fix: create a child div imperatively via document.createElement and append
it to the container ref, then mount the Dropin into that node. This gives
Preact full ownership of its mount target without conflicting with React.

Also remove debug console.log statements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adyen's sessions flow handles payment internally via its own Pay button.
Trying to call dropin.submit() externally caused "unknown error". Switch
to showPayButton: true and auto-complete via onApproved callback when
onPaymentCompleted fires with Authorised/Pending/Received.

Also rename handlePayPalApproved to handleGatewayApproved since it's
now shared by both PayPal and Adyen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two Pay buttons is bad UX. Adyen should work like Stripe — our "Pay Now"
button calls dropin.submit() which validates and processes via the session.
The earlier "unknown error" was a session/config issue, not a submit() issue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
For approval-driven gateways (Adyen, PayPal), confirmPayment() returns
immediately after triggering the gateway's submit — the actual result
arrives asynchronously via onApproved. The submit path now returns early
for these gateways, letting handleGatewayApproved complete the order
with the correct sessionResult.

Stripe continues completing inline since confirmPayment() is synchronous
(awaits the Stripe SDK response before returning).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Addresses page: switch from getCountries() (global) to
  getMarketCountries() so only countries available in the current
  market are shown in address forms
- getCountry: add remote cache with "hours" lifetime so repeated
  state lookups (e.g. switching countries in address form) don't
  hit the API every time

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- AddressSection: when shipping address has no country_iso, default to
  the first country from the market's country list
- Checkout page: prefetch states for the default country (from cart's
  shipping address or first market country) during loadOrder, warming
  the server cache so states render immediately without a spinner

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split the checkout page into a Server Component (page.tsx) that fetches
all initial data during SSR, and a Client Component (CheckoutPageContent)
that receives it as props.

Before: page rendered a loading skeleton, hydrated, fired 5 server action
calls from the client, then showed the checkout UI.

After: Server Component fetches cart, market, countries, addresses, and
auth status in parallel during the initial request. The client component
initializes with real data — no loading skeleton, no waterfall.

loadOrder() is kept as a refresh function for coupon changes, express
checkout completion, etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Only call getAddresses() when authenticated (guests don't have saved
  addresses, and the API throws "Not authenticated")
- Initialize prevOrderKeyRef to null so the sidebar useEffect fires on
  first render with SSR data (was skipping because ref and cartKey
  matched on init)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
getLocaleOptions() calls cookies() which rejects during Next.js static
prerendering. Wrap in try/catch and fall back to config defaults.
This fixes both the cookies() rejection and the "use cache" timeout
in cachedListMarkets (which received options derived from cookies).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract CheckoutSidebar to CheckoutSidebar.tsx (was inline in
  CheckoutPageContent.tsx)
- Use useLayoutEffect instead of useEffect for sidebar context so it
  renders before first paint (no empty sidebar flash with SSR data)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When initialData is provided from the server component, we already know
isAuthenticated — no need to wait for AuthContext to finish its own
client-side getCustomer() call. This eliminates the loading skeleton
flash after hydration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
With server-side data fetching, the checkout UI arrives fully rendered
via streaming. The Suspense fallback skeleton is never visible to users.
Keep Suspense for useSearchParams() requirement but with empty fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The sidebar renders via CheckoutContext which populates after hydration.
During streaming, the sidebar column was empty. Now CheckoutSummary
shows a skeleton (line items + totals) until real content arrives.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The sidebar was in the layout (renders before page data) and populated
via CheckoutContext after hydration — causing an empty sidebar during
SSR streaming.

Now the sidebar (Summary + CouponCode) renders inline in the checkout
page content, which has the cart data from the server component. Both
form and sidebar stream together in one chunk.

- Layout: simplified to just header + two-column grid + footer, no
  CheckoutContext/CheckoutProvider
- CheckoutPageContent: renders both form column and sidebar column,
  plus mobile summary toggle
- order-placed: removed CheckoutContext dependency (was just clearing it)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tions

- Sidebar: use PENDING sentinel so skeleton only shows before first
  setSummaryContent call, not when content is explicitly cleared (null)
- Sidebar skeleton: show 1 product placeholder instead of 2
- getCart: wrap clearCartCookies in try/catch for Server Component
  compatibility (can't set cookies outside Server Actions)
- Tests: update confirm-payment assertions to match 5-param signature

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@damianlegawiec damianlegawiec merged commit 9d7718d into main Apr 27, 2026
6 checks passed
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.

Support multiple payment gateways and manual payments on checkout

1 participant