Hanzo

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?

On this page