@shopify/hydrogen 2025.4.0 → 2025.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ var react$1 = require('@remix-run/react');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
  var hydrogenReact = require('@shopify/hydrogen-react');
7
7
  var cookie = require('worktop/cookie');
8
+ var serverRuntime = require('@remix-run/server-runtime');
8
9
  var cspBuilder = require('content-security-policy-builder');
9
10
  require('url');
10
11
  require('path');
@@ -170,7 +171,62 @@ var AnalyticsEvent = {
170
171
  // Custom
171
172
  CUSTOM_EVENT: `custom_`
172
173
  };
173
- var CONSENT_API = "https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.1/consent-tracking-api.js";
174
+
175
+ // src/constants.ts
176
+ var STOREFRONT_REQUEST_GROUP_ID_HEADER = "Custom-Storefront-Request-Group-ID";
177
+ var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
178
+ var SDK_VARIANT_HEADER = "X-SDK-Variant";
179
+ var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
180
+ var SDK_VERSION_HEADER = "X-SDK-Version";
181
+ var SHOPIFY_CLIENT_IP_HEADER = "X-Shopify-Client-IP";
182
+ var SHOPIFY_CLIENT_IP_SIG_HEADER = "X-Shopify-Client-IP-Sig";
183
+ var HYDROGEN_SFAPI_PROXY_KEY = "_sfapi_proxy";
184
+ var HYDROGEN_SERVER_TRACKING_KEY = "_server_tracking";
185
+
186
+ // src/utils/server-timing.ts
187
+ function buildServerTimingHeader(values) {
188
+ return Object.entries(values).map(([key, value]) => value ? `${key};desc=${value}` : void 0).filter(Boolean).join(", ");
189
+ }
190
+ function appendServerTimingHeader(response, values) {
191
+ const header = typeof values === "string" ? values : buildServerTimingHeader(values);
192
+ if (header) {
193
+ response.headers.append("Server-Timing", header);
194
+ }
195
+ }
196
+ var trackedTimings = ["_y", "_s", "_cmp"];
197
+ function extractServerTimingHeader(serverTimingHeader) {
198
+ const values = {};
199
+ if (!serverTimingHeader) return values;
200
+ const re = new RegExp(
201
+ `\\b(${trackedTimings.join("|")});desc="?([^",]+)"?`,
202
+ "g"
203
+ );
204
+ let match;
205
+ while ((match = re.exec(serverTimingHeader)) !== null) {
206
+ values[match[1]] = match[2];
207
+ }
208
+ return values;
209
+ }
210
+ function hasServerTimingInNavigationEntry(key) {
211
+ if (typeof window === "undefined") return false;
212
+ try {
213
+ const navigationEntry = window.performance.getEntriesByType(
214
+ "navigation"
215
+ )[0];
216
+ return !!navigationEntry?.serverTiming?.some((entry) => entry.name === key);
217
+ } catch (e) {
218
+ return false;
219
+ }
220
+ }
221
+ function isSfapiProxyEnabled() {
222
+ return hasServerTimingInNavigationEntry(HYDROGEN_SFAPI_PROXY_KEY);
223
+ }
224
+ function hasServerReturnedTrackingValues() {
225
+ return hasServerTimingInNavigationEntry(HYDROGEN_SERVER_TRACKING_KEY);
226
+ }
227
+
228
+ // src/customer-privacy/ShopifyCustomerPrivacy.tsx
229
+ var CONSENT_API = "https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.2/consent-tracking-api.js";
174
230
  var CONSENT_API_WITH_BANNER = "https://cdn.shopify.com/shopifycloud/privacy-banner/storefront-banner.js";
