Skip to content

Add Express Checkout with Apple Pay / Google Pay - #34

Closed
Cichorek wants to merge 5 commits into
mainfrom
quick-checkout
Closed

Add Express Checkout with Apple Pay / Google Pay#34
Cichorek wants to merge 5 commits into
mainfrom
quick-checkout

Conversation

@Cichorek

@Cichorek Cichorek commented Feb 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #29

  • Add ExpressCheckoutButton component using Stripe's Express Checkout Element with Apple Pay, Google Pay, and Link wallet support
  • Implement server-side quick checkout flow: partial address → advance → full address → payment session → finalize
  • Integrate Express Checkout in both the cart drawer and cart page, with a processing spinner that replaces the summary/action buttons during payment
  • Extract shared Stripe initialization to lib/utils/stripe.ts with Stripe Connect (stripeAccount) support
  • Add HTTPS dev server setup (mkcert + shop.lvh.me) required for Apple Pay testing locally

New files

File Purpose
src/components/checkout/ExpressCheckoutButton.tsx Client component wrapping Stripe ExpressCheckoutElement with shipping address/rate resolution and payment confirmation
src/lib/data/quick-checkout.ts Server actions for quick checkout address updates and order advancing via @spree/next
src/lib/data/express-checkout-flow.ts Orchestration layer composing quick-checkout + payment server actions
src/lib/utils/express-checkout.ts Pure utility helpers (line items, shipping rate maps, address conversion)
src/lib/utils/stripe.ts Shared stripePromise with optional Stripe Connect account

How it works

  1. Customer clicks Apple Pay / Google Pay in the cart drawer or cart page
  2. Stripe collects shipping address → we resolve shipping rates via Spree
  3. Customer selects a shipping rate → we select it in Spree and update the total
  4. Customer confirms payment → we set full addresses, create a payment session, confirm via Stripe, and finalize the order
  5. Customer is redirected to the order-placed page

Test plan

  • Verify Express Checkout buttons appear in cart drawer when Stripe env vars are configured
  • Verify Express Checkout buttons appear on cart page
  • Test Apple Pay flow end-to-end (requires HTTPS via npm run dev:https)
  • Test Google Pay flow end-to-end
  • Test cancellation clears temporary addresses
  • Verify fallback to regular checkout still works
  • Verify buttons don't appear when cart total is $0

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Express Checkout (Apple Pay, Google Pay, Link) in cart and drawer with automatic cart refresh on completion
    • Quick checkout flow for faster address, shipping-rate selection, and server-side express checkout support
    • Centralized Stripe client initialization for payments
  • Documentation

    • HTTPS local development guide for Apple/Google Pay (mkcert + dev HTTPS script)
    • Example environment variables for Stripe (publishable key, account ID)
  • Chores

    • Added dev HTTPS script and ignored certificates directory

@coderabbitai

coderabbitai Bot commented Feb 28, 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 a client-side Stripe Express Checkout UI and integration into cart UI, centralizes Stripe initialization, introduces server-side quick/express checkout actions and utilities, and adds HTTPS development docs/config and environment example updates.

Changes

Cohort / File(s) Summary
Env & HTTPS docs
\.env\.local\.example, README\.md, package\.json, \.gitignore
Documented Stripe client env vars, added HTTPS dev instructions and dev:https npm script, and ignored certificates directory.
Next Config
next\.config\.ts
Added allowedDevOrigins: ["shop.lvh.me"] to Next.js config.
Express Checkout UI
src/components/checkout/ExpressCheckoutButton.tsx, src/components/checkout/index.ts
New client-only ExpressCheckoutButton component implementing multi-step wallet checkout and re-exported from checkout barrel.
Cart Integration
src/app/.../cart/page.tsx, src/components/cart/CartDrawer.tsx
Integrated ExpressCheckoutButton into cart page and drawer; added refreshCart usage, expressProcessing state, conditional rendering, and spacing adjustments.
Stripe init refactor
src/components/checkout/StripePaymentForm.tsx, src/lib/utils/stripe.ts
Centralized Stripe initialization into src/lib/utils/stripe.ts exporting stripePromise; StripePaymentForm now consumes shared promise.
Server-side quick/express flows
src/lib/data/quick-checkout.ts, src/lib/data/express-checkout-flow.ts, src/lib/data/index.ts
Added quick-checkout helpers and express-checkout server actions (resolve shipping, select rates, prepare payment, create session, finalize) with actionResult wrappers and exports.
Express checkout utilities
src/lib/utils/express-checkout.ts
New utilities: currency-to-cents, line-item builders, name/address mapping, shipping rate mapping (Google Pay workarounds) and related types.
Misc
package\.json (script), next\.config\.ts (config), \.gitignore
Minor additions: HTTPS dev script, allowed dev origin, and certs ignore entry.

Sequence Diagram

sequenceDiagram
    participant Browser as Browser
    participant UI as ExpressCheckoutButton
    participant Stripe as Stripe SDK
    participant Server as Server Actions
    participant Spree as Spree Backend

    Browser->>UI: Click express checkout
    UI->>Stripe: Initialize Elements / present wallet
    Stripe-->>Browser: Wallet UI (Apple/Google/Link)
    Browser->>Stripe: Select payment method & shipping address
    Stripe->>UI: Address & paymentMethod token

    UI->>Server: expressCheckoutResolveShipping(orderId, address)
    Server->>Spree: updateAddresses + advance
    Spree-->>Server: order with shipments/rates
    Server-->>UI: shipping options

    UI->>Server: expressCheckoutSelectRates(orderId, selections)
    Server->>Spree: selectShippingRate(s)
    Spree-->>Server: updated totals
    Server-->>UI: updated order totals

    UI->>Server: expressCheckoutPreparePayment(orderId, fullAddress)
    Server->>Spree: update full address & email
    Spree-->>Server: ready order
    Server-->>UI: ready for payment

    UI->>Server: expressCheckoutCreateSession(orderId, paymentMethodId, stripePaymentMethodId)
    Server->>Spree: createCheckoutPaymentSession
    Spree-->>Server: sessionId
    Server-->>UI: sessionId

    UI->>Stripe: confirm payment with sessionId
    Stripe-->>UI: payment confirmed
    UI->>Server: expressCheckoutFinalize(orderId, sessionId)
    Server->>Spree: complete payment & order
    Spree-->>Server: success
    Server-->>UI: complete
    UI->>Browser: Navigate to order confirmation
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped from cart to wallet, swift and spry,
Clicked once, let Apple/Google do the sky,
Rates and names mapped, totals counted neat,
A tiny hop—order placed—carrot treat!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: adding Express Checkout functionality with Apple Pay and Google Pay support.
Linked Issues check ✅ Passed All coding requirements from issue #29 are met: Express Checkout component [ExpressCheckoutButton], server-side quick checkout flow [quick-checkout, express-checkout-flow], shipping rate resolution, address updates, payment session creation, and order finalization.
Out of Scope Changes check ✅ Passed All changes are scoped to Express Checkout implementation and its prerequisites: component, server actions, utilities, Stripe initialization, HTTPS dev setup, 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 quick-checkout

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: 8

