Hanzo
CommerceStorefront Development

Checkout

Implement the complete checkout flow in your storefront

The checkout flow converts a cart into a paid order through four steps: address, shipping, payment, and completion.

Checkout Flow

1. Shipping Address ──► 2. Shipping Method ──► 3. Payment ──► 4. Confirmation

Step 1: Shipping Address

async function setShippingAddress(cartId: string, address: Address) {
  const cart = await commerce.carts.update(cartId, {
    shippingAddress: {
      line1: address.line1,
      line2: address.line2,
      city: address.city,
      state: address.state,
      postalCode: address.postalCode,
      country: address.country,
    },
  })
  // Cart now includes calculated tax and available shipping options
  return cart
}

Step 2: Shipping Method

After setting an address, fetch available shipping options:

const options = await commerce.carts.getShippingOptions(cartId)
// Returns: [
//   { id: "opt_std", name: "Standard", price: 799, estimatedDays: "5-7" },
//   { id: "opt_exp", name: "Express", price: 1499, estimatedDays: "2-3" }
// ]

// Select a shipping option
const cart = await commerce.carts.setShippingOption(cartId, 'opt_std')

Step 3: Payment

Stripe Payment

Use Stripe Elements for PCI-compliant card collection:

import { loadStripe } from '@stripe/stripe-js'
import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js'

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_KEY!)

function PaymentForm({ cartId }: { cartId: string }) {
  const stripe = useStripe()
  const elements = useElements()

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    if (!stripe || !elements) return

    // Create payment intent via Commerce API
    const { clientSecret } = await commerce.checkout.createPaymentSession({
      cartId,
      provider: 'stripe',
    })

    // Confirm with Stripe
    const { error, paymentIntent } = await stripe.confirmCardPayment(
      clientSecret,
      { payment_method: { card: elements.getElement(CardElement)! } }
    )

    if (error) {
      console.error(error.message)
      return
    }

    // Complete the cart
    const order = await commerce.carts.complete(cartId)
    window.location.href = `/order/${order.id}/confirmation`
  }

  return (
    <form onSubmit={handleSubmit}>
      <CardElement />
      <button type="submit">Pay Now</button>
    </form>
  )
}

export function CheckoutPayment({ cartId }: { cartId: string }) {
  return (
    <Elements stripe={stripePromise}>
      <PaymentForm cartId={cartId} />
    </Elements>
  )
}

PayPal Payment

const { redirectUrl } = await commerce.checkout.createPaymentSession({
  cartId,
  provider: 'paypal',
  returnUrl: `${window.location.origin}/checkout/complete`,
  cancelUrl: `${window.location.origin}/checkout/payment`,
})

// Redirect to PayPal
window.location.href = redirectUrl

Step 4: Confirmation

export default async function ConfirmationPage({
  params,
}: {
  params: { orderId: string }
}) {
  const order = await commerce.orders.get(params.orderId)

  return (
    <div>
      <h1>Order Confirmed</h1>
      <p>Order #{order.id}</p>
      <p>Total: ${(order.total / 100).toFixed(2)} {order.currency}</p>
      <div>
        {order.items.map(item => (
          <div key={item.variantId}>
            <p>{item.productName} x {item.quantity}</p>
          </div>
        ))}
      </div>
    </div>
  )
}

Complete Checkout Page

'use client'

import { useState } from 'react'
import { useCart } from '@/components/cart-provider'

type Step = 'address' | 'shipping' | 'payment' | 'confirmation'

export function CheckoutPage() {
  const { cart } = useCart()
  const [step, setStep] = useState<Step>('address')

  if (!cart) return <p>No items in cart</p>

  return (
    <div>
      <div className="flex gap-4 mb-8">
        {['address', 'shipping', 'payment', 'confirmation'].map(s => (
          <span key={s} className={step === s ? 'font-bold' : 'text-gray-400'}>
            {s.charAt(0).toUpperCase() + s.slice(1)}
          </span>
        ))}
      </div>

      {step === 'address' && <AddressForm onNext={() => setStep('shipping')} />}
      {step === 'shipping' && <ShippingSelect onNext={() => setStep('payment')} />}
      {step === 'payment' && <CheckoutPayment cartId={cart.id} />}
    </div>
  )
}

Never send raw card numbers to your server. Always use tokenized payment methods (Stripe Elements, PayPal redirect) to maintain PCI compliance.

How is this guide?

Last updated on

On this page