175
231
  function logMissingConfig(fieldName) {
176
232
  console.error(
@@ -182,19 +238,34 @@ function useCustomerPrivacy(props) {
182
238
  withPrivacyBanner = false,
183
239
  onVisitorConsentCollected,
184
240
  onReady,
185
- ...consentConfig
241
+ checkoutDomain,
242
+ storefrontAccessToken,
243
+ country,
244
+ locale,
245
+ sameDomainForStorefrontApi
186
246
  } = props;
247
+ const hasSfapiProxy = react.useMemo(
248
+ () => sameDomainForStorefrontApi ?? isSfapiProxyEnabled(),
249
+ [sameDomainForStorefrontApi]
250
+ );
251
+ const fetchTrackingValuesFromBrowser = react.useMemo(
252
+ () => hasSfapiProxy && !hasServerReturnedTrackingValues(),
253
+ [hasSfapiProxy]
254
+ );
255
+ const cookiesReady = hydrogenReact.useShopifyCookies({
256
+ fetchTrackingValues: fetchTrackingValuesFromBrowser,
257
+ storefrontAccessToken,
258
+ ignoreDeprecatedCookies: true
259
+ });
260
+ const initialTrackingValues = react.useMemo(hydrogenReact.getTrackingValues, [cookiesReady]);
261
+ const { revalidate } = react$1.useRevalidator();
187
262
  hydrogenReact.useLoadScript(withPrivacyBanner ? CONSENT_API_WITH_BANNER : CONSENT_API, {
188
263
  attributes: {
189
264
  id: "customer-privacy-api"
190
265
  }
191
266
  });
192
- const { observing, setLoaded } = useApisLoaded({
193
- withPrivacyBanner,
194
- onLoaded: onReady
195
- });
267
+ const { observing, setLoaded, apisLoaded } = useApisLoaded({ withPrivacyBanner });
196
268
  const config = react.useMemo(() => {
197
- const { checkoutDomain, storefrontAccessToken } = consentConfig;
198
269
  if (!checkoutDomain) logMissingConfig("checkoutDomain");
199
270
  if (!storefrontAccessToken) logMissingConfig("storefrontAccessToken");
200
271
  if (storefrontAccessToken.startsWith("shpat_") || storefrontAccessToken.length !== 32) {
@@ -202,18 +273,50 @@ function useCustomerPrivacy(props) {
202
273
  `[h2:error:useCustomerPrivacy] It looks like you passed a private access token, make sure to use the public token`
203
274
  );
204
275
  }
276
+ const commonAncestorDomain = parseStoreDomain(checkoutDomain);
277
+ const sfapiDomain = (
278
+ // Check if standard route proxy is enabled in Hydrogen server
279
+ // to use it instead of doing a cross-origin request to checkout.
280
+ hasSfapiProxy && typeof window !== "undefined" ? window.location.host : checkoutDomain
281
+ );
205
282
  const config2 = {
206
- checkoutRootDomain: checkoutDomain,
283
+ // This domain is used to send requests to SFAPI for setting and getting consent.
284
+ checkoutRootDomain: sfapiDomain,
285
+ // Prefix with a dot to ensure this domain is different from checkoutRootDomain.
286
+ // This will ensure old cookies are set for a cross-subdomain checkout setup
287
+ // so that we keep backward compatibility until new cookies are rolled out.
288
+ // Once consent-tracking-api is updated to not rely on cookies anymore, we can remove this.
289
+ storefrontRootDomain: commonAncestorDomain ? "." + commonAncestorDomain : void 0,
207
290
  storefrontAccessToken,
208
- storefrontRootDomain: parseStoreDomain(checkoutDomain),
209
- country: consentConfig.country,
210
- locale: consentConfig.locale
291
+ country,
292
+ locale
211
293
  };
212
294
  return config2;
213
- }, [consentConfig, parseStoreDomain, logMissingConfig]);
295
+ }, [
296
+ logMissingConfig,
297
+ checkoutDomain,
298
+ storefrontAccessToken,
299
+ country,
300
+ locale
301
+ ]);
214
302
  react.useEffect(() => {
215
303
  const consentCollectedHandler = (event) => {
304
+ const latestTrackingValues = hydrogenReact.getTrackingValues();
305
+ if (initialTrackingValues.visitToken !== latestTrackingValues.visitToken || initialTrackingValues.uniqueToken !== latestTrackingValues.uniqueToken) {
306
+ revalidate();
307
+ }
216
308
  if (onVisitorConsentCollected) {
309
+ const customerPrivacy = getCustomerPrivacy();
310
+ if (customerPrivacy?.shouldShowBanner()) {
311
+ const consentValues = customerPrivacy.currentVisitorConsent();
312
+ if (consentValues) {
313
+ const NO_VALUE = "";
314
+ const noInteraction = consentValues.marketing === NO_VALUE && consentValues.analytics === NO_VALUE && consentValues.preferences === NO_VALUE;
315
+ if (noInteraction) {
316
+ return;
317
+ }
318
+ }
319
+ }
217
320
  onVisitorConsentCollected(event.detail);
218
321
  }
219
322
  };
@@ -239,14 +342,11 @@ function useCustomerPrivacy(props) {
239
342
  },
240
343
  set(value) {
241
344
  if (typeof value === "object" && value !== null && "showPreferences" in value && "loadBanner" in value) {
242
- const privacyBanner = value;
243
- privacyBanner.loadBanner(config);
244
345
  customPrivacyBanner = overridePrivacyBannerMethods({
245
- privacyBanner,
346
+ privacyBanner: value,
246
347
  config
247
348
  });
248
349
  setLoaded.privacyBanner();
249
- emitCustomerPrivacyApiLoaded();
250
350
  }
251
351
  }
252
352
  };
@@ -280,6 +380,8 @@ function useCustomerPrivacy(props) {
280
380
  const customerPrivacy = value2;
281
381
  customCustomerPrivacy = {
282
382
  ...customerPrivacy,
383
+ // Note: this method is not used by the privacy-banner,
384
+ // it bundles its own setTrackingConsent.
283
385
  setTrackingConsent: overrideCustomerPrivacySetTrackingConsent(
284
386
  { customerPrivacy, config }
285
387
  )
@@ -289,7 +391,6 @@ function useCustomerPrivacy(props) {
289
391
  customerPrivacy: customCustomerPrivacy
290
392
  };
291
393
  setLoaded.customerPrivacy();
292
- emitCustomerPrivacyApiLoaded();
293
394
  }
294
395
  }
295
396
  });