🧹 Nitpick comments (7)
src/lib/utils/express-checkout.ts (1)

17-34: Strengthen typing for checkout helpers (input + return types).

Please replace Record<string, unknown>/casts with concrete SDK types and add explicit return types on exported helpers to prevent silent shape drift.

💡 Example direction
+import type { StoreOrder } from "@spree/sdk";

-export function buildLineItems(order: Record<string, unknown>) {
+export function buildLineItems(order: StoreOrder): Array<{ name: string; amount: number }> {
   const items: Array<{ name: string; amount: number }> = [];

-  const itemTotal = toCents(order.item_total as string);
+  const itemTotal = toCents(order.item_total);
   items.push({ name: "Subtotal", amount: itemTotal });

-  const promoTotal = toCents(order.promo_total as string);
+  const promoTotal = toCents(order.promo_total);
   if (promoTotal < 0) {
     items.push({ name: "Discount", amount: promoTotal });
   }

-  const additionalTaxTotal = toCents(order.additional_tax_total as string);
+  const additionalTaxTotal = toCents(order.additional_tax_total);
   if (additionalTaxTotal > 0) {
     items.push({ name: "Tax", amount: additionalTaxTotal });
   }

   return items;
 }

As per coding guidelines, "Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type."
Based on learnings, "Import and use Spree SDK types (StoreProduct, StoreVariant, StoreOrder, StoreLineItem, PaginatedResponse) for type safety."

Also applies to: 52-75

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

In `@src/lib/utils/express-checkout.ts` around lines 17 - 34, The exported helper
buildLineItems uses loose typing (Record<string, unknown>) and runtime casts
which can hide shape drift; replace the input type with the appropriate Spree
SDK type (e.g., StoreOrder) and give the function an explicit return type (e.g.,
StoreLineItem[] or Array<{ name: string; amount: number }>), remove all "as"
casts for order.item_total / promo_total / additional_tax_total and instead
access those fields with proper types and safe checks (optional chaining /
default values) so the compiler enforces the shape; apply the same pattern to
the other helpers mentioned (lines ~52-75) by importing and using StoreProduct,
StoreVariant, PaginatedResponse, etc., and returning the concrete SDK types.
src/lib/data/express-checkout-flow.ts (1)

16-16: Use absolute import for actionResult

This file uses ./utils; prefer @/lib/... to match the project import convention.

As per coding guidelines: **/*.{ts,tsx}: Use absolute imports with @ alias (e.g., @/components/..., @/lib/...) instead of relative imports

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

In `@src/lib/data/express-checkout-flow.ts` at line 16, The import for
actionResult in express-checkout-flow.ts uses a relative path ("./utils");
update it to the project's absolute alias (use "@/lib/data/utils" or the correct
`@/lib` path where utils.ts lives) so the import reads with the @ alias and
matches project convention; ensure the imported symbol name actionResult remains
unchanged and that the file still builds after switching the import.
src/components/cart/CartDrawer.tsx (1)

6-8: Consider dynamically importing ExpressCheckoutButton to keep Stripe out of the always-loaded drawer bundle

CartDrawer is typically present site-wide; a static import can pull Stripe Elements code into a very hot client bundle. A next/dynamic split (with ssr: false + small loading fallback) would likely help.

As per coding guidelines: src/components/**/*.{ts,tsx}: Use dynamic imports with loading fallback for non-critical components to improve code splitting

Also applies to: 274-282

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

In `@src/components/cart/CartDrawer.tsx` around lines 6 - 8, Replace the static
import of ExpressCheckoutButton in CartDrawer with a dynamic import using
next/dynamic (ssr: false) and a small loading fallback so Stripe Elements aren't
bundled into the always-loaded drawer; locate the ExpressCheckoutButton
reference in the CartDrawer component and swap the import for something like
dynamic(() => import('@/components/checkout').then(m =>
m.ExpressCheckoutButton), { ssr: false, loading: () => <SmallLoadingFallback/>
}), and apply the same change to the other ExpressCheckoutButton usage in this
file (the block referenced around the second occurrence).
src/app/[country]/[locale]/(storefront)/cart/page.tsx (1)

8-9: Optional: code-split ExpressCheckoutButton on the cart page too

Less critical than the drawer (route-level bundle vs persistent bundle), but still a reasonable win if Stripe adds noticeable weight.

As per coding guidelines: src/components/**/*.{ts,tsx}: Use dynamic imports with loading fallback for non-critical components to improve code splitting

Also applies to: 180-189

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

In `@src/app/`[country]/[locale]/(storefront)/cart/page.tsx around lines 8 - 9,
Replace the static import of ExpressCheckoutButton with a dynamic import to
code-split the cart page: remove the top-level import of ExpressCheckoutButton
in page.tsx and use Next.js dynamic import (dynamic(() =>
import(".../ExpressCheckoutButton"), { ssr: false, loading: () =>
<YourLoadingFallback/> })) where the component is rendered; keep ShoppingBagIcon
as a normal import. Apply the same change for the other occurrences noted
(around the existing ExpressCheckoutButton usage at lines ~180-189) so the
non-critical Stripe UI is loaded asynchronously with a simple loading fallback
component.
src/components/checkout/ExpressCheckoutButton.tsx (2)

313-321: expressCheckoutFinalize is treated as best-effort, but you’ll never see business failures via catch

expressCheckoutFinalize is an actionResult-style API (it returns { success: false, error } instead of throwing). So the current try/catch only catches transport/runtime failures, not a “failed finalize” response. Consider checking the returned { success } and logging/telemetry at least (even if you still proceed).

As per coding guidelines: **/*.ts{,x}: Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type

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

In `@src/components/checkout/ExpressCheckoutButton.tsx` around lines 313 - 321,
The try/catch around expressCheckoutFinalize only catches transport/runtime
errors but not business failures because expressCheckoutFinalize returns an
actionResult object; update the code that calls expressCheckoutFinalize(orderId,
sessionId) to await the result, explicitly check the returned { success, error }
(or typed ActionResult) and log or send telemetry when success is false (include
orderId/sessionId/context), then proceed with
router.push(`${basePath}/order-placed/${orderId}`) and onComplete() as before;
also ensure expressCheckoutFinalize has an explicit TypeScript return type
(e.g., ActionResult) instead of any so the caller can rely on typing.

36-41: Tighten types to reduce Record<string, unknown> / assertion chains (and consider exporting the props type)

There are several cascading assertions (e.g., cart as unknown as Record<string, unknown>, result.order as Record<string, unknown>, inline shipments casting). This makes it easy to miss a shape mismatch (especially around shipments.shipping_rates).

Suggestions:

  • Define a small “order-like” type with only the fields you read (item_total, promo_total, additional_tax_total, shipments) and use it across client + server action return types.
  • Export ExpressCheckoutButtonProps if you want to enable typed dynamic imports from the cart drawer/page without falling back to any.

As per coding guidelines: **/*.ts{,x}: Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type

Also applies to: 87-91, 110-120, 239-266, 288-295, 401-439

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

In `@src/components/checkout/ExpressCheckoutButton.tsx` around lines 36 - 41, The
props and many inline casts are too loose—define a narrow exported order-like
type (e.g., export type OrderSummary = { item_total: number; promo_total?:
number; additional_tax_total?: number; shipments?: { shipping_rates?: Array<{
id: string; price: number; }>; }[] } ) that captures only the fields read, then
replace the ad-hoc casts (e.g., the uses of cart as unknown as Record<string,
unknown>, result.order as Record<string, unknown>, and inline shipments casting)
with that type (use OrderSummary | undefined where appropriate), update any
client/server action return types to return OrderSummary for the order shape,
and export ExpressCheckoutButtonProps so dynamic imports keep strong typing;
adjust the functions/methods that reference shipments.shipping_rates to use the
typed shipments shape instead of assertion chains.
src/lib/data/quick-checkout.ts (1)

5-8: Avoid as casts on address literals; use typed variables / satisfies

Right now you’re using as QuickCheckoutAddressParams to bypass excess-property checks. Prefer a typed variable (or satisfies) so TypeScript still validates the shape (and to follow the repo guideline).

Proposed change (typed variables, no `as`)
-import { actionResult } from "./utils";
+import { actionResult } from "@/lib/data/utils";
@@
 export async function quickCheckoutUpdateAddress(
   orderId: string,
   address: QuickCheckoutPartialAddress,
 ) {
   return actionResult(async () => {
+    const ship_address: QuickCheckoutAddressParams = {
+      firstname: address.firstname || "Express",
+      lastname: address.lastname || "Checkout",
+      address1: "TBD",
+      ...address,
+      quick_checkout: true,
+    };
     const order = await updateAddresses(orderId, {
-      ship_address: {
-        firstname: address.firstname || "Express",
-        lastname: address.lastname || "Checkout",
-        address1: "TBD",
-        ...address,
-        quick_checkout: true,
-      } as QuickCheckoutAddressParams,
+      ship_address,
     });
     return { order };
   }, "Failed to update address for quick checkout");
 }
@@
 export async function quickCheckoutUpdateFullAddress(
@@
 ) {
   return actionResult(async () => {
+    const ship_address: QuickCheckoutAddressParams = {
+      ...params.shipAddress,
+      quick_checkout: true,
+    };
+    const bill_address: QuickCheckoutAddressParams = {
+      ...params.billAddress,
+      quick_checkout: true,
+    };
     const order = await updateAddresses(orderId, {
       email: params.email,
-      ship_address: {
-        ...params.shipAddress,
-        quick_checkout: true,
-      } as QuickCheckoutAddressParams,
-      bill_address: {
-        ...params.billAddress,
-        quick_checkout: true,
-      } as QuickCheckoutAddressParams,
+      ship_address,
+      bill_address,
     });
     return { order };
   }, "Failed to update full address");
 }

As per coding guidelines: **/*.ts{,x}: Use 'satisfies' operator for type checking object literals instead of casting with 'as'

Also applies to: 19-35, 44-66

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

In `@src/lib/data/quick-checkout.ts` around lines 5 - 8, You’re bypassing TS
excess-property checks by casting address object literals to
QuickCheckoutAddressParams with `as`; instead declare the object with the type
or use the `satisfies` operator so the compiler validates the shape. Find all
places that use `as QuickCheckoutAddressParams` (the address literal creations
referenced in the review) and replace them with either `const addr:
QuickCheckoutAddressParams = { ... }` or `const addr = { ... } satisfies
QuickCheckoutAddressParams`, ensuring required keys match the
QuickCheckoutAddressParams type; update any callers that expect the previous
cast only if needed.
🤖 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/cart/CartDrawer.tsx`:
- Around line 274-282: Replace the current ExpressCheckoutButton callback that
only closes the drawer with an onComplete handler that calls refreshCart()
before closeCart() so the cart badge state is updated immediately; update the
onComplete prop on the ExpressCheckoutButton in CartDrawer to invoke refreshCart
(awaiting it if needed) and then call closeCart, referring to the existing
CartDrawer, ExpressCheckoutButton, refreshCart, and closeCart symbols to locate
the change.

In `@src/components/checkout/ExpressCheckoutButton.tsx`:
- Around line 188-336: handleConfirm can run twice causing non-idempotent side
effects; add a re-entrancy guard using a React ref (e.g., const isConfirmingRef
= useRef(false)) and check it at the top of handleConfirm: if
isConfirmingRef.current is true, call event.paymentFailed({ reason: "fail" })
and return; otherwise set isConfirmingRef.current = true before starting async
work. Ensure you only clear the guard when it's safe: if stripePaymentConfirmed
is false (failure/cancel paths) reset isConfirmingRef.current = false in the
fail() paths and in the catch/finally; if payment succeeds (after
stripePaymentConfirmed = true and router.push/onComplete) you may leave it true
(or reset after full finalization) to prevent re-run. Reference handleConfirm,
stripePaymentConfirmed, fail, event.paymentFailed, expressCheckoutFinalize,
router.push and onComplete when making the changes.
- Around line 93-186: In handleShippingAddressChange, after building
shippingRates and lineItems but before event.resolve, compute the total amount
for the default selected rate (use shippingRates[0] as the default), sum
buildLineItems(order).amount plus defaultRate.amount (fallback 0), and call
elements?.update({ amount: defaultAmount }) to ensure Elements is synced; keep
shippingRateMapRef handling and then call event.resolve({ shippingRates,
lineItems }) as before.
- Around line 83-91: Update the payment failure reason type and the express
payment handling: change the type annotation used by fail() to include
"invalid_billing_address" and "address_unserviceable" in addition to the
existing "fail" | "invalid_shipping_address" | "invalid_payment_data" so it
matches Stripe v8.7.0; then review handleClick and the isGooglePayRef logic in
function handleClick (which reads event.expressPaymentType) — either explicitly
handle other values ('apple_pay'|'link'|'paypal') if you need distinct behavior
or add a comment clarifying that only google_pay is intentionally special-cased
and all other methods are treated the same. Use the symbols fail(), handleClick,
isGooglePayRef and event.expressPaymentType to locate the code to update.

In `@src/lib/data/express-checkout-flow.ts`:
- Around line 83-92: expressCheckoutFinalize currently ignores the return values
of completeCheckoutPaymentSession and completeCheckoutOrder (they return {
success: false, error } on failure) so it can report success incorrectly; modify
expressCheckoutFinalize to capture each call's result, check result.success, and
if false throw an Error (or return a rejected result) with the returned error
message (e.g., throw new Error(result.error || 'completeCheckoutPaymentSession
failed')) before proceeding to the next step so actionResult will report
failure; ensure you reference the functions completeCheckoutPaymentSession and
completeCheckoutOrder when adding these checks and throw meaningful errors that
include the underlying error text.

In `@src/lib/utils/express-checkout.ts`:
- Around line 115-123: The current logic in the block that constructs rateMap
(using rate.shipping_method_id) only sets the amount once and ignores subsequent
rates with the same shipping_method_id, causing shippingRates.amount to
undercount; modify the handling in the code that references rateMap,
rate.shipping_method_id, rate.cost and toCents so that if
rateMap.has(rate.shipping_method_id) you update the existing entry by adding to
its amount (e.g., existing.amount += toCents(rate.cost)) instead of skipping,
while preserving the existing id/displayName (and keep the current id generation
using randomSuffix() and isGooglePay only when creating a new entry).
- Around line 2-4: The toCents function currently converts its input with
Number(...) and can return NaN/Infinity; update toCents to first coerce the
input into a numeric value (e.g., const n = Number(amount)), then validate
Number.isFinite(n) and if not finite throw a clear TypeError or RangeError
describing the invalid amount (include the original value/type in the message);
finally return Math.round(n * 100). This change should be applied inside the
toCents function to ensure only finite numeric inputs are converted for Stripe
amounts.

In `@src/lib/utils/stripe.ts`:
- Around line 5-7: Extract process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY into a
const (e.g. publishableKey), remove the non-null assertion from the loadStripe
call, and guard against a missing key by logging an explicit error
(console.error or a logger) and using a safe fallback (empty string or
undefined) when calling loadStripe in stripePromise; update the loadStripe
invocation that currently references stripeAccountId so it uses the new
publishableKey variable and preserves the existing stripeAccount option logic.

---

Nitpick comments:
In `@src/app/`[country]/[locale]/(storefront)/cart/page.tsx:
- Around line 8-9: Replace the static import of ExpressCheckoutButton with a
dynamic import to code-split the cart page: remove the top-level import of
ExpressCheckoutButton in page.tsx and use Next.js dynamic import (dynamic(() =>
import(".../ExpressCheckoutButton"), { ssr: false, loading: () =>
<YourLoadingFallback/> })) where the component is rendered; keep ShoppingBagIcon
as a normal import. Apply the same change for the other occurrences noted
(around the existing ExpressCheckoutButton usage at lines ~180-189) so the
non-critical Stripe UI is loaded asynchronously with a simple loading fallback
component.

In `@src/components/cart/CartDrawer.tsx`:
- Around line 6-8: Replace the static import of ExpressCheckoutButton in
CartDrawer with a dynamic import using next/dynamic (ssr: false) and a small
loading fallback so Stripe Elements aren't bundled into the always-loaded
drawer; locate the ExpressCheckoutButton reference in the CartDrawer component
and swap the import for something like dynamic(() =>
import('@/components/checkout').then(m => m.ExpressCheckoutButton), { ssr:
false, loading: () => <SmallLoadingFallback/> }), and apply the same change to
the other ExpressCheckoutButton usage in this file (the block referenced around
the second occurrence).

In `@src/components/checkout/ExpressCheckoutButton.tsx`:
- Around line 313-321: The try/catch around expressCheckoutFinalize only catches
transport/runtime errors but not business failures because
expressCheckoutFinalize returns an actionResult object; update the code that
calls expressCheckoutFinalize(orderId, sessionId) to await the result,
explicitly check the returned { success, error } (or typed ActionResult) and log
or send telemetry when success is false (include orderId/sessionId/context),
then proceed with router.push(`${basePath}/order-placed/${orderId}`) and
onComplete() as before; also ensure expressCheckoutFinalize has an explicit
TypeScript return type (e.g., ActionResult) instead of any so the caller can
rely on typing.
- Around line 36-41: The props and many inline casts are too loose—define a
narrow exported order-like type (e.g., export type OrderSummary = { item_total:
number; promo_total?: number; additional_tax_total?: number; shipments?: {
shipping_rates?: Array<{ id: string; price: number; }>; }[] } ) that captures
only the fields read, then replace the ad-hoc casts (e.g., the uses of cart as
unknown as Record<string, unknown>, result.order as Record<string, unknown>, and
inline shipments casting) with that type (use OrderSummary | undefined where
appropriate), update any client/server action return types to return
OrderSummary for the order shape, and export ExpressCheckoutButtonProps so
dynamic imports keep strong typing; adjust the functions/methods that reference
shipments.shipping_rates to use the typed shipments shape instead of assertion
chains.

In `@src/lib/data/express-checkout-flow.ts`:
- Line 16: The import for actionResult in express-checkout-flow.ts uses a
relative path ("./utils"); update it to the project's absolute alias (use
"@/lib/data/utils" or the correct `@/lib` path where utils.ts lives) so the import
reads with the @ alias and matches project convention; ensure the imported
symbol name actionResult remains unchanged and that the file still builds after
switching the import.

In `@src/lib/data/quick-checkout.ts`:
- Around line 5-8: You’re bypassing TS excess-property checks by casting address
object literals to QuickCheckoutAddressParams with `as`; instead declare the
object with the type or use the `satisfies` operator so the compiler validates
the shape. Find all places that use `as QuickCheckoutAddressParams` (the address
literal creations referenced in the review) and replace them with either `const
addr: QuickCheckoutAddressParams = { ... }` or `const addr = { ... } satisfies
QuickCheckoutAddressParams`, ensuring required keys match the
QuickCheckoutAddressParams type; update any callers that expect the previous
cast only if needed.

In `@src/lib/utils/express-checkout.ts`:
- Around line 17-34: The exported helper buildLineItems uses loose typing
(Record<string, unknown>) and runtime casts which can hide shape drift; replace
the input type with the appropriate Spree SDK type (e.g., StoreOrder) and give
the function an explicit return type (e.g., StoreLineItem[] or Array<{ name:
string; amount: number }>), remove all "as" casts for order.item_total /
promo_total / additional_tax_total and instead access those fields with proper
types and safe checks (optional chaining / default values) so the compiler
enforces the shape; apply the same pattern to the other helpers mentioned (lines
~52-75) by importing and using StoreProduct, StoreVariant, PaginatedResponse,
etc., and returning the concrete SDK types.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8698cae and 01cfb7d.

📒 Files selected for processing (14)
  • .env.local.example
  • README.md
  • next.config.ts
  • package.json
  • src/app/[country]/[locale]/(storefront)/cart/page.tsx
  • src/components/cart/CartDrawer.tsx
  • src/components/checkout/ExpressCheckoutButton.tsx
  • src/components/checkout/StripePaymentForm.tsx
  • src/components/checkout/index.ts
  • src/lib/data/express-checkout-flow.ts
  • src/lib/data/index.ts
  • src/lib/data/quick-checkout.ts
  • src/lib/utils/express-checkout.ts
  • src/lib/utils/stripe.ts

Comment thread src/components/cart/CartDrawer.tsx
Comment thread src/components/checkout/ExpressCheckoutButton.tsx
Comment thread src/components/checkout/ExpressCheckoutButton.tsx
Comment thread src/components/checkout/ExpressCheckoutButton.tsx
Comment thread src/lib/data/express-checkout-flow.ts
Comment thread src/lib/utils/express-checkout.ts Outdated
Comment thread src/lib/utils/express-checkout.ts
Comment thread src/lib/utils/stripe.ts

@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

🧹 Nitpick comments (5)
.gitignore (1)

48-48: Anchor the certificates ignore rule to the repo root.

certificates can match nested directories too. If this is specifically for ./certificates used by dev:https, prefer a root-anchored directory pattern.

Suggested diff
-#certs for https
-certificates
+# certs for https
+/certificates/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 48, The .gitignore entry "certificates" is unanchored and
will match nested paths; replace it with a root-anchored directory pattern (use
"/certificates/" or "/certificates") so only the repo-root ./certificates
directory used by dev:https is ignored; update the .gitignore line for the
"certificates" entry accordingly.
src/lib/data/express-checkout-flow.ts (2)

16-16: Use alias import here for consistency with project path rules.

Prefer @/lib/data/utils instead of ./utils in this module.

As per coding guidelines: "Use absolute imports with @ alias (e.g., @/components/..., @/lib/...) instead of relative imports".

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

In `@src/lib/data/express-checkout-flow.ts` at line 16, Replace the relative
import of actionResult in src/lib/data/express-checkout-flow.ts with the project
alias path: change the import that currently references "./utils" to
"@/lib/data/utils" so the symbol actionResult is imported via the alias and
matches the project's absolute import convention.

33-33: Avoid broad Record<string, unknown> casts for order payloads.

These casts hide contract mismatches. Define/propagate a typed order shape from the action layer instead.

As per coding guidelines: "Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type".

Also applies to: 49-49, 79-79

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

In `@src/lib/data/express-checkout-flow.ts` at line 33, Replace the broad cast to
Record<string, unknown> for the order payload by introducing and using an
explicit Order interface/type and propagating it through the action layer and
return types; locate the returns that cast advanceResult.order (the occurrences
at/around the lines returning "advanceResult.order as Record<string, unknown>")
and change the function signatures to return Promise<{ order: Order }> (or
appropriate sync type), update the type of advanceResult (or the action that
produces it) so advanceResult.order is typed as Order, and adjust any callers to
accept the new typed shape so the contract is enforced rather than hidden by a
broad cast.
src/components/checkout/ExpressCheckoutButton.tsx (1)

446-451: Consider replacing fixed $200 buffer with configurable/store-aware logic.

A hardcoded buffer can still be too low for high-shipping orders, and the wallet amount cannot be increased after opening. Making this configurable (or derived from known shipping ceilings) reduces edge-case payment failures.

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

In `@src/components/checkout/ExpressCheckoutButton.tsx` around lines 446 - 451,
Replace the hardcoded $200 buffer used in the useMemo that computes amount by
making the buffer configurable or calculated from store/shipping data: update
the amount calculation in the useMemo (referencing amount, toCents, cart.total)
to use a configurable prop or context value (e.g., checkoutConfig.walletBuffer)
or derive a ceiling from known shipping options/selectedShippingRate (e.g.,
maxShippingRate or shippingCeiling) and add that derived buffer; also ensure the
logic that opens the wallet (where amount is consumed) can accept an updated
amount rather than a fixed precomputed value so the wallet total can increase if
shipping changes.
src/lib/utils/express-checkout.ts (1)

23-39: Use a typed order shape instead of Record<string, unknown> in line-item building.

This function relies on known order totals; keeping it untyped forces unsafe casts and weakens compile-time checks.

As per coding guidelines: "Import and use Spree SDK types (StoreProduct, StoreVariant, StoreOrder, StoreLineItem, PaginatedResponse) for type safety".

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

In `@src/lib/utils/express-checkout.ts` around lines 23 - 39, Change the loose
Record type to the Spree SDK order type and remove unsafe casts: import and use
StoreOrder (from the Spree types) in the buildLineItems signature so
buildLineItems(order: StoreOrder) instead of Record<string, unknown>, then call
toCents with the properly typed order properties (order.item_total,
order.promo_total, order.additional_tax_total) without "as string" casts; update
the returned item shape if desired to a typed alias (or StoreLineItem-like
shape) to keep compile-time safety. Ensure you add the Spree import and update
references to buildLineItems and toCents accordingly.
🤖 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/ExpressCheckoutButton.tsx`:
- Around line 351-353: The call to onComplete() is not awaited which can leak
rejected promises and break the post-success flow; change the sequence to await
onComplete() (e.g., await onComplete()) and wrap it in try/catch/finally so you
handle errors and always reset isConfirmingRef.current = false in the finally
block, calling router.push(`${basePath}/order-placed/${orderId}`) either before
awaiting onComplete() if navigation must happen first or after if onComplete
must complete prior to navigation—adjust ordering accordingly in
ExpressCheckoutButton where router.push, onComplete, and isConfirmingRef.current
are used.

In `@src/lib/utils/express-checkout.ts`:
- Around line 2-9: toCents currently multiplies by 100 unconditionally, which
inflates amounts for zero-decimal currencies; change toCents to accept a
currency parameter (e.g. toCents(amount: string|number, currency: string)) and
compute the multiplier based on whether the currency is zero-decimal (use a
set/list of Stripe zero-decimal currencies or Stripe's minor unit map) so it
returns Math.round(n * multiplier). Thread the new currency argument through
buildLineItems and buildShippingRateMap so they call toCents(price, currency)
(and update their signatures/call sites accordingly) to ensure amounts for JPY,
KRW, VND, etc. are not multiplied by 100.

---

Nitpick comments:
In @.gitignore:
- Line 48: The .gitignore entry "certificates" is unanchored and will match
nested paths; replace it with a root-anchored directory pattern (use
"/certificates/" or "/certificates") so only the repo-root ./certificates
directory used by dev:https is ignored; update the .gitignore line for the
"certificates" entry accordingly.

In `@src/components/checkout/ExpressCheckoutButton.tsx`:
- Around line 446-451: Replace the hardcoded $200 buffer used in the useMemo
that computes amount by making the buffer configurable or calculated from
store/shipping data: update the amount calculation in the useMemo (referencing
amount, toCents, cart.total) to use a configurable prop or context value (e.g.,
checkoutConfig.walletBuffer) or derive a ceiling from known shipping
options/selectedShippingRate (e.g., maxShippingRate or shippingCeiling) and add
that derived buffer; also ensure the logic that opens the wallet (where amount
is consumed) can accept an updated amount rather than a fixed precomputed value
so the wallet total can increase if shipping changes.

In `@src/lib/data/express-checkout-flow.ts`:
- Line 16: Replace the relative import of actionResult in
src/lib/data/express-checkout-flow.ts with the project alias path: change the
import that currently references "./utils" to "@/lib/data/utils" so the symbol
actionResult is imported via the alias and matches the project's absolute import
convention.
- Line 33: Replace the broad cast to Record<string, unknown> for the order
payload by introducing and using an explicit Order interface/type and
propagating it through the action layer and return types; locate the returns
that cast advanceResult.order (the occurrences at/around the lines returning
"advanceResult.order as Record<string, unknown>") and change the function
signatures to return Promise<{ order: Order }> (or appropriate sync type),
update the type of advanceResult (or the action that produces it) so
advanceResult.order is typed as Order, and adjust any callers to accept the new
typed shape so the contract is enforced rather than hidden by a broad cast.

In `@src/lib/utils/express-checkout.ts`:
- Around line 23-39: Change the loose Record type to the Spree SDK order type
and remove unsafe casts: import and use StoreOrder (from the Spree types) in the
buildLineItems signature so buildLineItems(order: StoreOrder) instead of
Record<string, unknown>, then call toCents with the properly typed order
properties (order.item_total, order.promo_total, order.additional_tax_total)
without "as string" casts; update the returned item shape if desired to a typed
alias (or StoreLineItem-like shape) to keep compile-time safety. Ensure you add
the Spree import and update references to buildLineItems and toCents
accordingly.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01cfb7d and 44022bd.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • .gitignore
  • src/app/[country]/[locale]/(storefront)/cart/page.tsx
  • src/components/cart/CartDrawer.tsx
  • src/components/checkout/ExpressCheckoutButton.tsx
  • src/lib/data/express-checkout-flow.ts
  • src/lib/utils/express-checkout.ts
  • src/lib/utils/stripe.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/utils/stripe.ts

Comment thread src/components/checkout/ExpressCheckoutButton.tsx Outdated
Comment thread src/lib/utils/express-checkout.ts 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.

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

127-141: ⚠️ Potential issue | 🟠 Major

Resolve amount synchronization before event.resolve() and fail fast on update errors.

Right now the flow resolves the shipping event first, then treats elements.update failures as non-fatal. That can continue the wallet with a stale amount and defer failure to confirm-time.

🔧 Suggested fix
-        const lineItems = buildLineItems(order);
-        event.resolve({ shippingRates, lineItems });
-
-        // Sync Elements amount to match the default (first) shipping rate
-        // so subsequent rate changes can only decrease the authorized amount.
-        try {
-          const lineItemsSum = lineItems.reduce(
-            (sum, item) => sum + item.amount,
-            0,
-          );
-          const defaultShippingAmount = shippingRates[0]?.amount ?? 0;
-          elements?.update({ amount: lineItemsSum + defaultShippingAmount });
-        } catch (_) {
-          /* elements.update failed — non-fatal */
-        }
+        const lineItems = buildLineItems(order);
+        const lineItemsSum = lineItems.reduce((sum, item) => sum + item.amount, 0);
+        const defaultShippingAmount = shippingRates[0]?.amount ?? 0;
+        try {
+          elements?.update({ amount: lineItemsSum + defaultShippingAmount });
+        } catch (_updateErr) {
+          setError("Could not update payment amount for the selected shipping address.");
+          event.reject();
+          return;
+        }
+        event.resolve({ shippingRates, lineItems });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/checkout/ExpressCheckoutButton.tsx` around lines 127 - 141,
Compute the line items and default shipping amount (using buildLineItems and
shippingRates[0]?.amount) and call elements.update({ amount: lineItemsSum +
defaultShippingAmount }) before calling event.resolve; do not swallow errors
from elements.update — instead fail fast by propagating the error or calling
event.reject with the error so the wallet flow stops rather than continuing with
a stale amount; only call event.resolve({ shippingRates, lineItems }) after
elements.update succeeds.
🧹 Nitpick comments (3)
src/components/checkout/ExpressCheckoutButton.tsx (2)

43-48: Add explicit return types for component functions.

ExpressCheckoutInner and ExpressCheckoutButton currently rely on inference. Explicit return types make the component contracts clearer and stricter.

🧩 Suggested refactor
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import type { JSX } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@
 function ExpressCheckoutInner({
   cart,
   basePath,
   onComplete,
   onProcessingChange,
-}: ExpressCheckoutButtonProps) {
+}: ExpressCheckoutButtonProps): JSX.Element | null {
@@
 export function ExpressCheckoutButton({
   cart,
   basePath,
   onComplete,
   onProcessingChange,
-}: ExpressCheckoutButtonProps) {
+}: ExpressCheckoutButtonProps): JSX.Element {

As per coding guidelines: "**/*.ts{,x}: Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type".

Also applies to: 430-435

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

In `@src/components/checkout/ExpressCheckoutButton.tsx` around lines 43 - 48, Add
explicit return types to the component functions: annotate ExpressCheckoutInner
and ExpressCheckoutButton with a React/JSX return type (e.g., : JSX.Element or :
React.ReactElement) instead of relying on inference; update the function
signatures for ExpressCheckoutInner and the exported ExpressCheckoutButton to
include the chosen return type and ensure any corresponding props types remain
unchanged. Also apply the same explicit return-type change to the other
component occurrence referenced (the function around lines 430–435) so all
component functions in this file have explicit JSX/React return types.

449-455: Prefer satisfies for Stripe options object typing instead of literal casts.

Using as const here weakens object-literal shape validation. satisfies keeps literal precision and validates against Stripe’s options contract.

♻️ Suggested refactor
 import type {
   StripeExpressCheckoutElementClickEvent,
   StripeExpressCheckoutElementConfirmEvent,
+  StripeElementsOptions,
   StripeExpressCheckoutElementReadyEvent,
   StripeExpressCheckoutElementShippingAddressChangeEvent,
   StripeExpressCheckoutElementShippingRateChangeEvent,
 } from "@stripe/stripe-js";
@@
   const options = useMemo(
-    () => ({
-      mode: "payment" as const,
-      amount,
-      currency,
-      paymentMethodCreation: "manual" as const,
-    }),
+    () =>
+      ({
+        mode: "payment",
+        amount,
+        currency,
+        paymentMethodCreation: "manual",
+      }) satisfies StripeElementsOptions,
     [amount, currency],
   );

As per coding guidelines: "**/*.ts{,x}: Use 'satisfies' operator for type checking object literals instead of casting with 'as'".

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

In `@src/components/checkout/ExpressCheckoutButton.tsx` around lines 449 - 455,
Replace the two "as const" casts in the useMemo options object with a TypeScript
"satisfies" assertion to preserve literal precision and validate the object
shape against the Stripe options type; update the options creation in the
ExpressCheckoutButton component (the options variable returned by useMemo,
including the mode and paymentMethodCreation properties) to remove "as const" on
the properties and instead append a single "satisfies <StripeOptionsType>" (use
the appropriate Stripe options type from `@stripe/stripe-js` or your Stripe
typings) so the object is both literal-precise and type-checked.
src/lib/utils/express-checkout.ts (1)

55-73: Add explicit return types for exported utility functions.

buildLineItems and buildSpreeAddress should expose explicit return types to keep their public contracts stable and strict.

🧱 Suggested refactor
-export function buildLineItems(order: StoreOrder) {
+export function buildLineItems(
+  order: StoreOrder,
+): Array<{ name: string; amount: number }> {
@@
 export function buildSpreeAddress(
   name: { firstname: string; lastname: string },
   address: {
@@
   },
   phone?: string,
-) {
+): {
+  firstname: string;
+  lastname: string;
+  address1: string;
+  address2?: string;
+  city: string;
+  zipcode: string;
+  country_iso: string;
+  state_name?: string;
+  phone?: string;
+} {

As per coding guidelines: "**/*.ts{,x}: Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type".

Also applies to: 91-114

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

In `@src/lib/utils/express-checkout.ts` around lines 55 - 73, The exported
functions buildLineItems and buildSpreeAddress currently rely on inferred return
types; add explicit return type annotations to their function signatures to make
the public contract strict and stable — for buildLineItems annotate the return
type to match the array shape being returned (an array of objects with
name:string and amount:number) and for buildSpreeAddress add the explicit return
type that matches the Spree address shape used elsewhere (e.g., the
SpreeAddress/interface used in your codebase); update the exported signatures
for these functions so TypeScript enforces the exact structure instead of
inferring it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/components/checkout/ExpressCheckoutButton.tsx`:
- Around line 127-141: Compute the line items and default shipping amount (using
buildLineItems and shippingRates[0]?.amount) and call elements.update({ amount:
lineItemsSum + defaultShippingAmount }) before calling event.resolve; do not
swallow errors from elements.update — instead fail fast by propagating the error
or calling event.reject with the error so the wallet flow stops rather than
continuing with a stale amount; only call event.resolve({ shippingRates,
lineItems }) after elements.update succeeds.

