import { BrainerceClient } from 'brainerce'; import { cache } from 'react'; import { SALES_CHANNEL_ID } from '@/core/lib/brainerce'; import { pickPublicStoreInfo, type PublicStoreInfo } from '@/core/lib/store-info'; import { getRequestOrigin } from '@/core/lib/site-url'; /** * SERVER-ONLY half of the SDK wiring. Kept apart from `brainerce.ts` on * purpose: everything here reaches `next/headers` (to resolve the request's * origin), and ~30 client components import `getClient` from `brainerce.ts`. * A single shared module would drag `next/headers` into the client bundle and * fail the build outright — which is exactly what happened when these two * functions still lived there. * * Import from here in Server Components, `generateMetadata`, route handlers, * `sitemap.ts` / `robots.ts`. Never from a `'use client'` file. */ /** * Server-side client — calls the backend directly (no proxy needed for public * data). Used by Server Components for SSR data fetching. * * ⛔ ASYNC on purpose. The backend compares the `Origin` we send against the * domain configured on the sales channel, and a LIVE channel rejects both a * mismatched Origin AND a missing one with 403. The correct value is only * knowable from the request when no `SITE_URL` is configured — the norm for * AI-builder deploys — so resolving it needs an await. This previously read * `process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'`, which sent * localhost from every such deploy, so every SSR call failed the moment the * channel went Live. */ export async function getServerClient(locale?: string): Promise { const apiUrl = process.env.BRAINERCE_API_URL || 'https://api.brainerce.com'; const client = new BrainerceClient({ salesChannelId: SALES_CHANNEL_ID, baseUrl: apiUrl, origin: await getRequestOrigin(), }); if (locale) { client.setLocale(locale); } return client; } /** * Server-side cached fetch of store info, used to seed `` at * SSR so client components don't render with `null` storeInfo and bake the * wrong currency symbol into the HTML Googlebot indexes. * * Two cache layers: * - React `cache()` dedupes calls within a single request render so the * layout + `generateMetadata` share one fetch. * - Next.js `fetch({ next: { revalidate, tags } })` caches the response * across requests for ~60s and lets the dashboard bust the cache via * `revalidateTag('store-info')` on store-config save. * * Returns `null` on hard failure so the provider's client-side `useEffect` * fallback can take over — behaviour identical to pre-SSR-hydration. * * Bypasses {@link getServerClient} on purpose: the SDK's internal `fetch` * does not forward `next:` options, so it cannot participate in the Next.js * Data Cache today. Switch back to the SDK once it accepts `fetchOptions`. */ export const fetchStoreInfo = cache(async (locale?: string): Promise => { const apiUrl = process.env.BRAINERCE_API_URL || 'https://api.brainerce.com'; const siteOrigin = await getRequestOrigin(); const url = `${apiUrl}/api/vc/${encodeURIComponent(SALES_CHANNEL_ID)}/info`; try { const res = await fetch(url, { headers: { Origin: siteOrigin, ...(locale ? { 'Accept-Language': locale } : {}), }, next: { revalidate: 60, tags: ['store-info'] }, }); if (!res.ok) return null; return pickPublicStoreInfo(await res.json()); } catch { return null; } });