Final step in conversion funnel where customers provide shipping and payment information. Must be optimized for completion with minimal friction.
⚠️ CRITICAL: Always fetch shipping methods AND payment methods from backend. Users must be able to select from available options - never skip payment method selection.
Fetch from backend after address provided (shipping methods vary by address/region):
Display as radio buttons with cost + delivery estimate
Update order total immediately when method changes
Highlight free shipping if available
Show “Add $X for free shipping” if close to threshold
Handle unavailable shipping: show message, suggest alternatives
Split shipments (Medusa v2.16.0+): a cart can hold multiple shipping methods, one per shipping profile — so items with different fulfillment requirements (e.g. oversized vs. standard) can ship separately. If the store’s shipping options span multiple profiles, group the cart items by profile and let the customer pick a method per group instead of forcing a single store-wide choice. Query the docs/MCP for the exact add-shipping-method payload (it accepts an array of options).
CRITICAL: Always fetch payment methods from backend and allow user to select from available options.
Payment methods vary by store configuration (backend settings). NEVER assume which payment methods are available or hardcode payment options. Users MUST be able to choose their preferred payment method.
Fetch available methods from backend:
typescript
// ALWAYS fetch payment providers from backend// For Medusa:const { payment_providers } = await sdk.store.payment.listPaymentProviders()// For other backends:// Change based on the integrated backendconst paymentMethods = await fetch(`${apiUrl}/payment-methods`)// Returns: card, paypal, apple_pay, google_pay, stripe, etc.
Display payment method selection UI:
Show all enabled payment providers returned by backend
Allow user to select their preferred method (radio buttons or cards)
Don’t skip selection step - user must actively choose
Map backend codes to display names in the storefront. For example pp_system_manual -> Manual payment.
Common options: Credit/Debit Card, PayPal, Apple Pay, Google Pay, Buy Now Pay Later
Available payment methods (examples, actual options come from backend):
Credit/Debit Card (most common, via Stripe/Braintree/other gateway)
PayPal (redirect or in-context)
Apple Pay (Safari, iOS only)
Google Pay (Chrome, Android)
Buy Now Pay Later (Affirm, Klarna - if enabled by store)
Manual payment (bank transfer, cash on delivery - if enabled)
Why backend fetching is required:
Store admin controls which payment providers are enabled
Payment methods vary by region, currency, order value
Test vs production mode affects available methods
Can’t assume all stores use the same payment gateway
For Medusa backends - Payment flow:
List available payment providers:
typescript
const { payment_providers } = await sdk.store.payment.listPaymentProviders({ region_id: cart.region_id // Required to get region-specific providers})
Display providers and allow user to select:
Show payment providers as radio buttons or cards. User must actively select one.
Initialize payment session after selection:
typescript
// When user selects a providerawait sdk.store.payment.initiatePaymentSession(cart, { provider_id: selectedProvider.id // e.g., "pp_stripe_stripe", "pp_system_default"})// Re-fetch cart to get updated payment session dataconst { cart: updatedCart } = await sdk.store.cart.retrieve(cart.id)
Render provider-specific UI:
Stripe providers (pp_stripe_*): Render Stripe Elements card UI
Manual payment (pp_system_default): No additional UI needed
Other providers: Implement according to provider requirements
Important: Payment provider IDs are returned from the backend (e.g., pp_stripe_stripe, pp_system_manual). Map these to user-friendly display names in your UI.
Async payment methods (Medusa v2.17.2+): some payment methods (bank debits, vouchers, several Klarna/SEPA flows) aren’t confirmed during checkout — the provider confirms them later via webhook. Don’t design the confirmation step to assume payment succeeded the moment the order is placed:
Show a “payment pending” state on the order confirmation page rather than “paid”.
Never gate order placement on a synchronous payment success response.
Tell the customer what happens next (they’ll get an email once payment clears).
Digital wallets (mobile priority):
Apple Pay / Google Pay should be prominent on mobile
Desktop: Sticky sidebar with items, prices, totals. Updates in real-time.
Mobile: Collapsible at top (“Show order summary” toggle). Keeps focus on form.
After order is successfully placed, you MUST reset the cart state:
Common issue: Cart popup and cart state still show old cart content after order is placed. This happens because the global cart state (Context, Zustand, Redux) isn’t cleared after checkout completion.
Required actions on successful order:
Clear cart from global state:
Reset cart state in Context/Zustand/Redux to null or empty
Update cart count to 0 in navbar
Prevent old cart items from showing in cart popup
Clear localStorage cart ID:
Remove cart ID from localStorage: localStorage.removeItem('cart_id')
Or create new cart and update cart ID in localStorage
Ensures fresh cart for next shopping session
Invalidate cart queries (if using TanStack Query):