---

Nitpick comments:
In `@src/components/checkout/ExpressCheckoutButton.tsx`:
- Around line 43-48: Add explicit return types to the component functions:
annotate ExpressCheckoutInner and ExpressCheckoutButton with a React/JSX return
type (e.g., : JSX.Element or : React.ReactElement) instead of relying on
inference; update the function signatures for ExpressCheckoutInner and the
exported ExpressCheckoutButton to include the chosen return type and ensure any
corresponding props types remain unchanged. Also apply the same explicit
return-type change to the other component occurrence referenced (the function
around lines 430–435) so all component functions in this file have explicit
JSX/React return types.
- Around line 449-455: Replace the two "as const" casts in the useMemo options
object with a TypeScript "satisfies" assertion to preserve literal precision and
validate the object shape against the Stripe options type; update the options
creation in the ExpressCheckoutButton component (the options variable returned
by useMemo, including the mode and paymentMethodCreation properties) to remove
"as const" on the properties and instead append a single "satisfies
<StripeOptionsType>" (use the appropriate Stripe options type from
`@stripe/stripe-js` or your Stripe typings) so the object is both literal-precise
and type-checked.

In `@src/lib/utils/express-checkout.ts`:
- Around line 55-73: The exported functions buildLineItems and buildSpreeAddress
currently rely on inferred return types; add explicit return type annotations to
their function signatures to make the public contract strict and stable — for
buildLineItems annotate the return type to match the array shape being returned
(an array of objects with name:string and amount:number) and for
buildSpreeAddress add the explicit return type that matches the Spree address
shape used elsewhere (e.g., the SpreeAddress/interface used in your codebase);
update the exported signatures for these functions so TypeScript enforces the
exact structure instead of inferring it.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44022bd and ef49033.