@@ -301,6 +402,24 @@ function useCustomerPrivacy(props) {
301
402
  overrideCustomerPrivacySetTrackingConsent,
302
403
  setLoaded.customerPrivacy
303
404
  ]);
405
+ react.useEffect(() => {
406
+ if (!apisLoaded || !cookiesReady) return;
407
+ const customerPrivacy = getCustomerPrivacy();
408
+ if (customerPrivacy && !customerPrivacy.cachedConsent) {
409
+ const trackingValues = hydrogenReact.getTrackingValues();
410
+ if (trackingValues.consent) {
411
+ customerPrivacy.cachedConsent = trackingValues.consent;
412
+ }
413
+ }
414
+ if (withPrivacyBanner) {
415
+ const privacyBanner = getPrivacyBanner();
416
+ if (privacyBanner) {
417
+ privacyBanner.loadBanner(config);
418
+ }
419
+ }
420
+ emitCustomerPrivacyApiLoaded();
421
+ onReady?.();
422
+ }, [apisLoaded, cookiesReady]);
304
423
  const result = {
305
424
  customerPrivacy: getCustomerPrivacy()
306
425
  };
@@ -316,15 +435,12 @@ function emitCustomerPrivacyApiLoaded() {
316
435
  const event = new CustomEvent("shopifyCustomerPrivacyApiLoaded");
317
436
  document.dispatchEvent(event);
318
437
  }
