diff --git a/.changeset/currency-before-cart.md b/.changeset/currency-before-cart.md new file mode 100644 index 0000000000..93b145c03a --- /dev/null +++ b/.changeset/currency-before-cart.md @@ -0,0 +1,5 @@ +--- +"@shopify/hydrogen": patch +--- + +Update analytics skills to require `i18n.currency` during initial `ShopifyScripts` rendering so `window.Shopify.currency.active` exists before consent replays buffered events. diff --git a/.changeset/shiny-coins-count.md b/.changeset/shiny-coins-count.md new file mode 100644 index 0000000000..5f8d2827ba --- /dev/null +++ b/.changeset/shiny-coins-count.md @@ -0,0 +1,5 @@ +--- +"@shopify/hydrogen": minor +--- + +Require `i18n.currency` in `ShopifyScripts` / `getShopifyScriptTags()` whenever Shopify analytics is enabled (the default). Product events carry prices, and product pages can be viewed before the cart initializes currency, so `window.Shopify.currency.active` must be set from initial script rendering. `i18n` and `currency` remain optional with `shopifyAnalytics: false`. JS consumers and the Vue binding get a runtime warning instead of a compile-time error. Adds the `ShopifyScriptsI18nWithCurrency` type. diff --git a/examples/hydrogen/app/root.tsx b/examples/hydrogen/app/root.tsx index d0150e8511..8fc030f4d2 100644 --- a/examples/hydrogen/app/root.tsx +++ b/examples/hydrogen/app/root.tsx @@ -29,6 +29,9 @@ import appStyles from "~/styles/app.css?url"; import resetStyles from "~/styles/reset.css?url"; const FALLBACK_STOREFRONT_ID = "0"; +// Shopify analytics requires a currency for product events; used until the +// Storefront API localization currency is available. +const FALLBACK_ANALYTICS_CURRENCY = "USD"; export type RootLoader = typeof loader; @@ -81,7 +84,7 @@ export async function loader(args: Route.LoaderArgs) { country: storefront.i18n.country, language: storefront.i18n.language, pathPrefix: storefront.i18n.pathPrefix, - ...(analyticsCurrency ? { currency: analyticsCurrency } : {}), + currency: analyticsCurrency ?? FALLBACK_ANALYTICS_CURRENCY, }; return { diff --git a/packages/hydrogen/skills/hydrogen-analytics/SKILL.md b/packages/hydrogen/skills/hydrogen-analytics/SKILL.md index 7d858fba88..efdd758d01 100644 --- a/packages/hydrogen/skills/hydrogen-analytics/SKILL.md +++ b/packages/hydrogen/skills/hydrogen-analytics/SKILL.md @@ -19,7 +19,7 @@ Prerequisite: analytics depends on the same-origin SFAPI proxy (see `hydrogen-re ## Core Pattern -Render Shopify runtime scripts once from the app root/document head with the same resolved market used by Storefront API requests. Use `ShopifyScripts` from your framework binding if it exports one, or `getShopifyScriptTags()` / `renderShopifyScriptTags()` from core in other framework heads. Pass the `shop` object matching the `ShopifyScriptsShop` type, and `i18n` matching the `ShopifyScriptsI18n` type and serialize them into ShopifyScripts. — declare them as constants with type annotations to keep errors located close to the source of writing. The analytics bus is created by default; pass `analytics` only for optional bus configuration such as `customData`. Do not pass market `country` or `language` through analytics consent config. +Render Shopify runtime scripts once from the app root/document head with the same resolved market used by Storefront API requests. Use `ShopifyScripts` from your framework binding if it exports one, or `getShopifyScriptTags()` / `renderShopifyScriptTags()` from core in other framework heads. Pass the `shop` object matching the `ShopifyScriptsShop` type, and `i18n` matching the `ShopifyScriptsI18nWithCurrency` type and serialize them into ShopifyScripts. — declare them as constants with type annotations to keep errors located close to the source of writing. The analytics bus is created by default; pass `analytics` only for optional bus configuration such as `customData`. Do not pass market `country` or `language` through analytics consent config. Read one browser-lazy singleton from the Shopify global created by `ShopifyScripts`: @@ -43,7 +43,12 @@ export function getAnalytics(): StorefrontAnalytics | null { export { trackCartAnalytics }; ``` -Read `shop` and `i18n` values on the server and pass them to `ShopifyScripts`. Do not read env APIs in browser modules. +Read `shop` and `i18n` values on the server and pass them to `ShopifyScripts`. For analytics-enabled storefronts, +provide `i18n.currency` during initial script rendering so `window.Shopify.currency.active` exists before consent can +replay buffered events. Prefer the configured market currency when it is authoritative; otherwise query +`localization.country.currency.isoCode` under the same Storefront API `@inContext(country:, language:)` values used by +the request. Keep an explicit failure/mock fallback. Do not use `shop.paymentSettings.currencyCode`, which is the shop +currency rather than necessarily the active market's presentment currency. Do not read env APIs in browser modules. In the ShopifyScripts `analytics` config, `channel` is optional and defaults to `"hydrogen"`. The `"hydrogen"` channel is the one that requires `storefrontId` — it is pulled from the ShopifyScripts `shop` config into analytics payloads. Headless storefronts pass `channel: "headless"` in the ShopifyScripts `analytics` config; the analytics payload then omits `storefrontId`, but `shop.storefrontId` itself is still required (pass `"0"` when the app has no storefront ID). @@ -58,7 +63,7 @@ Publish these from route/page boundaries: - `CART_VIEWED` when the full cart page or cart drawer is viewed. - Wire cart tracking once per cart store lifecycle with `trackCartAnalytics(cartStore)` — React apps use the `useCartAnalytics()` hook from `@shopify/hydrogen/react` and Vue apps use the `useCartAnalytics()` composable from `@shopify/hydrogen/vue`; both call it with the provider's cart store and clean up on unmount. The tracker subscribes to the cart store itself, skips pending/revalidating/note updates, publishes cart delta events on confirmed cart changes, and returns an unsubscribe function. Call it from a client-only effect (`useEffect` / `onMounted`), never at cart-store creation time — it throws when `window.Shopify.analytics` is missing (SSR). Do not manually publish cart delta events. -The bus defaults `shop` from the top-level `shop` config passed to ShopifyScripts; pass `shop` in an event payload only when intentionally overriding that configured value. Shopify analytics reads language and currency from `window.Shopify.locale` and `window.Shopify.currency.active`. +The bus defaults `shop` from the top-level `shop` config passed to ShopifyScripts; pass `shop` in an event payload only when intentionally overriding that configured value. Shopify analytics reads language and currency from `window.Shopify.locale` and `window.Shopify.currency.active`. Cart tracking may synchronize the currency after cart data arrives, but it is not initial currency setup. Required product analytics fields include Shopify Product GID, ProductVariant GID when available, title, price, vendor, quantity, and variant title. @@ -78,3 +83,4 @@ Required product analytics fields include Shopify Product GID, ProductVariant GI - Confirmed cart data changes flow through `trackCartAnalytics(cartStore)` (React/Vue bindings: `useCartAnalytics()`), and the cart query includes `updatedAt`. - Consent-denied visitors do not deliver destination events. - No browser module reads private or server-only env variables. +- Before and after consent, `window.Shopify.currency.active` matches the resolved market without requiring a cart mutation. diff --git a/packages/hydrogen/skills/hydrogen-setup/references/analytics.md b/packages/hydrogen/skills/hydrogen-setup/references/analytics.md index c44cccc8b0..1fe1566870 100644 --- a/packages/hydrogen/skills/hydrogen-setup/references/analytics.md +++ b/packages/hydrogen/skills/hydrogen-setup/references/analytics.md @@ -23,7 +23,7 @@ **Prerequisites:** - A storefront built on `@shopify/hydrogen` with the request interceptors already wired (`handleShopifyRoutes` and `handleShopifyRedirects`). The analytics bus depends on the SFAPI proxy so the browser can observe same-origin Storefront API responses for session cookies. Without the proxy, analytics falls back to deprecated JavaScript-visible cookies and should be treated as incomplete. If you have not installed the interceptors yet, install them first with the local `hydrogen-request-handlers` skill. -- Shopify runtime scripts rendered from the root/document head. Use `ShopifyScripts` from your framework binding if it exports one, or `getShopifyScriptTags()` / `renderShopifyScriptTags()` from core in other framework heads. Pass `{country, language, currency?}` as `i18n`; pass `{shopId: env.SHOP_ID, storefrontId: env.PUBLIC_STOREFRONT_ID ?? "0", myshopifyDomain: env.PUBLIC_STORE_DOMAIN}` as `shop`. Resolve both on the server, declare them as consts annotated with the `ShopifyScriptsShop` / `ShopifyScriptsI18n` types from `@shopify/hydrogen` (so wrong or missing fields fail typecheck where they are built), and serialize them into ShopifyScripts. ShopifyScripts creates `window.Shopify.analytics` by default and exposes the permanent domain as `window.Shopify.shop`. Analytics consent config does not accept `country` or `language`. +- Shopify runtime scripts rendered from the root/document head. Use `ShopifyScripts` from your framework binding if it exports one, or `getShopifyScriptTags()` / `renderShopifyScriptTags()` from core in other framework heads. Pass `{country, language, currency}` as `i18n`; pass `{shopId: env.SHOP_ID, storefrontId: env.PUBLIC_STOREFRONT_ID ?? "0", myshopifyDomain: env.PUBLIC_STORE_DOMAIN}` as `shop`. Resolve both on the server, declare them as consts annotated with the `ShopifyScriptsShop` / `ShopifyScriptsI18nWithCurrency` types from `@shopify/hydrogen` (so wrong or missing fields fail typecheck where they are built), and serialize them into ShopifyScripts. ShopifyScripts creates `window.Shopify.analytics` by default and exposes the permanent domain as `window.Shopify.shop`. Analytics consent config does not accept `country` or `language`. - A client-side lifecycle hook in your framework (route-change effect, navigation event, `