/** * CLIENT-SAFE half of region resolution. Nothing here may reach * `next/headers`, because `` and the region switcher import it. * The server half lives in `region.server.ts`. * * ## Why regions exist even when the merchant has never heard of them * * A store can split the world into regions, each with its own currency and * payment providers. Every other optional feature in this project auto-hides * when it is off, so forgetting one costs nothing until the merchant switches * it on. Regions are the exception: a storefront that never sends `regionId` * does not hide anything, it silently renders the DEFAULT region's prices to * every shopper on earth. The page looks perfect and the prices are wrong. * * A single-region store is a complete no-op. `getStoreRegions()` returns one * row, `regions.length <= 1`, the switcher renders nothing, and the `regionId` * we attach to reads resolves to the default the backend would have used * anyway. There is no empty dropdown and no extra chrome. The whole point is * that the day a merchant adds a second region, this storefront is already * correct instead of quietly overcharging half its customers. */ import type { PublicRegion } from 'brainerce'; /** * Manual region choice, set by the switcher. A cookie rather than React state * on purpose: the server renders product pages and has to pick the same region * the shopper chose, or SSR paints the geo-guessed prices and the client flips * them a moment later. * * Not `httpOnly`, because the switcher is a client component and has to write * it. It holds a region id, which is public data already present in the page. */ export const REGION_COOKIE = 'brainerce_region'; /** One year. A region preference is not session state; it is where you live. */ const REGION_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; /** * Request headers that carry a buyer country, in priority order. * * Brainerce cannot derive this itself: the storefront SERVER is what reaches * the Brainerce API, so the backend sees this machine's IP, not the shopper's. * The country has to be extracted at the edge and forwarded, which is exactly * what these headers are. * * `x-country` is last and is the manual escape hatch: a host with no geo * header of its own can be made to set it in a proxy rule. */ export const COUNTRY_HEADERS = [ 'cf-ipcountry', // Cloudflare 'x-vercel-ip-country', // Vercel 'client-geo-country', // Fastly 'x-country', // manual / self-hosted proxies ] as const; /** * Accept only a plausible ISO-3166-1 alpha-2 code. Cloudflare sends `XX` for * unknown clients and `T1` for Tor, and both would otherwise be forwarded to * the API as if they were countries. */ export function normalizeCountry(raw: string | null | undefined): string | null { if (!raw) return null; const code = raw.trim().toUpperCase(); if (!/^[A-Z]{2}$/.test(code)) return null; if (code === 'XX' || code === 'T1') return null; return code; } /** * Pick the region for a country from an already-fetched list. Mirrors the * SDK's `client.detectRegion(country, regions)` without needing a client * instance, and keeps the return type narrowed to `PublicRegion`. * * Order: explicit country match → the default region → the first row. The last * two fallbacks are what make a misconfigured store render prices instead of * nothing. */ export function pickRegion( regions: PublicRegion[], country: string | null | undefined ): PublicRegion | null { if (regions.length === 0) return null; const code = normalizeCountry(country); if (code) { const matched = regions.find((r) => r.countries.includes(code)); if (matched) return matched; } return regions.find((r) => r.isDefault) ?? regions[0] ?? null; } /** Read the manual region choice in the browser. Returns null during SSR. */ export function readRegionCookie(): string | null { if (typeof document === 'undefined') return null; const match = document.cookie.match( new RegExp(`(?:^|;\\s*)${REGION_COOKIE}=([^;]*)`) ); return match ? decodeURIComponent(match[1]) : null; } /** * Persist the manual region choice. `SameSite=Lax` so a link from an email or * an ad still arrives with the shopper's chosen currency; `Secure` only off * localhost, where there is no https to attach it to. */ export function writeRegionCookie(regionId: string): void { if (typeof document === 'undefined') return; const secure = typeof location !== 'undefined' && location.protocol === 'https:'; document.cookie = `${REGION_COOKIE}=${encodeURIComponent(regionId)}; path=/; max-age=${REGION_COOKIE_MAX_AGE}; SameSite=Lax` + (secure ? '; Secure' : ''); }