Skip to content

Feat: add checkout stripe payments - #22

Merged
damianlegawiec merged 6 commits into
mainfrom
feature/checkout-stripe-payments
Feb 17, 2026
Merged

Feat: add checkout stripe payments#22
damianlegawiec merged 6 commits into
mainfrom
feature/checkout-stripe-payments

Conversation

@damianlegawiec

@damianlegawiec damianlegawiec commented Feb 16, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Stripe payments with saved-card and new-card flows, hosted Elements form, and post-payment redirect to an order confirmation page.
    • New order confirmation page with detailed order summary, contact and address info.
  • Improvements

    • Cart now refreshes on navigation; add/update/remove actions return updated cart immediately.
    • Enhanced credit-card UI with icons, labels, and clearer payment/error states.
  • Chores

    • Added Stripe env example key and Stripe/payment UI dependencies; core SDK versions updated.

@coderabbitai

coderabbitai Bot commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

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 Stripe payment integration (client & server), refactors checkout flow into billing-update + payment-complete steps, adds an Order Placed page, new payment/session APIs, cart cookie constant and utilities, credit-card helpers, dependency updates, env example change, and broad UI styling/token updates.

Changes

Cohort / File(s) Summary
Configuration & Dependencies
\.env.example, package.json
Added NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY to .env.example. Bumped @spree/next and @spree/sdk to ^0.2.3. Added @stripe/react-stripe-js, @stripe/stripe-js, and react-svg-credit-card-payment-icons.
Checkout Pages & Flow
src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx, src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx
Refactored checkout page to split billing-address update and payment completion; changed post-completion redirect to order-placed/{orderId}; added OrderPlaced page to finalize/display completed orders and handle URL payment_intent flows.
Payment UI & Integration
src/components/checkout/PaymentStep.tsx, src/components/checkout/StripePaymentForm.tsx, src/components/checkout/index.ts
Introduced StripePaymentForm and saved-card flows; PaymentStep reworked to use onUpdateBillingAddress/onPaymentComplete/setProcessing/isAuthenticated; integrates payment sessions, Stripe Elements, and saved-card confirmation. Exported StripePaymentForm.
Server Payment APIs
src/lib/data/payment.ts
New server-side functions: createCheckoutPaymentSession, completeCheckoutPaymentSession, and completeCheckoutOrder (action-wrapped Spree API calls).
Cart & Checkout Data
src/lib/data/checkout.ts, src/lib/constants.ts, src/lib/data/cart.ts
Added clearCartCookie() and exported CART_TOKEN_KEY = "_spree_cart_token". Optimized add/update/remove cart item functions to return mutation results directly. selectShippingRate now returns { order }.
Cart Context
src/contexts/CartContext.tsx
CartContext now re-fetches when the pathname changes (navigation events) to keep cart in sync after navigation.
Utilities
src/lib/utils/address.ts, src/lib/utils/credit-card.ts
Address helpers now accept nullable inputs. Added credit-card utilities getCardIconType and getCardLabel.
Account: Credit Cards
src/app/.../account/credit-cards/page.tsx
Switched to SDK StoreCreditCard type, updated card rendering to use PaymentIcon/getCardLabel, adjusted delete/loading flows and UI tweaks.
Tests
src/lib/data/__tests__/cart.test.ts
Updated tests to reflect cart mutation functions returning the updated cart directly (removed follow-up getCart expectations).
Global Styling & Theme
src/app/globals.css, src/app/layout.tsx
Added primary color palette CSS variables, updated font variable usage, changed background; simplified font imports and added CartDrawer to layout.
Wide UI Token/Styling Changes
src/app/**, src/components/** (many files)
Large set of presentational changes switching indigo token classes to primary tokens and many rounded-lg → rounded-xl updates across pages/components (account, cart, checkout, product, layout, etc.).

Sequence Diagram

sequenceDiagram
    participant User
    participant PaymentStep
    participant StripeForm as StripePaymentForm
    participant Backend as ServerAPI
    participant Stripe as StripeService
    participant OrderPlaced

    User->>PaymentStep: submit billing address
    PaymentStep->>Backend: onUpdateBillingAddress(orderId, address)
    Backend-->>PaymentStep: success / failure

    alt billing update success
        PaymentStep->>Backend: createCheckoutPaymentSession(orderId, paymentMethodId?)
        Backend->>Stripe: request session
        Stripe-->>Backend: clientSecret, sessionId
        Backend-->>PaymentStep: { clientSecret, paymentSessionId }

        PaymentStep->>StripeForm: init(clientSecret)
        User->>StripeForm: enter card or choose saved card
        User->>PaymentStep: click Pay Now

        alt saved card
            PaymentStep->>Backend: confirmWithSavedCard(clientSecret, paymentMethodId)
            Backend-->>PaymentStep: confirmationResult
        else new card
            PaymentStep->>StripeForm: confirmPayment(returnUrl)
            StripeForm->>Stripe: stripe.confirmPayment(...)
            Stripe-->>StripeForm: result
            StripeForm-->>PaymentStep: result
        end

        alt payment confirmed
            PaymentStep->>Backend: completeCheckoutPaymentSession(orderId, paymentSessionId)
            Backend->>Backend: completeCheckoutOrder(orderId)
            Backend-->>PaymentStep: success
            PaymentStep->>OrderPlaced: redirect to order-placed/{orderId}
        else payment failed
            PaymentStep->>User: show error
        end
    else billing update failed
        PaymentStep->>User: show validation errors
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

"🐇
I hopped through fields of code tonight,
Spun sessions bright and confirmations light,
Keys added, orders placed with care —
A little hop for a checkout fair,
Carrots, cards, and a ribbon of delight."

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.20% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat: add checkout stripe payments' clearly and concisely summarizes the main change—adding Stripe payment integration to the checkout flow. It aligns well with the substantial changes across payment processing, checkout components, and configuration.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/checkout-stripe-payments

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.

In `@src/app/`[country]/[locale]/(checkout)/checkout/[id]/page.tsx:
- Around line 338-375: handlePaymentComplete currently treats a null return from
getCheckoutOrder as success and proceeds to redirect; explicitly guard against
updatedOrder being null by treating it as an error case: after calling
getCheckoutOrder(order.id) check if updatedOrder is falsy and, if so, call
setError with a clear message (e.g., "Order not found after payment"), call
setProcessing(false), and return before attempting to call completeCheckoutOrder
or router.push; keep existing flows for updatedOrder.state !== "complete"
(calling completeCheckoutOrder and handling its .success) and preserve the
try/catch behavior around completeCheckoutPaymentSession, completeCheckoutOrder,
and router.push.
- Around line 318-336: handleUpdateBillingAddress currently calls
updateOrderAddresses without catching exceptions; wrap the await
updateOrderAddresses(...) call in a try/catch inside handleUpdateBillingAddress
so any thrown errors are caught, call setError with a specific message (e.g.,
"Failed to save billing address: " + err.message or the error string) and return
false on catch, and keep the existing updateResult.success handling for
non-throwing failures; reference handleUpdateBillingAddress and
updateOrderAddresses so you modify that function to prevent exceptions from
bubbling to PaymentStep.handleSubmit.

In `@src/app/`[country]/[locale]/(checkout)/order-placed/[id]/page.tsx:
- Around line 142-146: Replace the plain <img> in page.tsx with Next.js' Image
component: import Image from 'next/image' at the top, and swap the <img
src={item.thumbnail_url} alt={item.name} className="w-full h-full object-cover"
/> for an <Image> using src={item.thumbnail_url} and alt={item.name} and
appropriate sizing (either width/height props or the fill prop with parent
positioned container and className="object-cover"); also ensure any external
host serving item.thumbnail_url is added to next.config.js' images.domains so
the image can be optimized.

In `@src/components/checkout/PaymentStep.tsx`:
- Around line 88-119: The useEffect watching stripePaymentMethod is being
retriggered due to unstable object identity from order.payment_methods?.find;
change the logic to depend on a stable scalar instead (e.g.,
stripePaymentMethod?.id) or memoize stripePaymentMethod with useMemo where you
compute it (the order.payment_methods?.find(...) call), so the effect that
creates the session (useEffect that checks clientSecret, setLoadingSession,
createCheckoutPaymentSession, setClientSecret, setPaymentSessionId,
setStripeError) only runs when the actual selected payment method ID changes
rather than on every order object reset.

In `@src/components/checkout/StripePaymentForm.tsx`:
- Around line 12-14: Remove the non-null assertion and add a guard that checks
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY before calling loadStripe: read
the value into a local const, if it's falsy throw or log a clear runtime error
(e.g., "Missing NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY") then call loadStripe(key)
to initialize stripePromise; reference stripePromise, loadStripe and
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY when making the change.

In `@src/contexts/CartContext.tsx`:
- Around line 118-123: The current useEffect with dependencies [refreshCart,
pathname] triggers refreshCart() on every route change (useEffect in
CartContext.tsx), causing excessive getCartAction() calls; narrow the re-fetch
by either (A) keeping the effect but adding a conditional that only calls
refreshCart() when pathname matches checkout-related routes (e.g., check
pathname startsWith '/checkout' or matches a checkout regex) or when navigating
away from checkout by tracking previousPath via a useRef and comparing
previousPath to pathname, or (B) remove the pathname-driven effect and instead
invoke refreshCart() explicitly from the checkout completion flow (where
checkout success is handled) so only post-checkout triggers the cart refresh;
update the effect or checkout handlers accordingly and keep refreshCart and
getCartAction references intact.
🧹 Nitpick comments (4)
🤖 Fix all nitpicks with AI agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.

In `@src/app/`[country]/[locale]/(checkout)/checkout/[id]/page.tsx:
- Around line 318-336: handleUpdateBillingAddress currently calls
updateOrderAddresses without catching exceptions; wrap the await
updateOrderAddresses(...) call in a try/catch inside handleUpdateBillingAddress
so any thrown errors are caught, call setError with a specific message (e.g.,
"Failed to save billing address: " + err.message or the error string) and return
false on catch, and keep the existing updateResult.success handling for
non-throwing failures; reference handleUpdateBillingAddress and
updateOrderAddresses so you modify that function to prevent exceptions from
bubbling to PaymentStep.handleSubmit.

In `@src/components/checkout/PaymentStep.tsx`:
- Around line 88-119: The useEffect watching stripePaymentMethod is being
retriggered due to unstable object identity from order.payment_methods?.find;
change the logic to depend on a stable scalar instead (e.g.,
stripePaymentMethod?.id) or memoize stripePaymentMethod with useMemo where you
compute it (the order.payment_methods?.find(...) call), so the effect that
creates the session (useEffect that checks clientSecret, setLoadingSession,
createCheckoutPaymentSession, setClientSecret, setPaymentSessionId,
setStripeError) only runs when the actual selected payment method ID changes
rather than on every order object reset.

In `@src/components/checkout/StripePaymentForm.tsx`:
- Around line 12-14: Remove the non-null assertion and add a guard that checks
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY before calling loadStripe: read
the value into a local const, if it's falsy throw or log a clear runtime error
(e.g., "Missing NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY") then call loadStripe(key)
to initialize stripePromise; reference stripePromise, loadStripe and
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY when making the change.

In `@src/contexts/CartContext.tsx`:
- Around line 118-123: The current useEffect with dependencies [refreshCart,
pathname] triggers refreshCart() on every route change (useEffect in
CartContext.tsx), causing excessive getCartAction() calls; narrow the re-fetch
by either (A) keeping the effect but adding a conditional that only calls
refreshCart() when pathname matches checkout-related routes (e.g., check
pathname startsWith '/checkout' or matches a checkout regex) or when navigating
away from checkout by tracking previousPath via a useRef and comparing
previousPath to pathname, or (B) remove the pathname-driven effect and instead
invoke refreshCart() explicitly from the checkout completion flow (where
checkout success is handled) so only post-checkout triggers the cart refresh;
update the effect or checkout handlers accordingly and keep refreshCart and
getCartAction references intact.
src/contexts/CartContext.tsx (1)

118-123: Cart re-fetch on every navigation may be excessive.

This fires getCartAction() on every route change, not just post-checkout. On a typical storefront with frequent browsing, this adds an API call per navigation. Consider scoping the re-fetch more narrowly — e.g., only when navigating to/from checkout-related paths, or by having the checkout completion flow explicitly call refreshCart() instead of relying on a pathname side-effect.

🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@src/contexts/CartContext.tsx` around lines 118 - 123, The current useEffect
with dependencies [refreshCart, pathname] triggers refreshCart() on every route
change (useEffect in CartContext.tsx), causing excessive getCartAction() calls;
narrow the re-fetch by either (A) keeping the effect but adding a conditional
that only calls refreshCart() when pathname matches checkout-related routes
(e.g., check pathname startsWith '/checkout' or matches a checkout regex) or
when navigating away from checkout by tracking previousPath via a useRef and
comparing previousPath to pathname, or (B) remove the pathname-driven effect and
instead invoke refreshCart() explicitly from the checkout completion flow (where
checkout success is handled) so only post-checkout triggers the cart refresh;
update the effect or checkout handlers accordingly and keep refreshCart and
getCartAction references intact.
src/components/checkout/StripePaymentForm.tsx (1)

12-14: Non-null assertion on environment variable could produce a confusing runtime error.

If NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not configured, loadStripe will silently receive undefined, leading to hard-to-debug failures. Consider adding a guard.

Suggested guard
+const stripePublishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
+
+if (!stripePublishableKey) {
+  console.error("Missing NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY environment variable");
+}
+
-const stripePromise = loadStripe(
-  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
-);
+const stripePromise = stripePublishableKey
+  ? loadStripe(stripePublishableKey)
+  : Promise.resolve(null);
🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@src/components/checkout/StripePaymentForm.tsx` around lines 12 - 14, Remove
the non-null assertion and add a guard that checks
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY before calling loadStripe: read
the value into a local const, if it's falsy throw or log a clear runtime error
(e.g., "Missing NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY") then call loadStripe(key)
to initialize stripePromise; reference stripePromise, loadStripe and
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY when making the change.
src/components/checkout/PaymentStep.tsx (1)

88-119: stripePaymentMethod in the dependency array is an unstable object reference.

order.payment_methods?.find(...) on line 74–76 produces a new object reference whenever order is re-set (e.g., after coupon apply/remove). While the clientSecret guard on line 90 prevents duplicate session creation in most cases, after a total change resets clientSecret to null (lines 79–85), the effect will correctly re-create the session. However, if order is updated without a total change (e.g., an address update), this effect re-evaluates with a new stripePaymentMethod reference, but clientSecret is still set, so it exits early — no bug, just unnecessary effect scheduling.

Consider memoizing stripePaymentMethod or using stripePaymentMethod?.id in the dependency array for clearer intent:

Suggested optimization
-  }, [stripePaymentMethod, order.id, clientSecret]);
+  }, [stripePaymentMethod?.id, order.id, clientSecret]);
🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@src/components/checkout/PaymentStep.tsx` around lines 88 - 119, The useEffect
watching stripePaymentMethod is being retriggered due to unstable object
identity from order.payment_methods?.find; change the logic to depend on a
stable scalar instead (e.g., stripePaymentMethod?.id) or memoize
stripePaymentMethod with useMemo where you compute it (the
order.payment_methods?.find(...) call), so the effect that creates the session
(useEffect that checks clientSecret, setLoadingSession,
createCheckoutPaymentSession, setClientSecret, setPaymentSessionId,
setStripeError) only runs when the actual selected payment method ID changes
rather than on every order object reset.
src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx (1)

318-336: Missing error handling for thrown exceptions in handleUpdateBillingAddress.

If updateOrderAddresses throws (e.g., network error), the exception propagates unhandled to PaymentStep.handleSubmit. While handleSubmit has a catch block, the error message there is generic ("An error occurred during payment"), which may confuse users since the actual failure was an address update. Consider wrapping with try/catch for a more specific error message, or this is acceptable if the generic message is intentional.

🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@src/app/`[country]/[locale]/(checkout)/checkout/[id]/page.tsx around lines
318 - 336, handleUpdateBillingAddress currently calls updateOrderAddresses
without catching exceptions; wrap the await updateOrderAddresses(...) call in a
try/catch inside handleUpdateBillingAddress so any thrown errors are caught,
call setError with a specific message (e.g., "Failed to save billing address: "
+ err.message or the error string) and return false on catch, and keep the
existing updateResult.success handling for non-throwing failures; reference
handleUpdateBillingAddress and updateOrderAddresses so you modify that function
to prevent exceptions from bubbling to PaymentStep.handleSubmit.

Comment thread src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx
Comment thread src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

62-66: useEffect calling onReady re-fires whenever confirmPayment identity changes.

Since confirmPayment is recreated when stripe or elements change, onReady can be invoked multiple times. This works today because the consumer stores the handle in a ref (overwrite is harmless), but it's a subtle contract. Consider documenting this expectation or using a ref internally to call onReady only once after Stripe loads.

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

In `@src/components/checkout/StripePaymentForm.tsx` around lines 62 - 66, The
useEffect currently calls onReady whenever stripe or the derived confirmPayment
identity changes, causing multiple invocations; change it to call onReady only
once after Stripe has first loaded by introducing an internal ref (e.g.,
readyCalledRef) that tracks whether onReady has been invoked, then in the effect
check if (stripe && !readyCalledRef.current) { readyCalledRef.current = true;
onReady({ confirmPayment }); } and keep dependencies minimal (stripe and
confirmPayment) so the handle is passed once; reference the useEffect,
confirmPayment, onReady, stripe, and elements symbols when making this change.
src/components/checkout/PaymentStep.tsx (2)

141-176: Initialization useEffect fetches data and creates a payment session on mount.

This works but is a pattern the coding guidelines discourage for data fetching. The saved cards could be fetched server-side and passed as a prop to avoid the client-side fetch. That said, since the payment session creation is inherently client-initiated and depends on the Stripe payment method from the order, keeping it here is pragmatic for now.

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

In `@src/components/checkout/PaymentStep.tsx` around lines 141 - 176, The effect
currently fetches saved cards client-side (getCreditCards) and then creates a
Stripe session (createSession); update it to prefer server-provided saved cards
passed in as a prop (e.g., savedCardsProp) instead of doing the client fetch
inside the useEffect: in the useEffect (initRef, stripePaymentMethod) use
savedCardsProp to call setSavedCards and determine initialCardId (use
gateway_payment_profile_id and default logic to call setSelectedCardId) and only
fall back to calling getCreditCards if savedCardsProp is missing; keep the
client-only createSession(initialCardId) behavior and retain initRef and the
dependencies (stripePaymentMethod, isAuthenticated, createSession) so the
session creation remains client-initiated.

155-164: TypeScript won't narrow gateway_payment_profile_id through .filter().

After filtering for truthy gateway_payment_profile_id, the type isn't narrowed, so defaultCard.gateway_payment_profile_id may still be typed as string | null | undefined. Consider using a type guard or non-null assertion after the filter to be explicit.

Suggested narrowing
-        const stripeCards = result.data.filter(
-          (card) => card.gateway_payment_profile_id,
-        );
+        const stripeCards = result.data.filter(
+          (card): card is StoreCreditCard & { gateway_payment_profile_id: string } =>
+            !!card.gateway_payment_profile_id,
+        );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/checkout/PaymentStep.tsx` around lines 155 - 164, The filtered
array stripeCards (from result.data.filter) doesn't narrow
gateway_payment_profile_id so defaultCard.gateway_payment_profile_id can still
be nullable; update the filter to use a type guard that asserts the field is
non-null (e.g., filter((c): c is { gateway_payment_profile_id: string; ... } =>
Boolean(c.gateway_payment_profile_id))) or otherwise narrow/mapping result.data
to a typed structure before calling setSavedCards, then safely read
gateway_payment_profile_id from defaultCard (or use a non-null assertion) when
assigning initialCardId and calling setSelectedCardId to ensure the value is
string only.
src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx (1)

318-336: handleUpdateBillingAddress lacks try/catch — exceptions propagate to the caller.

If updateOrderAddresses throws (e.g., network error), the exception will bubble up to PaymentStep's handleSubmit catch block, which sets a generic Stripe error message. This means a billing address failure would show "An error occurred during payment" rather than a billing-specific message. Consider wrapping in try/catch for a more accurate error message, or accept the current behavior since the catch in PaymentStep does handle it.

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

In `@src/app/`[country]/[locale]/(checkout)/checkout/[id]/page.tsx around lines
318 - 336, The handleUpdateBillingAddress function calls updateOrderAddresses
without a try/catch, so thrown exceptions bubble up to PaymentStep.handleSubmit
and produce a generic Stripe error; wrap the await updateOrderAddresses(...)
call in a try/catch inside handleUpdateBillingAddress, catch any error, call
setError with a billing-specific message (include the caught error message for
context), and return false on catch so the caller gets a clear billing-address
failure instead of the generic payment error.
src/lib/data/payment.ts (1)

10-24: Consider adding explicit return types for stronger type safety.

The return types of all three functions are inferred, and the session/order objects coming from the SDK are untyped. Adding explicit return types (or at least typing the SDK responses) would improve type safety and make the API contract clearer for consumers. As per coding guidelines, explicit return types should be defined and any should be avoided.

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

In `@src/lib/data/payment.ts` around lines 10 - 24, The function
createCheckoutPaymentSession (and the other two related functions in this file)
currently rely on inferred/untyped SDK responses; add explicit return types and
typed SDK response types instead of any: import or declare a PaymentSession (and
Order) interface matching the SDK shape, type the local variables (e.g., session
returned from createPaymentSession) with that interface, and update the function
signature to return a precise Promise type (e.g., Promise<...> rather than
inferred) and, if actionResult is generic, pass the response type into
actionResult so the result wrapper is typed; reference
createCheckoutPaymentSession, createPaymentSession, actionResult and
stripePaymentMethodId when making these changes.
🤖 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/PaymentStep.tsx`:
- Around line 473-477: The submit button's disabled prop in PaymentStep
currently allows the button when stripePaymentMethod is null because the
condition uses (!clientSecret && !!stripePaymentMethod); update the logic so the
button is disabled whenever there is no stripePaymentMethod or no clientSecret
or no paymentSessionId (matching the early bail in handleSubmit). Locate the
disabled expression in the PaymentStep component and change it to include a
check for !stripePaymentMethod (and optionally !paymentSessionId) so disabled
becomes true when any required payment artifact is missing, preventing clicks
that would immediately bail in handleSubmit.

In `@src/components/checkout/StripePaymentForm.tsx`:
- Around line 116-123: The block handling stripe.confirmCardPayment in
StripePaymentForm.tsx is misformatted; reformat the await call and the
subsequent error branch to match project style by ensuring consistent spacing
and indentation around the await expression
(stripe.confirmCardPayment(clientSecret, { payment_method: paymentMethodId,
return_url: returnUrl })) and the if (result.error) return { error:
result.error.message || "An error occurred during payment." }; statement (no
stray blank lines, proper semicolons if used elsewhere, and consistent
brace/newline placement) so the CI formatter no longer flags lines referencing
result, clientSecret, paymentMethodId, and returnUrl.
- Around line 12-14: The code currently uses a non-null assertion when calling
loadStripe with NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY which yields a confusing
runtime error if the env var is missing; update the initialization of
stripePromise to first validate process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
and if it is missing throw a clear error (e.g.,
"NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not set") or return null and surface that
to the StripePaymentForm component so it can render a helpful message; refer to
the stripePromise constant, loadStripe call, and the
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY env var to locate and change the code.

---

Duplicate comments:
In `@src/app/`[country]/[locale]/(checkout)/checkout/[id]/page.tsx:
- Around line 357-367: getCheckoutOrder can return null and the current code
silently skips completion; change the guard so a null updatedOrder is treated as
"not complete": replace the check with if (!updatedOrder || updatedOrder.state
!== "complete") and then call completeCheckoutOrder(order.id) as currently done;
on null or failed completion, setError with a descriptive message (include that
getCheckoutOrder returned null), setProcessing(false) and return so we do not
redirect to the placed page for an incomplete order; reference getCheckoutOrder,
updatedOrder, completeCheckoutOrder, setError, setProcessing.

---

Nitpick comments:
In `@src/app/`[country]/[locale]/(checkout)/checkout/[id]/page.tsx:
- Around line 318-336: The handleUpdateBillingAddress function calls
updateOrderAddresses without a try/catch, so thrown exceptions bubble up to
PaymentStep.handleSubmit and produce a generic Stripe error; wrap the await
updateOrderAddresses(...) call in a try/catch inside handleUpdateBillingAddress,
catch any error, call setError with a billing-specific message (include the
caught error message for context), and return false on catch so the caller gets
a clear billing-address failure instead of the generic payment error.

In `@src/components/checkout/PaymentStep.tsx`:
- Around line 141-176: The effect currently fetches saved cards client-side
(getCreditCards) and then creates a Stripe session (createSession); update it to
prefer server-provided saved cards passed in as a prop (e.g., savedCardsProp)
instead of doing the client fetch inside the useEffect: in the useEffect
(initRef, stripePaymentMethod) use savedCardsProp to call setSavedCards and
determine initialCardId (use gateway_payment_profile_id and default logic to
call setSelectedCardId) and only fall back to calling getCreditCards if
savedCardsProp is missing; keep the client-only createSession(initialCardId)
behavior and retain initRef and the dependencies (stripePaymentMethod,
isAuthenticated, createSession) so the session creation remains
client-initiated.
- Around line 155-164: The filtered array stripeCards (from result.data.filter)
doesn't narrow gateway_payment_profile_id so
defaultCard.gateway_payment_profile_id can still be nullable; update the filter
to use a type guard that asserts the field is non-null (e.g., filter((c): c is {
gateway_payment_profile_id: string; ... } =>
Boolean(c.gateway_payment_profile_id))) or otherwise narrow/mapping result.data
to a typed structure before calling setSavedCards, then safely read
gateway_payment_profile_id from defaultCard (or use a non-null assertion) when
assigning initialCardId and calling setSelectedCardId to ensure the value is
string only.

In `@src/components/checkout/StripePaymentForm.tsx`:
- Around line 62-66: The useEffect currently calls onReady whenever stripe or
the derived confirmPayment identity changes, causing multiple invocations;
change it to call onReady only once after Stripe has first loaded by introducing
an internal ref (e.g., readyCalledRef) that tracks whether onReady has been
invoked, then in the effect check if (stripe && !readyCalledRef.current) {
readyCalledRef.current = true; onReady({ confirmPayment }); } and keep
dependencies minimal (stripe and confirmPayment) so the handle is passed once;
reference the useEffect, confirmPayment, onReady, stripe, and elements symbols
when making this change.

In `@src/lib/data/payment.ts`:
- Around line 10-24: The function createCheckoutPaymentSession (and the other
two related functions in this file) currently rely on inferred/untyped SDK
responses; add explicit return types and typed SDK response types instead of
any: import or declare a PaymentSession (and Order) interface matching the SDK
shape, type the local variables (e.g., session returned from
createPaymentSession) with that interface, and update the function signature to
return a precise Promise type (e.g., Promise<...> rather than inferred) and, if
actionResult is generic, pass the response type into actionResult so the result
wrapper is typed; reference createCheckoutPaymentSession, createPaymentSession,
actionResult and stripePaymentMethodId when making these changes.

Comment on lines +12 to +14
const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Non-null assertion on env var will produce a confusing runtime error if unset.

If NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not configured, loadStripe receives undefined (cast via !), leading to a cryptic Stripe SDK error rather than a clear message.

Suggested guard
-const stripePromise = loadStripe(
-  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
-);
+const stripePublishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
+if (!stripePublishableKey) {
+  console.warn("NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not set. Stripe payments will not work.");
+}
+const stripePromise = stripePublishableKey ? loadStripe(stripePublishableKey) : Promise.resolve(null);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
);
const stripePublishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
if (!stripePublishableKey) {
console.warn("NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not set. Stripe payments will not work.");
}
const stripePromise = stripePublishableKey ? loadStripe(stripePublishableKey) : Promise.resolve(null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/checkout/StripePaymentForm.tsx` around lines 12 - 14, The code
currently uses a non-null assertion when calling loadStripe with
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY which yields a confusing runtime error if the
env var is missing; update the initialization of stripePromise to first validate
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY and if it is missing throw a
clear error (e.g., "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not set") or return
null and surface that to the StripePaymentForm component so it can render a
helpful message; refer to the stripePromise constant, loadStripe call, and the
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY env var to locate and change the code.

Comment thread src/components/checkout/StripePaymentForm.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx (1)

19-24: ⚠️ Potential issue | 🟡 Minor

setDeleting(false) is skipped if onDelete throws.

If deleteCreditCard (called via onDelete) rejects, the await on line 22 throws, and line 23 is never reached — the button stays stuck on "Removing...".

Suggested fix
   const handleDelete = async () => {
     if (!confirm("Are you sure you want to remove this card?")) return;
     setDeleting(true);
-    await onDelete();
-    setDeleting(false);
+    try {
+      await onDelete();
+    } finally {
+      setDeleting(false);
+    }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[country]/[locale]/(storefront)/account/credit-cards/page.tsx around
lines 19 - 24, handleDelete currently sets deleting true then awaits onDelete
but never resets it if onDelete throws; wrap the await in try/finally (or
try/catch/finally) so setDeleting(false) always runs. Update the handleDelete
function to call setDeleting(true) before the try, await onDelete() inside the
try, and call setDeleting(false) in the finally block; optionally handle or
rethrow errors in the catch to preserve existing behavior while ensuring the
"Removing..." state is cleared.
🧹 Nitpick comments (3)
src/components/checkout/PaymentStep.tsx (2)

223-224: Fragile URL construction via regex replacement.

window.location.pathname.replace(/\/checkout\/.*/, ...) assumes a specific URL structure. If the checkout route pattern changes, this silently produces a wrong redirect URL.