📒 Files selected for processing (4)
  • .gitignore
  • src/components/checkout/ExpressCheckoutButton.tsx
  • src/lib/data/express-checkout-flow.ts
  • src/lib/utils/express-checkout.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/lib/data/express-checkout-flow.ts
  • .gitignore

@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

🧹 Nitpick comments (2)
src/lib/data/quick-checkout.ts (2)

5-5: Use the repo alias for local imports.

./utils breaks the TS import rule used elsewhere in the app. Import this via @/lib/data/utils instead.

As per coding guidelines, "Use absolute imports with @ alias (e.g., @/components/..., @/lib/...) instead of relative imports".

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

In `@src/lib/data/quick-checkout.ts` at line 5, The import in quick-checkout.ts
uses a relative path ("./utils") which violates the project's TS import rule;
replace it with the repo alias import "@/lib/data/utils" so the symbol
actionResult is imported via the absolute alias (e.g., change the import that
brings in actionResult in quick-checkout.ts to use "@/lib/data/utils").

16-19: Add explicit return types to the exported server actions.

These helpers are part of the public lib/data surface and are already consumed by src/lib/data/express-checkout-flow.ts. Right now the contract is only whatever actionResult infers, which makes upstream type drift harder to catch.

Example of the shape to pin explicitly
-import type { AddressParams } from "@spree/sdk";
+import type { AddressParams, StoreOrder } from "@spree/sdk";
 import { actionResult } from "./utils";
