Hanzo
CommerceStorefront Development

Regions

Multi-region and multi-currency handling in your storefront

This guide covers region detection, currency display, and multi-region cart management in your storefront.

Region Detection

Detect the customer's region at the start of their session:

// Fetch all regions
const regions = await commerce.regions.list()

// Auto-detect from browser locale
function detectRegion(regions: Region[]): Region {
  const locale = navigator.language // e.g. "en-US", "de-DE"
  const countryCode = locale.split('-')[1] || 'US'

  const match = regions.find(r =>
    r.countries.some(c => c.iso2 === countryCode)
  )

  return match || regions.find(r => r.id === 'reg_us')!
}

Region Provider

'use client'

import { createContext, useContext, useState, useEffect } from 'react'
import { commerce } from '@/lib/commerce'
import type { Region } from '@hanzo/commerce'

const RegionContext = createContext<{
  region: Region | null
  regions: Region[]
  setRegion: (region: Region) => void
}>({} as any)

export function RegionProvider({ children }: { children: React.ReactNode }) {
  const [region, setRegionState] = useState<Region | null>(null)
  const [regions, setRegions] = useState<Region[]>([])

  useEffect(() => {
    commerce.regions.list().then(result => {
      setRegions(result)
      const saved = localStorage.getItem('regionId')
      const match = result.find(r => r.id === saved) || detectRegion(result)
      setRegionState(match)
    })
  }, [])

  function setRegion(r: Region) {
    setRegionState(r)
    localStorage.setItem('regionId', r.id)
  }

  return (
    <RegionContext.Provider value={{ region, regions, setRegion }}>
      {children}
    </RegionContext.Provider>
  )
}

export const useRegion = () => useContext(RegionContext)

Region Selector

'use client'

import { useRegion } from '@/components/region-provider'

export function RegionSelector() {
  const { region, regions, setRegion } = useRegion()

  return (
    <select
      value={region?.id || ''}
      onChange={e => {
        const r = regions.find(r => r.id === e.target.value)
        if (r) setRegion(r)
      }}
    >
      {regions.map(r => (
        <option key={r.id} value={r.id}>
          {r.name} ({r.currency})
        </option>
      ))}
    </select>
  )
}

Currency Formatting

Format prices according to the active region's currency:

function formatPrice(amount: number, currency: string, locale?: string): string {
  return new Intl.NumberFormat(locale || 'en-US', {
    style: 'currency',
    currency,
    minimumFractionDigits: 2,
  }).format(amount / 100)
}

// Usage
formatPrice(2999, 'USD')      // "$29.99"
formatPrice(2599, 'EUR', 'de-DE') // "25,99 EUR"
formatPrice(2299, 'GBP', 'en-GB') // "GBP22.99"

Region-Scoped Carts

Each cart is scoped to a single region. When a customer changes regions, create a new cart:

async function switchRegion(newRegion: Region) {
  setRegion(newRegion)

  // Create a new cart in the new region
  const newCart = await commerce.carts.create({ regionId: newRegion.id })

  // Optionally migrate items from the old cart
  if (oldCart) {
    for (const item of oldCart.items) {
      try {
        await commerce.carts.addItem(newCart.id, {
          variantId: item.variantId,
          quantity: item.quantity,
        })
      } catch {
        // Item may not be available in the new region
      }
    }
  }

  localStorage.setItem('cartId', newCart.id)
}

Tax Display

Regions control whether prices include or exclude tax:

function PriceDisplay({ price, region }: { price: number; region: Region }) {
  const formatted = formatPrice(price, region.currency)

  return (
    <span>
      {formatted}
      {region.taxInclusive && <span className="text-xs text-gray-500"> incl. tax</span>}
    </span>
  )
}

European regions typically use tax-inclusive pricing. North American regions use tax-exclusive pricing where tax is calculated and displayed at checkout.

How is this guide?

Last updated on

On this page