319
- function useApisLoaded({
320
- withPrivacyBanner,
321
- onLoaded
322
- }) {
438
+ function useApisLoaded({ withPrivacyBanner }) {
323
439
  const observing = react.useRef({ customerPrivacy: false, privacyBanner: false });
324
- const [apisLoaded, setApisLoaded] = react.useState(
440
+ const [apisLoadedArray, setApisLoaded] = react.useState(
325
441
  withPrivacyBanner ? [false, false] : [false]
326
442
  );
327
- const loaded = apisLoaded.every(Boolean);
443
+ const apisLoaded = apisLoadedArray.every(Boolean);
328
444
  const setLoaded = {
329
445
  customerPrivacy: () => {
330
446
  if (withPrivacyBanner) {
@@ -340,16 +456,11 @@ function useApisLoaded({
340
456
  setApisLoaded((prev) => [prev[0], true]);
341
457
  }
342
458
  };
343
- react.useEffect(() => {
344
- if (loaded && onLoaded) {
345
- onLoaded();
346
- }
347
- }, [loaded, onLoaded]);
348
- return { observing, setLoaded };
459
+ return { observing, setLoaded, apisLoaded };
349
460
  }
350
461
  function parseStoreDomain(checkoutDomain) {
351
462
  if (typeof window === "undefined") return;
352
- const host = window.document.location.host;
463
+ const host = window.location.host;
353
464
  const checkoutDomainParts = checkoutDomain.split(".").reverse();
354
465
  const currentDomainParts = host.split(".").reverse();
355
466
  const sameDomainParts = [];
@@ -358,7 +469,7 @@ function parseStoreDomain(checkoutDomain) {
358
469
  sameDomainParts.push(part);
359
470
  }
360
471
  });
361
- return sameDomainParts.reverse().join(".");
472
+ return sameDomainParts.reverse().join(".") || void 0;
362
473
  }
363
474
  function overrideCustomerPrivacySetTrackingConsent({
364
475
  customerPrivacy,
@@ -416,7 +527,7 @@ function getPrivacyBanner() {
416
527
  }
417
528
 
418
529
  // package.json
419
- var version = "2025.4.0";
530
+ var version = "2025.4.2";
420
531
 
421
532
  // src/analytics-manager/ShopifyAnalytics.tsx
422
533
  function getCustomerPrivacyRequired() {
@@ -436,6 +547,7 @@ function ShopifyAnalytics({
436
547
  const { subscribe: subscribe2, register: register2, canTrack } = useAnalytics();
437
548
  const [shopifyReady, setShopifyReady] = react.useState(false);
438
549
  const [privacyReady, setPrivacyReady] = react.useState(false);
550
+ const [collectedConsent, setCollectedConsent] = react.useState("");
439
551
  const init = react.useRef(false);
440
552
  const { checkoutDomain, storefrontAccessToken, language } = consent;
441
553
  const { ready: shopifyAnalyticsReady } = register2("Internal_Shopify_Analytics");
@@ -444,14 +556,31 @@ function ShopifyAnalytics({
444
556
  locale: language,
445
557
  checkoutDomain: !checkoutDomain ? "mock.shop" : checkoutDomain,
446
558
  storefrontAccessToken: !storefrontAccessToken ? "abcdefghijklmnopqrstuvwxyz123456" : storefrontAccessToken,
447
- onVisitorConsentCollected: () => setPrivacyReady(true),
448
- onReady: () => setPrivacyReady(true)
559
+ // If we use privacy banner, we should wait until consent is collected.
560
+ // Otherwise, we can consider privacy ready immediately:
561
+ onReady: () => !consent.withPrivacyBanner && setPrivacyReady(true),
562
+ onVisitorConsentCollected: (consent2) => {
563
+ try {
564
+ setCollectedConsent(JSON.stringify(consent2));
565
+ } catch (e) {
566
+ }
567
+ setPrivacyReady(true);
568
+ }
449
569
  });
570
+ const hasUserConsent = react.useMemo(
571
+ // must be initialized with true to avoid removing cookies too early
572
+ () => privacyReady ? canTrack() : true,
573
+ // Make this value depend on collectedConsent to re-run `canTrack()` when consent changes
574
+ [privacyReady, canTrack, collectedConsent]
575
+ );
450
576
  hydrogenReact.useShopifyCookies({
451
- hasUserConsent: privacyReady ? canTrack() : true,
452
- // must be initialized with true
577
+ hasUserConsent,
453
578
  domain,
454
- checkoutDomain
579
+ checkoutDomain,
580
+ // Already done inside useCustomerPrivacy
581
+ fetchTrackingValues: false,
582
+ // Avoid creating local cookies too early
583
+ ignoreDeprecatedCookies: !privacyReady
455
584
  });
456
585
  react.useEffect(() => {
457
586
  if (init.current) return;
@@ -501,11 +630,11 @@ function prepareBasePageViewPayload(payload) {
501
630
  ...payload.shop,
502
631
  hasUserConsent,
503
632
  ...hydrogenReact.getClientBrowserParameters(),
504
- ccpaEnforced: !customerPrivacy.saleOfDataAllowed(),
505
- gdprEnforced: !(customerPrivacy.marketingAllowed() && customerPrivacy.analyticsProcessingAllowed()),
506
633
  analyticsAllowed: customerPrivacy.analyticsProcessingAllowed(),
507
634
  marketingAllowed: customerPrivacy.marketingAllowed(),
508
- saleOfDataAllowed: customerPrivacy.saleOfDataAllowed()
635
+ saleOfDataAllowed: customerPrivacy.saleOfDataAllowed(),
636
+ ccpaEnforced: !customerPrivacy.saleOfDataAllowed(),
637
+ gdprEnforced: !(customerPrivacy.marketingAllowed() && customerPrivacy.analyticsProcessingAllowed())
509
638
  };
510
639
  return eventPayload;
511
640
  }
@@ -938,11 +1067,11 @@ function AnalyticsProvider({
938
1067
  shop: shopProp = null,
939
1068
  cookieDomain
940
1069
  }) {
941
- const listenerSet = react.useRef(false);
942
1070
  const { shop } = useShopAnalytics(shopProp);
943
1071
  const [analyticsLoaded, setAnalyticsLoaded] = react.useState(
944
1072
  customCanTrack ? true : false
945
1073
  );
1074
+ const [consentCollected, setConsentCollected] = react.useState(false);
946
1075
  const [carts, setCarts] = react.useState({ cart: null, prevCart: null });
947
1076
  const [canTrack, setCanTrack] = react.useState(
948
1077
  customCanTrack ? () => customCanTrack : () => shopifyCanTrack
@@ -1010,21 +1139,21 @@ function AnalyticsProvider({
1010
1139
  children,
1011
1140
  !!shop && /* @__PURE__ */ jsxRuntime.jsx(AnalyticsPageView, {}),
1012
1141
  !!shop && !!currentCart && /* @__PURE__ */ jsxRuntime.jsx(CartAnalytics, { cart: currentCart, setCarts }),
1013
- !!shop && consent.checkoutDomain && /* @__PURE__ */ jsxRuntime.jsx(
1142
+ !!shop && /* @__PURE__ */ jsxRuntime.jsx(
1014
1143
  ShopifyAnalytics,
1015
1144
  {
1016
1145
  consent,
1017
1146
  onReady: () => {
1018
- listenerSet.current = true;
1019
1147
  setAnalyticsLoaded(true);
1020
1148
  setCanTrack(
1021
1149
  customCanTrack ? () => customCanTrack : () => shopifyCanTrack
1022
1150
  );
1151
+ setConsentCollected(true);
1023
1152
  },
1024
1153
  domain: cookieDomain
1025
1154
  }
1026
1155
  ),
1027
- !!shop && /* @__PURE__ */ jsxRuntime.jsx(PerfKit, { shop })
1156
+ !!shop && consentCollected && /* @__PURE__ */ jsxRuntime.jsx(PerfKit, { shop })
1028
1157
  ] });
1029
1158
  }
1030
1159
  function useAnalytics() {
@@ -1103,6 +1232,21 @@ function getDebugHeaders(request) {
1103
1232
  purpose: request ? getHeader(request, "purpose") : void 0
1104
1233
  };
1105
1234
  }
1235
+ var SFAPI_RE = /^\/api\/(unstable|2\d{3}-\d{2})\/graphql\.json$/;
1236
+ var getSafePathname = (url) => {
1237
+ try {
1238
+ return new URL(url, "http://e.c").pathname;
1239
+ } catch {
1240
+ return "/";
1241
+ }
1242
+ };
1243
+ function extractHeaders(extract, keys) {
1244
+ return keys.reduce((acc, key) => {
1245
+ const forwardedValue = extract(key);
1246
+ if (forwardedValue) acc.push([key, forwardedValue]);
1247
+ return acc;
1248
+ }, []);
1249
+ }
1106
1250
 
1107
1251
  // src/utils/callsites.ts
1108
1252
  function withSyncStack(promise, options = {}) {
@@ -1472,13 +1616,16 @@ async function runWithCache(cacheKey, actionFn, {
1472
1616
  }
1473
1617
 
1474
1618
  // src/cache/server-fetch.ts
1619
+ var excludedHeaders = ["set-cookie", "server-timing"];
1475
1620
  function toSerializableResponse(body, response) {
1476
1621
  return [
1477
1622
  body,
1478
1623
  {
1479
1624
  status: response.status,
1480
1625
  statusText: response.statusText,
1481
- headers: Array.from(response.headers.entries())
1626
+ headers: [...response.headers].filter(
1627
+ ([key]) => !excludedHeaders.includes(key.toLowerCase())
1628
+ )
1482
1629
  }
1483
1630
  ];
1484
1631
  }
@@ -1491,7 +1638,8 @@ async function fetchWithServerCache(url, requestInit, {
1491
1638
  cacheKey = [url, requestInit],
1492
1639
  shouldCacheResponse,
1493
1640
  waitUntil,
1494
- debugInfo
1641
+ debugInfo,
1642
+ onRawHeaders
1495
1643
  }) {
1496
1644
  if (!cacheOptions && (!requestInit.method || requestInit.method === "GET")) {
1497
1645
  cacheOptions = CacheShort();
@@ -1500,6 +1648,7 @@ async function fetchWithServerCache(url, requestInit, {
1500
1648
  cacheKey,
1501
1649
  async () => {
1502
1650
  const response = await fetch(url, requestInit);
1651
+ onRawHeaders?.(response.headers);
1503
1652
  if (!response.ok) {
1504
1653
  return response;
1505
1654
  }
@@ -1722,13 +1871,6 @@ var cartSetIdDefault = (cookieOptions) => {
1722
1871
  };
1723
1872
  };
1724
1873
 
1725
- // src/constants.ts
1726
- var STOREFRONT_REQUEST_GROUP_ID_HEADER = "Custom-Storefront-Request-Group-ID";
1727
- var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
1728
- var SDK_VARIANT_HEADER = "X-SDK-Variant";
1729
- var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
1730
- var SDK_VERSION_HEADER = "X-SDK-Version";
1731
-
1732
1874
  // src/utils/uuid.ts
1733
1875
  function generateUUID() {
1734
1876
  if (typeof crypto !== "undefined" && !!crypto.randomUUID) {
@@ -1739,7 +1881,7 @@ function generateUUID() {
1739
1881
  }
1740
1882
 
1741
1883
  // src/version.ts
1742
- var LIB_VERSION = "2025.4.0";
1884
+ var LIB_VERSION = "2025.4.2";
1743
1885
 
1744
1886
  // src/utils/graphql.ts
1745
1887
  function minifyQuery(string) {
@@ -1900,16 +2042,34 @@ function createStorefrontClient(options) {
1900
2042
  contentType: "json",
1901
2043
  buyerIp: storefrontHeaders?.buyerIp || ""
1902
2044
  });
2045
+ if (storefrontHeaders?.buyerIp) {
2046
+ defaultHeaders[SHOPIFY_CLIENT_IP_HEADER] = storefrontHeaders.buyerIp;
2047
+ }
2048
+ if (storefrontHeaders?.buyerIpSig) {
2049
+ defaultHeaders[SHOPIFY_CLIENT_IP_SIG_HEADER] = storefrontHeaders.buyerIpSig;
2050
+ }
1903
2051
  defaultHeaders[STOREFRONT_REQUEST_GROUP_ID_HEADER] = storefrontHeaders?.requestGroupId || generateUUID();
1904
2052
  if (storefrontId) defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_ID_HEADER] = storefrontId;
1905
2053
  defaultHeaders["user-agent"] = `Hydrogen ${LIB_VERSION}`;
1906
- if (storefrontHeaders && storefrontHeaders.cookie) {
1907
- const cookies = hydrogenReact.getShopifyCookies(storefrontHeaders.cookie ?? "");
1908
- if (cookies[hydrogenReact.SHOPIFY_Y])
1909
- defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_Y_HEADER] = cookies[hydrogenReact.SHOPIFY_Y];
1910
- if (cookies[hydrogenReact.SHOPIFY_S])
1911
- defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_S_HEADER] = cookies[hydrogenReact.SHOPIFY_S];
2054
+ const requestCookie = storefrontHeaders?.cookie ?? "";
2055
+ if (requestCookie) defaultHeaders["cookie"] = requestCookie;
2056
+ let uniqueToken;
2057
+ let visitToken;
2058
+ if (!/\b_shopify_(analytics|marketing)=/.test(requestCookie)) {
2059
+ const legacyUniqueToken = requestCookie.match(/\b_shopify_y=([^;]+)/)?.[1];
2060
+ const legacyVisitToken = requestCookie.match(/\b_shopify_s=([^;]+)/)?.[1];
2061
+ if (legacyUniqueToken) {
2062
+ defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_Y_HEADER] = legacyUniqueToken;
2063
+ }
2064
+ if (legacyVisitToken) {
2065
+ defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_S_HEADER] = legacyVisitToken;
2066
+ }
2067
+ uniqueToken = legacyUniqueToken ?? generateUUID();
2068
+ visitToken = legacyVisitToken ?? generateUUID();
2069
+ defaultHeaders[hydrogenReact.SHOPIFY_UNIQUE_TOKEN_HEADER] = uniqueToken;
2070
+ defaultHeaders[hydrogenReact.SHOPIFY_VISIT_TOKEN_HEADER] = visitToken;
1912
2071
  }
2072
+ let collectedSubrequestHeaders;
1913
2073
  const cacheKeyHeader = JSON.stringify({
1914
2074
  "content-type": defaultHeaders["content-type"],
1915
2075
  "user-agent": defaultHeaders["user-agent"],
@@ -1971,6 +2131,13 @@ function createStorefrontClient(options) {
1971
2131
  stackInfo,
1972
2132
  graphql: graphqlData,
1973
2133
  purpose: storefrontHeaders?.purpose
2134
+ },
2135
+ onRawHeaders: (headers2) => {
2136
+ collectedSubrequestHeaders ??= {
2137
+ // `getSetCookie` may not be available in all environments (e.g., classic Remix compiler)
2138
+ setCookie: typeof headers2.getSetCookie === "function" ? headers2.getSetCookie() : [],
2139
+ serverTiming: headers2.get("server-timing") ?? ""
2140
+ };
1974
2141
  }
1975
2142
  });
1976
2143
  const errorOptions = {
@@ -2069,9 +2236,90 @@ function createStorefrontClient(options) {
2069
2236
  generateCacheControlHeader,
2070
2237
  getPublicTokenHeaders,
2071
2238
  getPrivateTokenHeaders,
2239
+ getHeaders: () => ({ ...defaultHeaders }),
2072
2240
  getShopifyDomain,
2073
2241
  getApiUrl: getStorefrontApiUrl,
2074
- i18n: i18n ?? defaultI18n
2242
+ i18n: i18n ?? defaultI18n,
2243
+ /**
2244
+ * Checks if the request is targeting the Storefront API endpoint.
2245
+ */
2246
+ isStorefrontApiUrl(request) {
2247
+ return SFAPI_RE.test(getSafePathname(request.url ?? ""));
2248
+ },
2249
+ /**
2250
+ * Forwards the request to the Storefront API.
2251
+ */
2252
+ async forward(request, options2) {
2253
+ const forwardedHeaders = new Headers([
2254
+ // Forward only a selected set of headers to the Storefront API
2255
+ // to avoid getting 403 errors due to unexpected headers.
2256
+ ...extractHeaders(
2257
+ (key) => request.headers.get(key),
2258
+ [
2259
+ "accept",
2260
+ "accept-encoding",
2261
+ "accept-language",
2262
+ // Access-Control headers are used for CORS preflight requests.
2263
+ "access-control-request-headers",
2264
+ "access-control-request-method",
2265
+ "content-type",
2266
+ "content-length",
2267
+ "cookie",
2268
+ "origin",
2269
+ "referer",
2270
+ "user-agent",
2271
+ STOREFRONT_ACCESS_TOKEN_HEADER,
2272
+ hydrogenReact.SHOPIFY_UNIQUE_TOKEN_HEADER,
2273
+ hydrogenReact.SHOPIFY_VISIT_TOKEN_HEADER
2274
+ ]
2275
+ ),
2276
+ // Add some headers to help with geolocalization and debugging
2277
+ ...extractHeaders(
2278
+ (key) => defaultHeaders[key],
2279
+ [
2280
+ SHOPIFY_CLIENT_IP_HEADER,
2281
+ SHOPIFY_CLIENT_IP_SIG_HEADER,
2282
+ hydrogenReact.SHOPIFY_STOREFRONT_ID_HEADER,
2283
+ STOREFRONT_REQUEST_GROUP_ID_HEADER
2284
+ ]
2285
+ )
2286
+ ]);
2287
+ if (storefrontHeaders?.buyerIp) {
2288
+ forwardedHeaders.set("x-forwarded-for", storefrontHeaders.buyerIp);
2289
+ }
2290
+ const storefrontApiVersion = options2?.storefrontApiVersion ?? getSafePathname(request.url).match(SFAPI_RE)?.[1];
2291
+ const sfapiResponse = await fetch(
2292
+ getStorefrontApiUrl({ storefrontApiVersion }),
2293
+ {
2294
+ method: request.method,
2295
+ body: request.body,
2296
+ headers: forwardedHeaders
2297
+ }
2298
+ );
2299
+ return new Response(sfapiResponse.body, sfapiResponse);
2300
+ },
2301
+ setCollectedSubrequestHeaders: (response) => {
2302
+ if (collectedSubrequestHeaders) {
2303
+ for (const value of collectedSubrequestHeaders.setCookie) {
2304
+ response.headers.append("Set-Cookie", value);
2305
+ }
2306
+ }
2307
+ const serverTiming = extractServerTimingHeader(
2308
+ collectedSubrequestHeaders?.serverTiming
2309
+ );
2310
+ const isDocumentResponse = response.headers.get("content-type")?.startsWith("text/html");
2311
+ const fallbackValues = isDocumentResponse ? { _y: uniqueToken, _s: visitToken } : void 0;
2312
+ appendServerTimingHeader(response, {
2313
+ ...fallbackValues,
2314
+ ...serverTiming
2315
+ });
2316
+ if (isDocumentResponse && collectedSubrequestHeaders && // _shopify_essential cookie is always set, but we need more than that
2317
+ collectedSubrequestHeaders.setCookie.length > 1 && serverTiming?._y && serverTiming?._s && serverTiming?._cmp) {
2318
+ appendServerTimingHeader(response, {
2319
+ [HYDROGEN_SERVER_TRACKING_KEY]: "1"
2320
+ });
2321
+ }
2322
+ }
2075
2323
  }
2076
2324
  };
2077
2325
  }
@@ -2868,7 +3116,8 @@ function createCartHandler(options) {
2868
3116
  storefront,
2869
3117
  customerAccount,
2870
3118
  cartQueryFragment,
2871
- cartMutateFragment
3119
+ cartMutateFragment,
3120
+ buyerIdentity
2872
3121
  } = options;
2873
3122
  let cartId = _getCartId();
2874
3123
  const getCartId = () => cartId || _getCartId();
@@ -2880,6 +3129,10 @@ function createCartHandler(options) {
2880
3129
  };
2881
3130
  const _cartCreate = cartCreateDefault(mutateOptions);
2882
3131
  const cartCreate = async function(...args) {
3132
+ args[0].buyerIdentity = {
3133
+ ...buyerIdentity,
3134
+ ...args[0].buyerIdentity
3135
+ };
2883
3136
  const result = await _cartCreate(...args);
2884
3137
  cartId = result?.cart?.id;
2885
3138
  return result;
@@ -2903,7 +3156,7 @@ function createCartHandler(options) {
2903
3156
  sellingPlanId: line.sellingPlanId
2904
3157
  };
2905
3158
  });
2906
- return cartId || optionalParams?.cartId ? await cartLinesAddDefault(mutateOptions)(lines, optionalParams) : await cartCreate({ lines }, optionalParams);
3159
+ return cartId || optionalParams?.cartId ? await cartLinesAddDefault(mutateOptions)(lines, optionalParams) : await cartCreate({ lines, buyerIdentity }, optionalParams);
2907
3160
  },
2908
3161
  updateLines: cartLinesUpdateDefault(mutateOptions),
2909
3162
  removeLines: cartLinesRemoveDefault(mutateOptions),
@@ -2919,11 +3172,11 @@ function createCartHandler(options) {
2919
3172
  optionalParams
2920
3173
  ) : await cartCreate({ giftCardCodes }, optionalParams);
2921
3174
  },
2922
- updateBuyerIdentity: async (buyerIdentity, optionalParams) => {
3175
+ updateBuyerIdentity: async (buyerIdentity2, optionalParams) => {
2923
3176
  return cartId || optionalParams?.cartId ? await cartBuyerIdentityUpdateDefault(mutateOptions)(
2924
- buyerIdentity,
3177
+ buyerIdentity2,
2925
3178
  optionalParams
2926
- ) : await cartCreate({ buyerIdentity }, optionalParams);
3179
+ ) : await cartCreate({ buyerIdentity: buyerIdentity2 }, optionalParams);
2927
3180
  },
2928
3181
  updateNote: async (note, optionalParams) => {
2929
3182
  return cartId || optionalParams?.cartId ? await cartNoteUpdateDefault(mutateOptions)(note, optionalParams) : await cartCreate({ note }, optionalParams);
@@ -3818,7 +4071,8 @@ function createHydrogenContext(options) {
3818
4071
  logErrors,
3819
4072
  storefront: storefrontOptions = {},
3820
4073
  customerAccount: customerAccountOptions,
3821
- cart: cartOptions = {}
4074
+ cart: cartOptions = {},
4075
+ buyerIdentity
3822
4076
  } = options;
3823
4077
  if (!session) {
3824
4078
  console.warn(
@@ -3868,6 +4122,7 @@ function createHydrogenContext(options) {
3868
4122
  cartQueryFragment: cartOptions.queryFragment,
3869
4123
  cartMutateFragment: cartOptions.mutateFragment,
3870
4124
  customMethods: cartOptions.customMethods,
4125
+ buyerIdentity,
3871
4126
  // defaults
3872
4127
  storefront,
3873
4128
  customerAccount
@@ -3885,8 +4140,63 @@ function getStorefrontHeaders(request) {
3885
4140
  return {
3886
4141
  requestGroupId: getHeader(request, "request-id"),
3887
4142
  buyerIp: getHeader(request, "oxygen-buyer-ip"),
4143
+ buyerIpSig: getHeader(request, "X-Shopify-Client-IP-Sig"),
3888
4144
  cookie: getHeader(request, "cookie"),
3889
- purpose: getHeader(request, "purpose")
4145
+ purpose: getHeader(request, "sec-purpose") || getHeader(request, "purpose")
4146
+ };
4147
+ }
4148
+ function createRequestHandler({
4149
+ build,
4150
+ mode,
4151
+ poweredByHeader = true,
4152
+ getLoadContext,
4153
+ collectTrackingInformation = true,
4154
+ proxyStandardRoutes = true
4155
+ }) {
4156
+ const handleRequest = serverRuntime.createRequestHandler(build, mode);
4157
+ const appendPoweredByHeader = poweredByHeader ? (response) => response.headers.append("powered-by", "Shopify, Hydrogen") : void 0;
4158
+ return async (request) => {
4159
+ const method = request.method;
4160
+ if ((method === "GET" || method === "HEAD") && request.body) {
4161
+ return new Response(`${method} requests cannot have a body`, {
4162
+ status: 400
4163
+ });
4164
+ }
4165
+ const url = new URL(request.url);
4166
+ if (url.pathname.includes("//")) {
4167
+ return new Response(null, {
4168
+ status: 301,
4169
+ headers: {
4170
+ location: url.pathname.replace(/\/+/g, "/")
4171
+ }
4172
+ });
4173
+ }
4174
+ const context = getLoadContext ? await getLoadContext(request) : void 0;
4175
+ const storefront = context?.storefront;
4176
+ if (proxyStandardRoutes) {
4177
+ if (!storefront) {
4178
+ warnOnce(
4179
+ "[h2:createRequestHandler] Storefront instance is required to proxy standard routes."
4180
+ );
4181
+ }
4182
+ if (storefront?.isStorefrontApiUrl(request)) {
4183
+ const response2 = await storefront.forward(request);
4184
+ appendPoweredByHeader?.(response2);
4185
+ return response2;
4186
+ }
4187
+ }
4188
+ const response = await handleRequest(request, context);
4189
+ if (storefront && proxyStandardRoutes) {
4190
+ if (collectTrackingInformation) {
4191
+ storefront.setCollectedSubrequestHeaders(response);
4192
+ }
4193
+ const fetchDest = request.headers.get("sec-fetch-dest");
4194
+ if (fetchDest && fetchDest === "document" || request.headers.get("accept")?.includes("text/html")) {
4195
+ appendServerTimingHeader(response, { [HYDROGEN_SFAPI_PROXY_KEY]: "1" });
4196
+ }
4197
+ }
4198
+ appendPoweredByHeader?.(response);
4199
+ return response;
3890
4200
  };
3891
4201
  }
3892
4202
  var NonceContext = react.createContext(void 0);
@@ -5946,6 +6256,10 @@ Object.defineProperty(exports, "getShopifyCookies", {
5946
6256
  enumerable: true,
5947
6257
  get: function () { return hydrogenReact.getShopifyCookies; }
5948
6258
  });
6259
+ Object.defineProperty(exports, "getTrackingValues", {
6260
+ enumerable: true,
6261
+ get: function () { return hydrogenReact.getTrackingValues; }
6262
+ });
5949
6263
  Object.defineProperty(exports, "isOptionValueCombinationInEncodedVariant", {
5950
6264
  enumerable: true,
5951
6265
  get: function () { return hydrogenReact.isOptionValueCombinationInEncodedVariant; }
@@ -6021,6 +6335,7 @@ exports.createCartHandler = createCartHandler;
6021
6335
  exports.createContentSecurityPolicy = createContentSecurityPolicy;
6022
6336
  exports.createCustomerAccountClient = createCustomerAccountClient;
6023
6337
  exports.createHydrogenContext = createHydrogenContext;
6338
+ exports.createRequestHandler = createRequestHandler;
6024
6339
  exports.createStorefrontClient = createStorefrontClient;
6025
6340
  exports.createWithCache = createWithCache;
6026
6341
  exports.formatAPIResult = formatAPIResult;