+
+type QuickCheckoutActionResult = Promise<
+  | { success: true; order: StoreOrder }
+  | { success: false; error: string }
+>;

 export async function quickCheckoutUpdateAddress(
   orderId: string,
   address: QuickCheckoutPartialAddress,
-) {
+): QuickCheckoutActionResult {
   return actionResult(async () => {
     const order = await updateOrder(orderId, {
       ship_address: {
         firstname: address.firstname || "Express",
         lastname: address.lastname || "Checkout",
         address1: "TBD",
         ...address,
         quick_checkout: true,
       },
     });
     return { order };
   }, "Failed to update address for quick checkout");
 }

-export async function quickCheckoutAdvance(orderId: string) {
+export async function quickCheckoutAdvance(
+  orderId: string,
+): QuickCheckoutActionResult {

As per coding guidelines, "Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type" and "Import and use Spree SDK types (StoreProduct, StoreVariant, StoreOrder, StoreLineItem, PaginatedResponse) for type safety".

Also applies to: 34-34, 41-48, 65-65

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

In `@src/lib/data/quick-checkout.ts` around lines 16 - 19, The exported server
action quickCheckoutUpdateAddress (and the other exported helpers noted)
currently rely on inferred actionResult types; add explicit TypeScript return
types to each exported function (e.g., quickCheckoutUpdateAddress): import and
use the appropriate Spree SDK types (such as StoreOrder, StoreLineItem,
PaginatedResponse, StoreProduct/StoreVariant as applicable) to declare a
concrete Promise<...> return type that matches the actionResult shape consumed
by src/lib/data/express-checkout-flow.ts, and update the function signatures to
return that explicit type instead of relying on inference.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/data/quick-checkout.ts`:
- Around line 22-26: In the ship_address object, the current order spreads
...address after setting fallback firstname/lastname which allows empty or
undefined values from address to overwrite the fallbacks; move the spread
operator (...address) before the firstname and lastname assignments in the
ship_address construction and change the assignments to use trimmed existence
checks (e.g., use address.firstname?.trim() and address.lastname?.trim() to
decide whether to use address values or the fallbacks "Express" and "Checkout")
so whitespace-only or empty strings don't override the defaults.
- Around line 67-69: The update in quick-checkout.ts is incorrectly clearing
addresses by passing empty strings to updateOrder; change the payload passed to
updateOrder(orderId, ...) so that ship_address_id and bill_address_id are set to
the literal string "CLEAR" (not ""), ensuring the temporary wallet address is
removed; locate the call to updateOrder in the quick-checkout logic (the
function/updateOrder invocation around orderId) and replace the empty-string
values with "CLEAR".

---

Nitpick comments:
In `@src/lib/data/quick-checkout.ts`:
- Line 5: The import in quick-checkout.ts uses a relative path ("./utils") which
violates the project's TS import rule; replace it with the repo alias import
"@/lib/data/utils" so the symbol actionResult is imported via the absolute alias
(e.g., change the import that brings in actionResult in quick-checkout.ts to use
"@/lib/data/utils").
- Around line 16-19: The exported server action quickCheckoutUpdateAddress (and
the other exported helpers noted) currently rely on inferred actionResult types;
add explicit TypeScript return types to each exported function (e.g.,
quickCheckoutUpdateAddress): import and use the appropriate Spree SDK types
(such as StoreOrder, StoreLineItem, PaginatedResponse, StoreProduct/StoreVariant
as applicable) to declare a concrete Promise<...> return type that matches the
actionResult shape consumed by src/lib/data/express-checkout-flow.ts, and update
the function signatures to return that explicit type instead of relying on
inference.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5c3d87d9-0d59-4f7e-9db6-235d8d62ac6a

📥 Commits

Reviewing files that changed from the base of the PR and between 6d9eb73 and 49ada51.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • README.md
  • next.config.ts
  • package.json
  • src/lib/data/index.ts
  • src/lib/data/quick-checkout.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/lib/data/index.ts
  • package.json
  • README.md

Comment thread src/lib/data/quick-checkout.ts
Comment thread src/lib/data/quick-checkout.ts Outdated
Cichorek and others added 5 commits March 12, 2026 10:05
Enable customers to skip the full checkout flow and pay directly from the
cart drawer and cart page using Stripe Express Checkout Element with
Apple Pay, Google Pay, and Link wallet payments.

- Add ExpressCheckoutButton component with full shipping address/rate
  resolution and Stripe payment confirmation flow
- Add server actions for quick checkout (address update, advance, finalize)
- Add express-checkout-flow orchestration layer between component and
  server actions
- Extract shared Stripe initialization to lib/utils/stripe.ts with
  Stripe Connect (stripeAccount) support
- Add utility helpers for building line items, shipping rate maps, and
  address conversion between Stripe and Spree formats
- Integrate Express Checkout in CartDrawer and cart page with processing
  state that hides summary/actions during payment
- Add HTTPS dev server setup (mkcert + shop.lvh.me) for Apple Pay testing
- Add Stripe env vars to .env.local.example

Closes #29

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- toCents: add Number.isFinite validation to prevent NaN propagation
- buildShippingRateMap: accumulate shipping cost across multiple shipments
- stripe.ts: remove non-null assertion, add graceful degradation for missing key
- expressCheckoutFinalize: check return values from payment session/order completion
- ExpressCheckoutButton: export props interface, add re-entrancy guard,
  widen fail() reason type, sync Elements amount after shipping resolve,
  check finalize result, add Google Pay comment
- CartDrawer: await refreshCart() before closeCart in onComplete,
  dynamic import for ExpressCheckoutButton (code-split Stripe bundle)
- Cart page: dynamic import for ExpressCheckoutButton
- .gitignore: add certificates directory
- Update @spree/sdk to ^0.5.0 (fixes CountrySwitcher build error)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Complete

- toCents: add currency parameter with Stripe zero-decimal currency support
  (JPY, KRW, VND, etc.) to prevent inflated amounts
- buildLineItems: use StoreOrder type instead of Record<string, unknown>,
  thread currency through toCents calls
- buildShippingRateMap: use StoreShipment type from SDK, add currency param
- express-checkout-flow: remove unnecessary Record<string, unknown> casts,
  let StoreOrder type flow through from SDK functions
- ExpressCheckoutButton: remove all Record<string, unknown> casts, await
  onComplete() in try/finally to prevent promise leaks and ensure
  isConfirmingRef cleanup, widen onComplete type to void | Promise<void>
- Extract SHIPPING_BUFFER_AMOUNT constant for Apple Pay pre-authorization,
  make it currency-aware via toCents
- .gitignore: anchor /certificates to repo root only

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace removed `updateAddresses` with `updateOrder` in quick-checkout
and remove `getStore`/`StoreStore` which no longer exist in the SDK.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…hs, and return types

- Move spread operator before firstname/lastname fallbacks with trim checks
- Use "CLEAR" instead of empty strings for clearing addresses
- Replace relative import paths with @/ aliases
- Add explicit TypeScript return types to all exported functions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

Quick checkout with Apple pay / Google pay

2 participants