Consider constructing the return URL from known route segments or a shared route helper instead.

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

In `@src/components/checkout/PaymentStep.tsx` around lines 223 - 224, The current
returnUrl is built using a fragile regex replace on window.location.pathname
(see returnUrl and the replace(/\/checkout\/.*/, ...) call in PaymentStep.tsx),
which can break if routes change; instead construct the URL from known route
segments or a shared route helper: derive the base origin via
window.location.origin and combine it with a deterministic path (e.g.,
`/order-placed/${order.id}`) or use your app's route builder/utility to produce
the checkout success path, replacing the regex-based logic in the returnUrl
assignment with a call to that route helper or an explicit concatenation of
known segments.

108-121: createSession silently swallows a result.success && !secret scenario without surfacing the session object.

When result.success is true and result.session exists but external_data?.client_secret is missing (line 110–112), the user sees a generic error. Consider logging the session response in development to aid debugging, since this would indicate an unexpected API response shape.

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

In `@src/components/checkout/PaymentStep.tsx` around lines 108 - 121, The handler
for createSession currently treats a missing client_secret as a generic user
error and discards the successful session object; update the branch where
result.success && result.session but client_secret is missing to log or capture
the full result.session (only in non-production/dev) for debugging and still set
a helpful user-facing error via setStripeError. Specifically, inside the block
checking secret (the result.session.external_data?.client_secret path), add a
development-only console.log or call to your logging utility that records
result.session and any external_data, and keep calling setStripeError("Failed to
initialize payment. Please try again.") while preserving existing state
functions setClientSecret and setPaymentSessionId behavior. Ensure logging is
gated by NODE_ENV !== 'production' (or your appEnv) to avoid leaking sensitive
session data.
src/lib/utils/credit-card.ts (1)

3-42: Consider consolidating the duplicate key mappings into a single data structure.

CC_TYPE_MAP (lines 3–14) and the getCardLabel switch (lines 21–42) enumerate the exact same set of card-type keys. Adding a new type requires updating both in lockstep.

A single lookup table could drive both functions:

♻️ Suggested refactor
-const CC_TYPE_MAP: Record<string, PaymentType> = {
-  visa: "Visa",
-  mastercard: "Mastercard",
-  master: "Mastercard",
-  american_express: "AmericanExpress",
-  amex: "AmericanExpress",
-  discover: "Discover",
-  jcb: "JCB",
-  diners_club: "DinersClub",
-  maestro: "Maestro",
-  unionpay: "UnionPay",
-};
-
-export function getCardIconType(ccType: string): PaymentType {
-  return CC_TYPE_MAP[ccType.toLowerCase()] ?? "Generic";
-}
-
-export function getCardLabel(ccType: string): string {
-  switch (ccType.toLowerCase()) {
-    case "visa":
-      return "Visa";
-    case "mastercard":
-    case "master":
-      return "Mastercard";
-    case "american_express":
-    case "amex":
-      return "Amex";
-    case "discover":
-      return "Discover";
-    case "jcb":
-      return "JCB";
-    case "diners_club":
-      return "Diners Club";
-    case "maestro":
-      return "Maestro";
-    case "unionpay":
-      return "UnionPay";
-    default:
-      return ccType || "Card";
-  }
-}
+const CARD_TYPES: Record<string, { icon: PaymentType; label: string }> = {
+  visa:             { icon: "Visa",            label: "Visa" },
+  mastercard:       { icon: "Mastercard",      label: "Mastercard" },
+  master:           { icon: "Mastercard",      label: "Mastercard" },
+  american_express: { icon: "AmericanExpress", label: "Amex" },
+  amex:             { icon: "AmericanExpress", label: "Amex" },
+  discover:         { icon: "Discover",        label: "Discover" },
+  jcb:              { icon: "JCB",             label: "JCB" },
+  diners_club:      { icon: "DinersClub",      label: "Diners Club" },
+  maestro:          { icon: "Maestro",         label: "Maestro" },
+  unionpay:         { icon: "UnionPay",        label: "UnionPay" },
+};
+
+export function getCardIconType(ccType: string): PaymentType {
+  return CARD_TYPES[ccType.toLowerCase()]?.icon ?? "Generic";
+}
+
+export function getCardLabel(ccType: string): string {
+  return CARD_TYPES[ccType.toLowerCase()]?.label ?? ccType || "Card";
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/credit-card.ts` around lines 3 - 42, The CC_TYPE_MAP and
getCardLabel switch duplicate the same card-type keys; replace them with a
single shared map (e.g., CARD_TYPE_INFO) that maps normalized keys (visa,
mastercard, master, american_express, amex, etc.) to an object containing both
the PaymentType icon value and the display label, then update getCardIconType to
return CARD_TYPE_INFO[ccType.toLowerCase()]?.icon ?? "Generic" and update
getCardLabel to return CARD_TYPE_INFO[ccType.toLowerCase()]?.label ?? (ccType ||
"Card"); keep aliases (master → Mastercard, amex → AmericanExpress) in the map
so adding a new type only requires one change.
🤖 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/app/`[country]/[locale]/(storefront)/account/credit-cards/page.tsx:
- Around line 3-8: The imports at the top of page.tsx are not sorted and failing
CI; reorder the import statements to satisfy your project's
organizeImports/formatter rules (e.g., group external packages then local
modules, alphabetize within groups). Specifically, reorder the lines importing
StoreCreditCard, PaymentIcon, useEffect/useState, CreditCardIcon/LockIcon,
deleteCreditCard/getCreditCards, and getCardIconType/getCardLabel so they follow
the canonical import ordering for the project and then run your
formatter/organize-imports; the unique symbols to locate are StoreCreditCard,
PaymentIcon, useEffect, useState, CreditCardIcon, LockIcon, deleteCreditCard,
getCreditCards, getCardIconType, and getCardLabel.

In `@src/components/checkout/PaymentStep.tsx`:
- Around line 194-198: handleCardSelect triggers createSession(cardId) without
awaiting/cancelling, allowing race conditions that can leave
clientSecret/paymentSessionId from a stale response; update handleCardSelect to
(a) set a local "requestedCardId" or use the existing selectedCardId and await
createSession(cardId) (or return early when loading) and disable the radio
inputs while loading, or (b) add a response guard inside createSession that
compares the response's cardId to the latest selectedCardId before calling
setClientSecret/setPaymentSessionId; reference functions/vars: handleCardSelect,
createSession, selectedCardId, setSelectedCardId, loading, clientSecret,
paymentSessionId.
- Around line 3-27: The CI failure is caused by unsorted imports in
PaymentStep.tsx; run your project's formatting/organizing tool (e.g., eslint
--fix, tsserver organizeImports, or the repo's Prettier/organizeImports command)
to reorder the import statements so they satisfy organizeImports. Specifically,
update the import block containing AddressParams/StoreState, PaymentIcon, React
hooks (useCallback/useEffect/...), CreditCardIcon, getCreditCards,
createCheckoutPaymentSession, the address helpers
(AddressFormData/addressesMatch/addressToFormData/formDataToAddress),
credit-card utils (getCardIconType/getCardLabel), AddressFormFields, and
StripePaymentForm/StripePaymentFormHandle/confirmWithSavedCard so they are
sorted per the repo rules, then commit the changed file.

---

Outside diff comments:
In `@src/app/`[country]/[locale]/(storefront)/account/credit-cards/page.tsx:
- Around line 19-24: handleDelete currently sets deleting true then awaits
onDelete but never resets it if onDelete throws; wrap the await in try/finally
(or try/catch/finally) so setDeleting(false) always runs. Update the
handleDelete function to call setDeleting(true) before the try, await onDelete()
inside the try, and call setDeleting(false) in the finally block; optionally
handle or rethrow errors in the catch to preserve existing behavior while
ensuring the "Removing..." state is cleared.

---

Duplicate comments:
In `@src/components/checkout/PaymentStep.tsx`:
- Around line 456-462: The submit button's disabled condition only checks
(!clientSecret && !!stripePaymentMethod) and still allows submission when
required prerequisites like paymentSessionId or stripeHandleRef are missing;
update the disabled logic to mirror the early-return checks in handleSubmit (the
checks around paymentSessionId, stripePaymentMethod, selectedCardId, and
stripeHandleRef.current) so the button is disabled unless all of: clientSecret
is present, paymentSessionId exists (when needed), and either a selectedCardId
exists or stripeHandleRef.current is set for new-card flows; reference the
button rendering (disabled prop), handleSubmit, clientSecret,
stripePaymentMethod, paymentSessionId, selectedCardId, and stripeHandleRef to
implement the guard, and consider converting the Stripe “ready” flag to state if
you need fully reactive UI updates.

---

Nitpick comments:
In `@src/components/checkout/PaymentStep.tsx`:
- Around line 223-224: The current returnUrl is built using a fragile regex
replace on window.location.pathname (see returnUrl and the
replace(/\/checkout\/.*/, ...) call in PaymentStep.tsx), which can break if
routes change; instead construct the URL from known route segments or a shared
route helper: derive the base origin via window.location.origin and combine it
with a deterministic path (e.g., `/order-placed/${order.id}`) or use your app's
route builder/utility to produce the checkout success path, replacing the
regex-based logic in the returnUrl assignment with a call to that route helper
or an explicit concatenation of known segments.
- Around line 108-121: The handler for createSession currently treats a missing
client_secret as a generic user error and discards the successful session
object; update the branch where result.success && result.session but
client_secret is missing to log or capture the full result.session (only in
non-production/dev) for debugging and still set a helpful user-facing error via
setStripeError. Specifically, inside the block checking secret (the
result.session.external_data?.client_secret path), add a development-only
console.log or call to your logging utility that records result.session and any
external_data, and keep calling setStripeError("Failed to initialize payment.
Please try again.") while preserving existing state functions setClientSecret
and setPaymentSessionId behavior. Ensure logging is gated by NODE_ENV !==
'production' (or your appEnv) to avoid leaking sensitive session data.

In `@src/lib/utils/credit-card.ts`:
- Around line 3-42: The CC_TYPE_MAP and getCardLabel switch duplicate the same
card-type keys; replace them with a single shared map (e.g., CARD_TYPE_INFO)
that maps normalized keys (visa, mastercard, master, american_express, amex,
etc.) to an object containing both the PaymentType icon value and the display
label, then update getCardIconType to return
CARD_TYPE_INFO[ccType.toLowerCase()]?.icon ?? "Generic" and update getCardLabel
to return CARD_TYPE_INFO[ccType.toLowerCase()]?.label ?? (ccType || "Card");
keep aliases (master → Mastercard, amex → AmericanExpress) in the map so adding
a new type only requires one change.

Comment thread src/components/checkout/PaymentStep.tsx
Comment thread src/components/checkout/PaymentStep.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

62-66: onReady in the dependency array may cause unnecessary re-invocations if the parent doesn't memoize it.

The useEffect will re-fire whenever onReady changes identity. Currently PaymentStep wraps handleGatewayReady in useCallback([], []) so this is stable, but the contract is fragile — any future caller that passes an unstable callback will trigger repeated onReady calls.

Consider either documenting that onReady must be stable, or guarding against re-invocation internally (e.g., only call onReady once via a ref).

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

In `@src/components/checkout/StripePaymentForm.tsx` around lines 62 - 66, The
useEffect in StripePaymentForm currently depends on onReady and will re-invoke
whenever its identity changes; to avoid fragile contracts with callers, guard
the invocation so onReady({ confirmPayment }) is only called once: add a ref
like calledOnReadyRef checked inside the useEffect and set it after first call,
keep the useEffect dependencies as [stripe, confirmPayment, onReady] (or drop
onReady if you prefer) and reference the ref to prevent repeated calls; mention
PaymentStep/handleGatewayReady as the expected stable caller but implement this
internal once-only guard in useEffect to make the component robust to unstable
parent callbacks.
src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx (2)

46-79: Duplicate getCheckoutOrder call when payment_intent is present.

When handling a Stripe redirect, getCheckoutOrder(orderId) is called at line 52 and again at line 62. The second call could reuse the result of the first (or the completed order) to save a network round-trip.

Suggested optimization
     async function loadAndComplete() {
       try {
+        let orderData: StoreOrder | null = null;
+
         // Handle Stripe redirect return (3DS, etc.)
         const paymentIntent = searchParams.get("payment_intent");
         if (paymentIntent) {
-          const orderData = await getCheckoutOrder(orderId);
+          orderData = await getCheckoutOrder(orderId);
           if (orderData && orderData.state !== "complete") {
             await completeCheckoutOrder(orderId);
+            // Re-fetch to get the completed state
+            orderData = await getCheckoutOrder(orderId);
           }
         }

-        // Load the order
-        const orderData = await getCheckoutOrder(orderId);
+        // Load the order if not already fetched
+        if (!orderData) {
+          orderData = await getCheckoutOrder(orderId);
+        }
+
         if (!cancelled) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[country]/[locale]/(checkout)/order-placed/[id]/page.tsx around
lines 46 - 79, The code calls getCheckoutOrder(orderId) twice in loadAndComplete
when a payment_intent is present; change the flow to call getCheckoutOrder once,
store its result in a local orderData variable, and reuse it: if paymentIntent
is present, await getCheckoutOrder(orderId) into orderData, if orderData exists
and orderData.state !== "complete" call completeCheckoutOrder(orderId) and then
update orderData by using the response from completeCheckoutOrder if it returns
the updated order, or only call getCheckoutOrder again once more to refresh
state if completeCheckoutOrder returns no order; finally use that single
orderData to setOrder/setError and set loading flags (preserve loadedRef and
cancelled checks).

86-86: searchParams in the dependency array may cause unnecessary effect re-registrations.

In Next.js App Router, useSearchParams() returns a new object on each render. The loadedRef guard prevents re-fetching, but the effect cleanup/setup cycle still runs. Consider extracting searchParams.get("payment_intent") into a variable and using that as the dependency instead, or simply use [] since loadedRef already prevents re-execution.

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

In `@src/app/`[country]/[locale]/(checkout)/order-placed/[id]/page.tsx at line 86,
The effect currently depends on searchParams which is a new object each render;
compute const paymentIntent = searchParams.get("payment_intent") above the
useEffect and replace searchParams in the dependency array with paymentIntent
(so use [orderId, paymentIntent]) or, if you prefer relying solely on the
loadedRef guard, change the dependency array to [] for the effect that calls
your fetch logic; update references inside the effect to use paymentIntent and
keep loadedRef, orderId and the effect body unchanged.
src/components/checkout/PaymentStep.tsx (1)

224-224: returnUrl construction via regex is fragile.

pathname.replace(/\/checkout\/.*/, ...) assumes a specific URL structure. If the pathname doesn't contain /checkout/, the replace is a no-op, producing an incorrect return URL. Consider using extractBasePath (already imported elsewhere in the project) or basePath for a more robust construction.

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

In `@src/components/checkout/PaymentStep.tsx` at line 224, The returnUrl
construction in PaymentStep.tsx is fragile because
pathname.replace(/\/checkout\/.*/, ...) can be a no-op; replace that logic by
deriving the base path via the project's extractBasePath (or existing basePath)
helper and then build returnUrl as
`${window.location.origin}${basePath}/order-placed/${order.id}` (or, if
extractBasePath returns empty, fall back to just `/order-placed/${order.id}`) so
the URL is correct even when the path doesn't contain /checkout/; update the
code that sets returnUrl to call extractBasePath(window.location.pathname) or
use basePath and append `/order-placed/${order.id}`.
🤖 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/PaymentStep.tsx`:
- Around line 459-461: The submit button can be enabled while Stripe Elements
hasn't finished initializing because we only check
processing/loading/sessionPaymentMethod/clientSecret; update the disabled logic
to also require the gateway handle to be ready: add a gatewayReady boolean
(e.g., state like gatewayReady) that you set true when gatewayHandleRef.current
is populated (or when the Elements onReady callback fires) and false when
tearing down, then include gatewayReady (or gatewayHandleRef.current != null) in
the disabled expression for the submit button in PaymentStep; also ensure
handleSubmit still guards against missing gateway handle but prefer disabling
the button when isAddingNew and gateway isn't ready so the user cannot click
prematurely.

---

Duplicate comments:
In `@src/components/checkout/PaymentStep.tsx`:
- Around line 194-198: handleCardSelect currently fires createSession regardless
of in-flight requests, causing race conditions on rapid card switches; add a
minimal guard to prevent concurrent sessions and disable the radio inputs while
loading. Specifically, in handleCardSelect check the loading flag and return
early if loading is true (prevent calling setSelectedCardId/createSession), and
pass loading to your radio inputs (or RadioGroup) to set the disabled prop so
the UI cannot be clicked while a session is being created; keep createSession
resolution logic unchanged but ensure loading is set/unset around the async call
so the inputs re-enable when complete.

In `@src/components/checkout/StripePaymentForm.tsx`:
- Around line 12-14: The code uses a non-null assertion on
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY when calling loadStripe (stripePromise),
which can pass undefined and yield an opaque Stripe SDK error; update the
initialization to validate process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY first
(in the module or a helper) and throw or log a clear error if it's missing, then
call loadStripe with the validated value (or return a safe fallback) so that
stripePromise/loadStripe always receives a defined publishable key and failures
are surfaced with a descriptive message.

---

Nitpick comments:
In `@src/app/`[country]/[locale]/(checkout)/order-placed/[id]/page.tsx:
- Around line 46-79: The code calls getCheckoutOrder(orderId) twice in
loadAndComplete when a payment_intent is present; change the flow to call
getCheckoutOrder once, store its result in a local orderData variable, and reuse
it: if paymentIntent is present, await getCheckoutOrder(orderId) into orderData,
if orderData exists and orderData.state !== "complete" call
completeCheckoutOrder(orderId) and then update orderData by using the response
from completeCheckoutOrder if it returns the updated order, or only call
getCheckoutOrder again once more to refresh state if completeCheckoutOrder
returns no order; finally use that single orderData to setOrder/setError and set
loading flags (preserve loadedRef and cancelled checks).
- Line 86: The effect currently depends on searchParams which is a new object
each render; compute const paymentIntent = searchParams.get("payment_intent")
above the useEffect and replace searchParams in the dependency array with
paymentIntent (so use [orderId, paymentIntent]) or, if you prefer relying solely
on the loadedRef guard, change the dependency array to [] for the effect that
calls your fetch logic; update references inside the effect to use paymentIntent
and keep loadedRef, orderId and the effect body unchanged.

In `@src/components/checkout/PaymentStep.tsx`:
- Line 224: The returnUrl construction in PaymentStep.tsx is fragile because
pathname.replace(/\/checkout\/.*/, ...) can be a no-op; replace that logic by
deriving the base path via the project's extractBasePath (or existing basePath)
helper and then build returnUrl as
`${window.location.origin}${basePath}/order-placed/${order.id}` (or, if
extractBasePath returns empty, fall back to just `/order-placed/${order.id}`) so
the URL is correct even when the path doesn't contain /checkout/; update the
code that sets returnUrl to call extractBasePath(window.location.pathname) or
use basePath and append `/order-placed/${order.id}`.

In `@src/components/checkout/StripePaymentForm.tsx`:
- Around line 62-66: The useEffect in StripePaymentForm currently depends on
onReady and will re-invoke whenever its identity changes; to avoid fragile
contracts with callers, guard the invocation so onReady({ confirmPayment }) is
only called once: add a ref like calledOnReadyRef checked inside the useEffect
and set it after first call, keep the useEffect dependencies as [stripe,
confirmPayment, onReady] (or drop onReady if you prefer) and reference the ref
to prevent repeated calls; mention PaymentStep/handleGatewayReady as the
expected stable caller but implement this internal once-only guard in useEffect
to make the component robust to unstable parent callbacks.

Comment on lines +459 to +461
disabled={
processing || loading || !sessionPaymentMethod || !clientSecret
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Submit button can be clicked before Stripe Elements is ready when adding a new card.

When isAddingNew is true and clientSecret is set, the button is enabled. But if gatewayHandleRef.current hasn't been populated yet (Stripe Elements still loading), handleSubmit silently bails at line 204. Consider also disabling the button until the gateway handle is ready, e.g. by tracking it in state.

Sketch
+ const [gatewayReady, setGatewayReady] = useState(false);

  const handleGatewayReady = useCallback((handle: StripePaymentFormHandle) => {
    gatewayHandleRef.current = handle;
+   setGatewayReady(true);
  }, []);

  // Reset when clientSecret changes (new session)
+ useEffect(() => { setGatewayReady(false); }, [clientSecret]);

  ...
  disabled={
    processing || loading || !sessionPaymentMethod || !clientSecret
+   || (isAddingNew && !gatewayReady)
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
disabled={
processing || loading || !sessionPaymentMethod || !clientSecret
}
const [gatewayReady, setGatewayReady] = useState(false);
const handleGatewayReady = useCallback((handle: StripePaymentFormHandle) => {
gatewayHandleRef.current = handle;
setGatewayReady(true);
}, []);
useEffect(() => {
setGatewayReady(false);
}, [clientSecret]);
// ... other code ...
disabled={
processing || loading || !sessionPaymentMethod || !clientSecret || (isAddingNew && !gatewayReady)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/checkout/PaymentStep.tsx` around lines 459 - 461, The submit
button can be enabled while Stripe Elements hasn't finished initializing because
we only check processing/loading/sessionPaymentMethod/clientSecret; update the
disabled logic to also require the gateway handle to be ready: add a
gatewayReady boolean (e.g., state like gatewayReady) that you set true when
gatewayHandleRef.current is populated (or when the Elements onReady callback
fires) and false when tearing down, then include gatewayReady (or
gatewayHandleRef.current != null) in the disabled expression for the submit
button in PaymentStep; also ensure handleSubmit still guards against missing
gateway handle but prefer disabling the button when isAddingNew and gateway
isn't ready so the user cannot click prematurely.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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/app/[country]/[locale]/(storefront)/account/orders/page.tsx (1)

20-32: ⚠️ Potential issue | 🟡 Minor

Remove unused getStatusColor function (lines 20–32).

This function is dead code and never called. Only getPaymentStatusColor and getShipmentStatusColor are used in the template.

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

In `@src/app/`[country]/[locale]/(storefront)/account/orders/page.tsx around lines
20 - 32, Remove the unused getStatusColor function: locate the function named
getStatusColor and delete its entire declaration (the switch returning various
tailwind class strings) since only getPaymentStatusColor and
getShipmentStatusColor are used in the template; after removal, run a quick
project build or search to ensure there are no remaining references to
getStatusColor and that no imports or types depend on it.
🧹 Nitpick comments (8)
src/app/[country]/[locale]/(storefront)/account/orders/page.tsx (2)

162-162: replace("_", " ") only replaces the first underscore.

If a payment state like "balance_due" is the only multi-underscore case this is fine, but for robustness use replaceAll("_", " ") or a regex with the global flag.

Proposed fix
-                        {order.payment_state?.replace("_", " ") || "N/A"}
+                        {order.payment_state?.replaceAll("_", " ") || "N/A"}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[country]/[locale]/(storefront)/account/orders/page.tsx at line 162,
The code uses order.payment_state?.replace("_", " ") which only replaces the
first underscore; update the transformation to replace all underscores (e.g.,
use replaceAll("_", " ") or replace(/_/g, " ")) so multi-underscore states like
"balance_due_now" are fully converted; locate the usage of
order.payment_state?.replace("_", " ") in the page.tsx render and replace it
with a global-replace variant while keeping the fallback "N/A".

64-78: Consider converting to a Server Component to eliminate client-side data fetching.

This page uses useEffect solely to fetch orders on mount, then filters them. Since there are no event handlers, interactive state, or browser-only APIs driving the fetch, this entire page could be a Server Component that awaits getOrders() directly and renders the result — with a sibling loading.tsx for the skeleton UI. This would also allow adding a generateMetadata export for SEO.

As per coding guidelines: "Avoid using useEffect for data fetching triggered by state changes; use event handlers or Server Actions instead" and "Use Server Components by default; only add 'use client' when needing event handlers, hooks."

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

In `@src/app/`[country]/[locale]/(storefront)/account/orders/page.tsx around lines
64 - 78, The OrdersPage component should be converted from a client component to
a Server Component: remove any "use client" directive and the
useEffect/loadOrders logic, and instead call await getOrders({ per_page: 50 })
directly inside the OrdersPage function, filter the returned data for o.state
=== "complete" and render the orders synchronously; add a sibling loading.tsx
for the skeleton UI and add a generateMetadata export to provide page
metadata/SEO. Ensure references to the existing getOrders, OrdersPage, and the
current filtering logic are preserved when moving to server-side rendering.
src/components/checkout/AddressSelector.tsx (1)

36-53: Pre-existing: useEffect used to derive state from props.

This useEffect computes selectedAddressId from savedAddresses and currentAddress, which is exactly the pattern the coding guidelines ask to avoid. Additionally, the eslint-disable on line 53 suppresses a legitimate warning — currentAddress is read inside the effect but excluded from the dependency array, so the matching logic goes stale when only the address fields change.

Consider computing the initial match with useMemo during render instead:

♻️ Suggested approach
-  const [selectedAddressId, setSelectedAddressId] = useState<string | "new">(
-    "new",
-  );
-
-  // Check if current address matches a saved address
-  useEffect(() => {
-    if (savedAddresses.length === 0) return;
-
-    const matchingAddress = savedAddresses.find(
-      (addr) =>
-        addr.address1 === currentAddress.address1 &&
-        addr.city === currentAddress.city &&
-        addr.zipcode === currentAddress.zipcode &&
-        addr.country_iso === currentAddress.country_iso,
-    );
-
-    if (matchingAddress) {
-      setSelectedAddressId(matchingAddress.id);
-    } else if (currentAddress.address1) {
-      // If there's an address but it doesn't match saved ones, show as new
-      setSelectedAddressId("new");
-    }
-  }, [savedAddresses]); // eslint-disable-line react-hooks/exhaustive-deps
+  const initialAddressId = useMemo<string | "new">(() => {
+    const match = savedAddresses.find(
+      (addr) =>
+        addr.address1 === currentAddress.address1 &&
+        addr.city === currentAddress.city &&
+        addr.zipcode === currentAddress.zipcode &&
+        addr.country_iso === currentAddress.country_iso,
+    );
+    return match ? match.id : "new";
+  }, [savedAddresses, currentAddress]);
+
+  const [selectedAddressId, setSelectedAddressId] = useState<string | "new">(initialAddressId);

As per coding guidelines, "Avoid using useEffect to reset state when props change; use component key prop to reset state or compute initial state from props".

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

In `@src/components/checkout/AddressSelector.tsx` around lines 36 - 53, The effect
is incorrectly deriving component state from props and suppresses the missing
dependency (currentAddress); replace the useEffect logic with a derived value
computed during render (e.g., useMemo) so selected address is computed from
savedAddresses and currentAddress instead of being set in useEffect; remove the
eslint-disable and the effect block, compute const selectedAddressId =
useMemo(() => { /* find matchingAddress by comparing address1, city, zipcode,
country_iso and return matchingAddress?.id || (currentAddress.address1 ? "new" :
undefined) */ }, [savedAddresses, currentAddress]) and update the component to
use this derived selectedAddressId (or lift control to parent if mutable state
is required) so matching stays correct when currentAddress changes.
src/app/globals.css (1)

14-25: Primary palette has a noticeable hue/saturation jump between 400 and 500.

The 50–400 shades are Tailwind's default blue values (e.g., #60a5fa for 400), while 500–950 are custom shades derived from #0077FF. This creates a visible discontinuity — primary-400 is a lighter sky-blue that doesn't smoothly transition into the deeper primary-500. Components using primary-400 or lighter shades (e.g., hover states, backgrounds) may look off-brand.

Consider generating the full 50–950 range from the #0077FF base using a tool like UIColors or Tailwind's color generator for a harmonious palette.

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

In `@src/app/globals.css` around lines 14 - 25, The primary color scale currently
mixes Tailwind's default blues (50–400) with custom shades (500–950) causing a
visible jump; regenerate the entire --color-primary-50 through
--color-primary-950 scale from the single base color `#0077FF` (using a generator
like UIColors or Tailwind color generator) and replace the mismatched 50–400
values so all --color-primary-50..--color-primary-950 are derived consistently
from `#0077FF` to ensure smooth hue/saturation transitions.
src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx (1)

509-515: Simplify identical conditional branches in step indicator.

Both index < currentStepIndex and index === currentStepIndex produce the same class "bg-primary-600 text-white". These can be collapsed.

Suggested simplification
                   className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
-                    index < currentStepIndex
-                      ? "bg-primary-600 text-white"
-                      : index === currentStepIndex
-                        ? "bg-primary-600 text-white"
-                        : "bg-gray-200 text-gray-500"
+                    index <= currentStepIndex
+                      ? "bg-primary-600 text-white"
+                      : "bg-gray-200 text-gray-500"
                   }`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[country]/[locale]/(checkout)/checkout/[id]/page.tsx around lines
509 - 515, The conditional in the step indicator JSX can be simplified because
both index < currentStepIndex and index === currentStepIndex yield the same
classes; update the className expression (the JSX that references index and
currentStepIndex) to use a single branch like index <= currentStepIndex ?
"bg-primary-600 text-white" : "bg-gray-200 text-gray-500" so you remove the
redundant nested ternary and keep the same visual behavior.
src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx (2)

42-86: Double getCheckoutOrder call when payment_intent is present.

When a payment_intent query param exists, getCheckoutOrder is called on line 52 and again on line 62. If the order was already complete (line 53 check), the second fetch is redundant — the result from line 52 could be reused.

Proposed optimization
     async function loadAndComplete() {
       try {
         // Handle Stripe redirect return (3DS, etc.)
         const paymentIntent = searchParams.get("payment_intent");
+        let orderData: StoreOrder | null = null;
+
         if (paymentIntent) {
-          // Find the payment session and complete it
-          const orderData = await getCheckoutOrder(orderId);
-          if (orderData && orderData.state !== "complete") {
+          orderData = await getCheckoutOrder(orderId);
+          if (orderData && orderData.state !== "complete") {
             await completeCheckoutOrder(orderId);
+            // Re-fetch to get updated order after completion
+            orderData = await getCheckoutOrder(orderId);
           }
         }

-        // Load the order
-        const orderData = await getCheckoutOrder(orderId);
+        // Load the order (skip if already fetched above)
+        if (!orderData) {
+          orderData = await getCheckoutOrder(orderId);
+        }
+
         if (!cancelled) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[country]/[locale]/(checkout)/order-placed/[id]/page.tsx around
lines 42 - 86, The effect double-fetches the order: loadAndComplete calls
getCheckoutOrder twice when searchParams has payment_intent; reuse the first
fetched orderData instead of refetching. Update loadAndComplete to store the
initial getCheckoutOrder(orderId) result in a local variable, pass that into the
complete flow (call completeCheckoutOrder only if needed) and then use the same
orderData for setOrder/setError, keeping the existing
cancelled/loadedRef/setLoading logic in the function; reference loadAndComplete,
getCheckoutOrder, completeCheckoutOrder, loadedRef, setOrder, setError, and
setLoading when making the change.

22-23: Consider adding generateMetadata for this page.

As a page component, this file should ideally export a generateMetadata function for SEO (e.g., "Order Confirmed - #12345"). Since this is a client component, metadata generation could be handled in a separate layout.tsx or by splitting server/client concerns. As per coding guidelines, page files should use generateMetadata to dynamically generate SEO metadata.

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

In `@src/app/`[country]/[locale]/(checkout)/order-placed/[id]/page.tsx around
lines 22 - 23, Add a generateMetadata export that produces dynamic SEO metadata
for this page (e.g., title "Order Confirmed - `#12345`") by reading the route
params (same shape as OrderPlacedPageProps / params -> { id: orderId }) or by
moving metadata into the parent layout.tsx if you prefer server-only logic;
implement generateMetadata(params) to format the order id into a title and
return a metadata object (title, description, maybe openGraph) so the
OrderPlacedPage route has proper dynamic metadata.
src/components/checkout/StripePaymentForm.tsx (1)

62-66: onReady in the dependency array can cause re-render loops if the parent doesn't memoize it.

The useEffect depends on onReady. If a consumer passes an unstable callback, this triggers infinite re-renders. Currently PaymentStep wraps handleGatewayReady in useCallback, so it works — but this is a fragile contract.

Consider either documenting the stability requirement or defending against it:

Defensive approach using a ref
+const onReadyRef = useRef(onReady);
+onReadyRef.current = onReady;

 useEffect(() => {
   if (stripe) {
-    onReady({ confirmPayment });
+    onReadyRef.current({ confirmPayment });
   }
-}, [stripe, confirmPayment, onReady]);
+}, [stripe, confirmPayment]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/checkout/StripePaymentForm.tsx` around lines 62 - 66, The
effect currently lists onReady in its dependency array which can cause re-render
loops if the parent passes an unstable callback; instead defend against
instability by storing the latest onReady in a ref and calling that ref inside
the effect so the effect only depends on stable values (stripe and
confirmPayment). Update StripePaymentForm to create an onReadyRef (e.g., useRef)
and update onReadyRef.current whenever onReady changes, then change the existing
useEffect (the one that calls onReady({ confirmPayment })) to depend only on
stripe and confirmPayment and invoke onReadyRef.current?.({ confirmPayment });
this preserves current behavior while avoiding infinite re-renders even if
PaymentStep/handleGatewayReady is not memoized.
🤖 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/app/`[country]/[locale]/(storefront)/account/credit-cards/page.tsx:
- Around line 73-84: The loadCards function and its caller loadData don't handle
getCreditCards() failures, which can leave the UI stuck loading; update
loadCards to wrap the getCreditCards() call in try/catch (or let it rethrow) and
ensure loadData always calls setLoading(false) in a finally block so the
skeleton is cleared on error, and in the catch set an error state or surface the
failure (e.g., via setCards([]) or a toast) so the user isn't left waiting;
reference the functions loadCards, loadData, getCreditCards, setCards, and
setLoading when making the changes.

In `@src/components/checkout/PaymentStep.tsx`:
- Around line 91-124: The createSession callback lacks protection against thrown
exceptions from createCheckoutPaymentSession which can leave loading true; wrap
the async body of createSession in try/catch/finally: call
createCheckoutPaymentSession inside try, handle success/result like today inside
try, in catch set an appropriate gateway error via setGatewayError(error.message
|| "Failed to create payment session.") and clear client/session state as needed
(setClientSecret(null), setPaymentSessionId(null), gatewayHandleRef.current =
null), and ensure setLoading(false) runs in finally so loading is always
cleared; keep references to createSession, createCheckoutPaymentSession,
setLoading, setGatewayError, setClientSecret, setPaymentSessionId, and
gatewayHandleRef when making the change.

---

Outside diff comments:
In `@src/app/`[country]/[locale]/(storefront)/account/orders/page.tsx:
- Around line 20-32: Remove the unused getStatusColor function: locate the
function named getStatusColor and delete its entire declaration (the switch
returning various tailwind class strings) since only getPaymentStatusColor and
getShipmentStatusColor are used in the template; after removal, run a quick
project build or search to ensure there are no remaining references to
getStatusColor and that no imports or types depend on it.

---

Duplicate comments:
In `@src/components/checkout/PaymentStep.tsx`:
- Around line 194-198: handleCardSelect calls createSession(cardId) without
awaiting and the radio inputs remain enabled when loading, so rapid card
switches can race and leave stale clientSecret/paymentSessionId; fix by
disabling all payment method radio inputs while loading (use the existing
loading state) including the "Add new payment method" radio, and either await or
serialize createSession calls in handleCardSelect (e.g., await
createSession(cardId) or cancel/ignore results from older calls) to ensure the
latest selection wins; update references: handleCardSelect, createSession,
selectedCardId, loading, and the "Add new payment method" radio.
- Around line 457-465: The submit button can become enabled before Stripe
Elements is ready because we only check clientSecret and sessionPaymentMethod;
update the component to track gateway readiness (e.g., add a state like
isGatewayReady) and set it true when gatewayHandleRef.current is populated (or
when the Stripe Elements mount callback completes) and false while loading; then
include isGatewayReady in the disabled expression alongside processing, loading,
sessionPaymentMethod and clientSecret so the button stays disabled until
gatewayHandleRef is ready, and ensure handleSubmit still checks
gatewayHandleRef.current for safety.

In `@src/components/checkout/StripePaymentForm.tsx`:
- Around line 12-14: Replace the non-null assertion on
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY so loadStripe never receives undefined: check
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY at startup and throw or log a
clear, descriptive error if it's missing, then pass the validated value into
loadStripe (the symbol stripePromise and the loadStripe call are where to make
the change); this ensures you fail fast with a readable message instead of
producing a cryptic Stripe SDK runtime error.

---

Nitpick comments:
In `@src/app/`[country]/[locale]/(checkout)/checkout/[id]/page.tsx:
- Around line 509-515: The conditional in the step indicator JSX can be
simplified because both index < currentStepIndex and index === currentStepIndex
yield the same classes; update the className expression (the JSX that references
index and currentStepIndex) to use a single branch like index <=
currentStepIndex ? "bg-primary-600 text-white" : "bg-gray-200 text-gray-500" so
you remove the redundant nested ternary and keep the same visual behavior.

In `@src/app/`[country]/[locale]/(checkout)/order-placed/[id]/page.tsx:
- Around line 42-86: The effect double-fetches the order: loadAndComplete calls
getCheckoutOrder twice when searchParams has payment_intent; reuse the first
fetched orderData instead of refetching. Update loadAndComplete to store the
initial getCheckoutOrder(orderId) result in a local variable, pass that into the
complete flow (call completeCheckoutOrder only if needed) and then use the same
orderData for setOrder/setError, keeping the existing
cancelled/loadedRef/setLoading logic in the function; reference loadAndComplete,
getCheckoutOrder, completeCheckoutOrder, loadedRef, setOrder, setError, and
setLoading when making the change.
- Around line 22-23: Add a generateMetadata export that produces dynamic SEO
metadata for this page (e.g., title "Order Confirmed - `#12345`") by reading the
route params (same shape as OrderPlacedPageProps / params -> { id: orderId }) or
by moving metadata into the parent layout.tsx if you prefer server-only logic;
implement generateMetadata(params) to format the order id into a title and
return a metadata object (title, description, maybe openGraph) so the
OrderPlacedPage route has proper dynamic metadata.

In `@src/app/`[country]/[locale]/(storefront)/account/orders/page.tsx:
- Line 162: The code uses order.payment_state?.replace("_", " ") which only
replaces the first underscore; update the transformation to replace all
underscores (e.g., use replaceAll("_", " ") or replace(/_/g, " ")) so
multi-underscore states like "balance_due_now" are fully converted; locate the
usage of order.payment_state?.replace("_", " ") in the page.tsx render and
replace it with a global-replace variant while keeping the fallback "N/A".
- Around line 64-78: The OrdersPage component should be converted from a client
component to a Server Component: remove any "use client" directive and the
useEffect/loadOrders logic, and instead call await getOrders({ per_page: 50 })
directly inside the OrdersPage function, filter the returned data for o.state
=== "complete" and render the orders synchronously; add a sibling loading.tsx
for the skeleton UI and add a generateMetadata export to provide page
metadata/SEO. Ensure references to the existing getOrders, OrdersPage, and the
current filtering logic are preserved when moving to server-side rendering.

In `@src/app/globals.css`:
- Around line 14-25: The primary color scale currently mixes Tailwind's default
blues (50–400) with custom shades (500–950) causing a visible jump; regenerate
the entire --color-primary-50 through --color-primary-950 scale from the single
base color `#0077FF` (using a generator like UIColors or Tailwind color generator)
and replace the mismatched 50–400 values so all
--color-primary-50..--color-primary-950 are derived consistently from `#0077FF` to
ensure smooth hue/saturation transitions.

In `@src/components/checkout/AddressSelector.tsx`:
- Around line 36-53: The effect is incorrectly deriving component state from
props and suppresses the missing dependency (currentAddress); replace the
useEffect logic with a derived value computed during render (e.g., useMemo) so
selected address is computed from savedAddresses and currentAddress instead of
being set in useEffect; remove the eslint-disable and the effect block, compute
const selectedAddressId = useMemo(() => { /* find matchingAddress by comparing
address1, city, zipcode, country_iso and return matchingAddress?.id ||
(currentAddress.address1 ? "new" : undefined) */ }, [savedAddresses,
currentAddress]) and update the component to use this derived selectedAddressId
(or lift control to parent if mutable state is required) so matching stays
correct when currentAddress changes.

In `@src/components/checkout/StripePaymentForm.tsx`:
- Around line 62-66: The effect currently lists onReady in its dependency array
which can cause re-render loops if the parent passes an unstable callback;
instead defend against instability by storing the latest onReady in a ref and
calling that ref inside the effect so the effect only depends on stable values
(stripe and confirmPayment). Update StripePaymentForm to create an onReadyRef
(e.g., useRef) and update onReadyRef.current whenever onReady changes, then
change the existing useEffect (the one that calls onReady({ confirmPayment }))
to depend only on stripe and confirmPayment and invoke onReadyRef.current?.({
confirmPayment }); this preserves current behavior while avoiding infinite
re-renders even if PaymentStep/handleGatewayReady is not memoized.

Comment on lines +73 to +84
const loadCards = useCallback(async () => {
const response = await getCreditCards();
setCards(response.data);
};
}, []);

useEffect(() => {
async function loadData() {
await loadCards();
setLoading(false);
}
loadData();
}, []);
}, [loadCards]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Data fetching error silently swallowed.

loadCards doesn't handle errors from getCreditCards(). If the API call fails, the promise rejects and setLoading(false) on line 81 is never reached, leaving the user stuck on the skeleton loader.

Proposed fix
   const loadCards = useCallback(async () => {
-    const response = await getCreditCards();
-    setCards(response.data);
+    try {
+      const response = await getCreditCards();
+      setCards(response.data);
+    } catch {
+      // getCreditCards uses withFallback, but guard against unexpected errors
+      setCards([]);
+    }
   }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[country]/[locale]/(storefront)/account/credit-cards/page.tsx around
lines 73 - 84, The loadCards function and its caller loadData don't handle
getCreditCards() failures, which can leave the UI stuck loading; update
loadCards to wrap the getCreditCards() call in try/catch (or let it rethrow) and
ensure loadData always calls setLoading(false) in a finally block so the
skeleton is cleared on error, and in the catch set an error state or surface the
failure (e.g., via setCards([]) or a toast) so the user isn't left waiting;
reference the functions loadCards, loadData, getCreditCards, setCards, and
setLoading when making the changes.

Comment on lines +91 to +124
const createSession = useCallback(
async (cardId: string | null) => {
if (!sessionPaymentMethod) return;

setLoading(true);
setGatewayError(null);
setClientSecret(null);
setPaymentSessionId(null);
gatewayHandleRef.current = null;

const result = await createCheckoutPaymentSession(
order.id,
sessionPaymentMethod.id,
cardId ?? undefined,
);

setLoading(false);

if (result.success && result.session) {
const secret = result.session.external_data?.client_secret as
| string
| undefined;
if (secret) {
setClientSecret(secret);
setPaymentSessionId(result.session.id);
} else {
setGatewayError("Failed to initialize payment. Please try again.");
}
} else if (!result.success) {
setGatewayError(result.error || "Failed to create payment session.");
}
},
[sessionPaymentMethod, order.id],
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

createSession has no error boundary for unexpected exceptions.

If createCheckoutPaymentSession throws (network failure, etc.), the error is unhandled. The loading state would remain true since setLoading(false) on line 107 wouldn't be reached, leaving the user stuck on the spinner.

Proposed fix: wrap in try/catch
   const createSession = useCallback(
     async (cardId: string | null) => {
       if (!sessionPaymentMethod) return;

       setLoading(true);
       setGatewayError(null);
       setClientSecret(null);
       setPaymentSessionId(null);
       gatewayHandleRef.current = null;

+      try {
         const result = await createCheckoutPaymentSession(
           order.id,
           sessionPaymentMethod.id,
           cardId ?? undefined,
         );

         setLoading(false);

         if (result.success && result.session) {
           const secret = result.session.external_data?.client_secret as
             | string
             | undefined;
           if (secret) {
             setClientSecret(secret);
             setPaymentSessionId(result.session.id);
           } else {
             setGatewayError("Failed to initialize payment. Please try again.");
           }
         } else if (!result.success) {
           setGatewayError(result.error || "Failed to create payment session.");
         }
+      } catch {
+        setLoading(false);
+        setGatewayError("Failed to initialize payment. Please try again.");
+      }
     },
     [sessionPaymentMethod, order.id],
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/checkout/PaymentStep.tsx` around lines 91 - 124, The
createSession callback lacks protection against thrown exceptions from
createCheckoutPaymentSession which can leave loading true; wrap the async body
of createSession in try/catch/finally: call createCheckoutPaymentSession inside
try, handle success/result like today inside try, in catch set an appropriate
gateway error via setGatewayError(error.message || "Failed to create payment
session.") and clear client/session state as needed (setClientSecret(null),
setPaymentSessionId(null), gatewayHandleRef.current = null), and ensure
setLoading(false) runs in finally so loading is always cleared; keep references
to createSession, createCheckoutPaymentSession, setLoading, setGatewayError,
setClientSecret, setPaymentSessionId, and gatewayHandleRef when making the
change.

@damianlegawiec
damianlegawiec merged commit a8b8550 into main Feb 17, 2026
4 checks passed
This was referenced Feb 18, 2026
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