import { cache } from 'react'; import { cookies, headers } from 'next/headers'; import type { PublicRegion } from 'brainerce'; import { getServerClient } from '@/core/lib/brainerce.server'; import { COUNTRY_HEADERS, REGION_COOKIE, normalizeCountry, pickRegion } from '@/core/lib/region'; /** * SERVER-ONLY half of region resolution. Everything here reaches * `next/headers`; importing it from a `'use client'` file fails the build. * Import it in Server Components, `generateMetadata`, and route handlers. * * Resolution order, highest first: * 1. the `brainerce_region` cookie (the shopper used the switcher) * 2. the edge geo header for this host * 3. the store's default region * * The cookie wins because an IP guess is wrong for travellers, expats, VPNs * and anyone on a corporate egress, and it is the shopper who knows. */ /** * The buyer country as the edge saw it, or null. * * Read straight off the incoming request rather than forwarded by middleware: * `headers()` in a Server Component already sees the original request headers, * including through the locale rewrite, so a middleware hop would be a second * copy of the same value that can drift out of step with this one. * * ⛔ Next 15 removed `request.geo`. The country lives in a header now, and * which header depends on the host, so we try each in turn. */ export const getBuyerCountry = cache(async (): Promise => { try { const h = await headers(); for (const name of COUNTRY_HEADERS) { const code = normalizeCountry(h.get(name)); if (code) return code; } } catch { // headers() throws outside a request scope (a build-time prerender). // No country is a valid answer: the default region takes over. } return null; }); /** * The store's active regions, default first. `[]` on any failure. * * ⛔ Never throws. A brand-new store is empty, the channel may not be live * yet, and the backend can hiccup. An empty list makes every caller below a * no-op and the storefront behaves exactly as it did before regions existed, * which is the only acceptable failure mode for a call this widely used. */ export const fetchRegions = cache(async (): Promise => { try { const client = await getServerClient(); const result = await client.getStoreRegions(); return Array.isArray(result?.data) ? result.data : []; } catch { return []; } }); export interface ResolvedRegion { /** Every active region. Empty when the store has none or the call failed. */ regions: PublicRegion[]; /** The region to price this request in, or null when there are none. */ region: PublicRegion | null; /** Buyer country from the edge, or null. Reused by the tax estimate. */ country: string | null; } /** * Resolve the region for this request. `cache()`-wrapped, so the layout, a * page and `generateMetadata` share one round trip. * * Returns `{ regions: [], region: null, country }` for a store with no * regions. Callers spread `region?.id` into their SDK calls, so that case * sends no `regionId` at all, which is the pre-regions behaviour. */ export const resolveRegion = cache(async (): Promise => { const [regions, country] = await Promise.all([fetchRegions(), getBuyerCountry()]); if (regions.length === 0) return { regions, region: null, country }; // 1. The shopper's explicit choice, if it still names a live region. A // stale id (the merchant deleted or deactivated that region) falls // through to the geo guess rather than pricing in a region that is gone. let chosen: PublicRegion | null = null; try { const cookieValue = (await cookies()).get(REGION_COOKIE)?.value; if (cookieValue) chosen = regions.find((r) => r.id === cookieValue) ?? null; } catch { // Same as headers(): outside a request scope there is no cookie jar. } // 2/3. Geo header, then the default region. return { regions, region: chosen ?? pickRegion(regions, country), country }; }); /** * Just the id, for spreading into an SDK call: * * const regionId = await getRegionId(); * await client.getProducts({ limit: 24, ...(regionId ? { regionId } : {}) }); * * ⛔ Pass it to the product reads AND to `createCheckout`. Product reads only * and the shopper is shown one price and charged another, which is worse than * not having built regions at all. */ export async function getRegionId(): Promise { return (await resolveRegion()).region?.id; }