diff --git a/.env.example b/.env.example index 400a157..afffa50 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ # Shopify Storefront NEXT_PUBLIC_SHOPIFY_DOMAIN=mock.shop -# NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN= -# NEXT_PUBLIC_SHOPIFY_API_VERSION=2025-07 +# NEXT_PUBLIC_SHOPIFY_PUBLIC_ACCESS_TOKEN= +# NEXT_PUBLIC_SHOPIFY_API_VERSION=2026-07 # Store assistant — set to 1 to show the Ask launcher; anything else hides it NEXT_PUBLIC_ENABLE_AI=0 diff --git a/services/shopify/client.ts b/services/shopify/client.ts index 1d7ad4e..4b3b896 100644 --- a/services/shopify/client.ts +++ b/services/shopify/client.ts @@ -12,8 +12,8 @@ import { } from '@shopify/hydrogen'; import { SHOPIFY_API_VERSION, + SHOPIFY_PUBLIC_ACCESS_TOKEN, SHOPIFY_STORE_DOMAIN, - SHOPIFY_STOREFRONT_ACCESS_TOKEN, } from '@/services/shopify/config'; // No incoming request and no buyer context: these clients are module-scoped and @@ -24,16 +24,38 @@ const requestContext = createShopifyRequestContext({ i18n: { country: 'US', language: 'EN' }, }); +/** + * Workaround for a bug in this preview build of `@shopify/hydrogen`. + * + * The client tags every request with `X-Hydrogen-Version`, but the Storefront + * API does not list that header in its CORS `access-control-allow-headers`. + * Browsers therefore reject the preflight and `fetch` throws, which hydrogen + * reports as the generic "SFAPI request failed". It only bites against real + * stores — `mock.shop` answers `access-control-allow-headers: *`. + * + * Stripped in the browser only: server-side requests are not subject to CORS, + * so they keep sending the header. Remove this once the API allows it (or once + * these queries move server-side, which is the better long-term fix). + */ +const CORS_BLOCKED_HEADERS = ['X-Hydrogen-Version']; + // Hydrogen calls `fetch(url, init, cacheOptions)`; Next's caching hints ride // along on `init`, which is how the two ways of caching get to coexist. const fetchWith = (overrides: RequestInit): typeof globalThis.fetch => - ((url, init) => - globalThis.fetch(url, { ...init, ...overrides })) as typeof globalThis.fetch; + ((url, init) => { + const headers = new Headers(init?.headers); + + if (typeof document !== 'undefined') { + for (const header of CORS_BLOCKED_HEADERS) headers.delete(header); + } + + return globalThis.fetch(url, { ...init, ...overrides, headers }); + }) as typeof globalThis.fetch; const config = { storeDomain: SHOPIFY_STORE_DOMAIN!, apiVersion: SHOPIFY_API_VERSION, - publicStorefrontToken: SHOPIFY_STOREFRONT_ACCESS_TOKEN, + publicStorefrontToken: SHOPIFY_PUBLIC_ACCESS_TOKEN, }; /** diff --git a/services/shopify/config.ts b/services/shopify/config.ts index fd5d344..2cbb729 100644 --- a/services/shopify/config.ts +++ b/services/shopify/config.ts @@ -4,9 +4,13 @@ // safe to read from client components as well as server code. export const SHOPIFY_STORE_DOMAIN = process.env.NEXT_PUBLIC_SHOPIFY_DOMAIN; -/** Omitted for tokenless storefronts such as `mock.shop`. */ -export const SHOPIFY_STOREFRONT_ACCESS_TOKEN = - process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN; +/** + * Public Storefront API access token. Safe to expose to the browser — that is + * what "public" means here. Omitted for tokenless storefronts such as + * `mock.shop`. Never put a *private* token behind a `NEXT_PUBLIC_` name. + */ +export const SHOPIFY_PUBLIC_ACCESS_TOKEN = + process.env.NEXT_PUBLIC_SHOPIFY_PUBLIC_ACCESS_TOKEN; export const SHOPIFY_API_VERSION = - process.env.NEXT_PUBLIC_SHOPIFY_API_VERSION || '2025-07'; + process.env.NEXT_PUBLIC_SHOPIFY_API_VERSION || '2026-07';