@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.
@@ -1,9 +1,10 @@
1
1
  import { createContext, forwardRef, useContext, lazy, useMemo, useEffect, useRef, useState, createElement, Fragment as Fragment$1, Suspense } from 'react';
2
- import { useFetcher, useFetchers, useNavigation, useLocation, useNavigate, Link, useMatches } from '@remix-run/react';
2
+ import { useRevalidator, useFetcher, useFetchers, useNavigation, useLocation, useNavigate, Link, useMatches } from '@remix-run/react';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
- import { useLoadScript, createStorefrontClient as createStorefrontClient$1, SHOPIFY_STOREFRONT_ID_HEADER, getShopifyCookies, SHOPIFY_Y, SHOPIFY_STOREFRONT_Y_HEADER, SHOPIFY_S, SHOPIFY_STOREFRONT_S_HEADER, flattenConnection, RichText as RichText$1, ShopPayButton as ShopPayButton$1, useShopifyCookies, parseGid, sendShopifyAnalytics, AnalyticsEventName, AnalyticsPageType, getClientBrowserParameters } from '@shopify/hydrogen-react';
5
- export { AnalyticsEventName, AnalyticsPageType, ExternalVideo, IMAGE_FRAGMENT, Image, MediaFile, ModelViewer, Money, ShopifySalesChannel, Video, customerAccountApiCustomScalars, decodeEncodedVariant, flattenConnection, getAdjacentAndFirstAvailableVariants, getClientBrowserParameters, getProductOptions, getShopifyCookies, isOptionValueCombinationInEncodedVariant, mapSelectedProductOptionToObject, parseGid, parseMetafield, sendShopifyAnalytics, storefrontApiCustomScalars, useLoadScript, useMoney, useSelectedOptionInUrlParam, useShopifyCookies } from '@shopify/hydrogen-react';
4
+ import { useLoadScript, useShopifyCookies, getTrackingValues, createStorefrontClient as createStorefrontClient$1, SHOPIFY_STOREFRONT_ID_HEADER, SHOPIFY_STOREFRONT_Y_HEADER, SHOPIFY_STOREFRONT_S_HEADER, SHOPIFY_UNIQUE_TOKEN_HEADER, SHOPIFY_VISIT_TOKEN_HEADER, flattenConnection, RichText as RichText$1, ShopPayButton as ShopPayButton$1, parseGid, sendShopifyAnalytics, AnalyticsEventName, AnalyticsPageType, getClientBrowserParameters } from '@shopify/hydrogen-react';
5
+ export { AnalyticsEventName, AnalyticsPageType, ExternalVideo, IMAGE_FRAGMENT, Image, MediaFile, ModelViewer, Money, ShopifySalesChannel, Video, customerAccountApiCustomScalars, decodeEncodedVariant, flattenConnection, getAdjacentAndFirstAvailableVariants, getClientBrowserParameters, getProductOptions, getShopifyCookies, getTrackingValues, isOptionValueCombinationInEncodedVariant, mapSelectedProductOptionToObject, parseGid, parseMetafield, sendShopifyAnalytics, storefrontApiCustomScalars, useLoadScript, useMoney, useSelectedOptionInUrlParam, useShopifyCookies } from '@shopify/hydrogen-react';
6
6
  import { parse, stringify } from 'worktop/cookie';
7
+ import { createRequestHandler as createRequestHandler$1 } from '@remix-run/server-runtime';
7
8
  import cspBuilder from 'content-security-policy-builder';
8
9
 
9
10
  // src/analytics-manager/AnalyticsProvider.tsx
@@ -72,7 +73,62 @@ var AnalyticsEvent = {
72
73
  // Custom
73
74
  CUSTOM_EVENT: `custom_`
74
75
  };
75
- var CONSENT_API = "https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.1/consent-tracking-api.js";
76
+
77
+ // src/constants.ts
78
+ var STOREFRONT_REQUEST_GROUP_ID_HEADER = "Custom-Storefront-Request-Group-ID";
79
+ var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
80
+ var SDK_VARIANT_HEADER = "X-SDK-Variant";
81
+ var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
82
+ var SDK_VERSION_HEADER = "X-SDK-Version";
83
+ var SHOPIFY_CLIENT_IP_HEADER = "X-Shopify-Client-IP";
84
+ var SHOPIFY_CLIENT_IP_SIG_HEADER = "X-Shopify-Client-IP-Sig";
85
+ var HYDROGEN_SFAPI_PROXY_KEY = "_sfapi_proxy";
86
+ var HYDROGEN_SERVER_TRACKING_KEY = "_server_tracking";
87
+
88
+ // src/utils/server-timing.ts
89
+ function buildServerTimingHeader(values) {
90
+ return Object.entries(values).map(([key, value]) => value ? `${key};desc=${value}` : void 0).filter(Boolean).join(", ");
91
+ }
92
+ function appendServerTimingHeader(response, values) {
93
+ const header = typeof values === "string" ? values : buildServerTimingHeader(values);
94
+ if (header) {
95
+ response.headers.append("Server-Timing", header);
96
+ }
97
+ }
98
+ var trackedTimings = ["_y", "_s", "_cmp"];
99
+ function extractServerTimingHeader(serverTimingHeader) {
100
+ const values = {};
101
+ if (!serverTimingHeader) return values;
102
+ const re = new RegExp(
103
+ `\\b(${trackedTimings.join("|")});desc="?([^",]+)"?`,
104
+ "g"
105
+ );
106
+ let match;
107
+ while ((match = re.exec(serverTimingHeader)) !== null) {
108
+ values[match[1]] = match[2];
109
+ }
110
+ return values;
111
+ }
112
+ function hasServerTimingInNavigationEntry(key) {
113
+ if (typeof window === "undefined") return false;
114
+ try {
115
+ const navigationEntry = window.performance.getEntriesByType(
116
+ "navigation"
117
+ )[0];
118
+ return !!navigationEntry?.serverTiming?.some((entry) => entry.name === key);
119
+ } catch (e) {
120
+ return false;
121
+ }
122
+ }
123
+ function isSfapiProxyEnabled() {
124
+ return hasServerTimingInNavigationEntry(HYDROGEN_SFAPI_PROXY_KEY);
125
+ }
126
+ function hasServerReturnedTrackingValues() {
127
+ return hasServerTimingInNavigationEntry(HYDROGEN_SERVER_TRACKING_KEY);
128
+ }
129
+
130
+ // src/customer-privacy/ShopifyCustomerPrivacy.tsx
131
+ var CONSENT_API = "https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.2/consent-tracking-api.js";
76
132
  var CONSENT_API_WITH_BANNER = "https://cdn.shopify.com/shopifycloud/privacy-banner/storefront-banner.js";
77
133
  function logMissingConfig(fieldName) {
78
134
  console.error(
@@ -84,19 +140,34 @@ function useCustomerPrivacy(props) {
84
140
  withPrivacyBanner = false,
85
141
  onVisitorConsentCollected,
86
142
  onReady,
87
- ...consentConfig
143
+ checkoutDomain,
144
+ storefrontAccessToken,
145
+ country,
146
+ locale,
147
+ sameDomainForStorefrontApi
88
148
  } = props;
149
+ const hasSfapiProxy = useMemo(
150
+ () => sameDomainForStorefrontApi ?? isSfapiProxyEnabled(),
151
+ [sameDomainForStorefrontApi]
152
+ );
153
+ const fetchTrackingValuesFromBrowser = useMemo(
154
+ () => hasSfapiProxy && !hasServerReturnedTrackingValues(),
155
+ [hasSfapiProxy]
156
+ );
157
+ const cookiesReady = useShopifyCookies({
158
+ fetchTrackingValues: fetchTrackingValuesFromBrowser,
159
+ storefrontAccessToken,
160
+ ignoreDeprecatedCookies: true
161
+ });
162
+ const initialTrackingValues = useMemo(getTrackingValues, [cookiesReady]);
163
+ const { revalidate } = useRevalidator();
89
164
  useLoadScript(withPrivacyBanner ? CONSENT_API_WITH_BANNER : CONSENT_API, {
90
165
  attributes: {
91
166
  id: "customer-privacy-api"
92
167
  }
93
168
  });
94
- const { observing, setLoaded } = useApisLoaded({
95
- withPrivacyBanner,
96
- onLoaded: onReady
97
- });
169
+ const { observing, setLoaded, apisLoaded } = useApisLoaded({ withPrivacyBanner });
98
170
  const config = useMemo(() => {
99
- const { checkoutDomain, storefrontAccessToken } = consentConfig;
100
171
  if (!checkoutDomain) logMissingConfig("checkoutDomain");
101
172
  if (!storefrontAccessToken) logMissingConfig("storefrontAccessToken");
102
173
  if (storefrontAccessToken.startsWith("shpat_") || storefrontAccessToken.length !== 32) {
@@ -104,18 +175,50 @@ function useCustomerPrivacy(props) {
104
175
  `[h2:error:useCustomerPrivacy] It looks like you passed a private access token, make sure to use the public token`
105
176
  );
106
177
  }
178
+ const commonAncestorDomain = parseStoreDomain(checkoutDomain);
179
+ const sfapiDomain = (
180
+ // Check if standard route proxy is enabled in Hydrogen server
181
+ // to use it instead of doing a cross-origin request to checkout.
182
+ hasSfapiProxy && typeof window !== "undefined" ? window.location.host : checkoutDomain
183
+ );
107
184
  const config2 = {
108
- checkoutRootDomain: checkoutDomain,
185
+ // This domain is used to send requests to SFAPI for setting and getting consent.
186
+ checkoutRootDomain: sfapiDomain,
187
+ // Prefix with a dot to ensure this domain is different from checkoutRootDomain.
188
+ // This will ensure old cookies are set for a cross-subdomain checkout setup
189
+ // so that we keep backward compatibility until new cookies are rolled out.
190
+ // Once consent-tracking-api is updated to not rely on cookies anymore, we can remove this.
191
+ storefrontRootDomain: commonAncestorDomain ? "." + commonAncestorDomain : void 0,
109
192
  storefrontAccessToken,
110
- storefrontRootDomain: parseStoreDomain(checkoutDomain),
111
- country: consentConfig.country,
112
- locale: consentConfig.locale
193
+ country,
194
+ locale
113
195
  };
114
196
  return config2;
115
- }, [consentConfig, parseStoreDomain, logMissingConfig]);
197
+ }, [
198
+ logMissingConfig,
199
+ checkoutDomain,
200
+ storefrontAccessToken,
201
+ country,
202
+ locale
203
+ ]);
116
204
  useEffect(() => {
117
205
  const consentCollectedHandler = (event) => {
206
+ const latestTrackingValues = getTrackingValues();
207
+ if (initialTrackingValues.visitToken !== latestTrackingValues.visitToken || initialTrackingValues.uniqueToken !== latestTrackingValues.uniqueToken) {
208
+ revalidate();
209
+ }
118
210
  if (onVisitorConsentCollected) {
211
+ const customerPrivacy = getCustomerPrivacy();
212
+ if (customerPrivacy?.shouldShowBanner()) {
213
+ const consentValues = customerPrivacy.currentVisitorConsent();
214
+ if (consentValues) {
215
+ const NO_VALUE = "";
216
+ const noInteraction = consentValues.marketing === NO_VALUE && consentValues.analytics === NO_VALUE && consentValues.preferences === NO_VALUE;
217
+ if (noInteraction) {
218
+ return;
219
+ }
220
+ }
221
+ }
119
222
  onVisitorConsentCollected(event.detail);
120
223
  }
121
224
  };
@@ -141,14 +244,11 @@ function useCustomerPrivacy(props) {
141
244
  },
142
245
  set(value) {
143
246
  if (typeof value === "object" && value !== null && "showPreferences" in value && "loadBanner" in value) {
144
- const privacyBanner = value;
145
- privacyBanner.loadBanner(config);
146
247
  customPrivacyBanner = overridePrivacyBannerMethods({
147
- privacyBanner,
248
+ privacyBanner: value,
148
249
  config
149
250
  });
150
251
  setLoaded.privacyBanner();
151
- emitCustomerPrivacyApiLoaded();
152
252
  }
153
253
  }
154
254
  };
@@ -182,6 +282,8 @@ function useCustomerPrivacy(props) {
182
282
  const customerPrivacy = value2;
183
283
  customCustomerPrivacy = {
184
284
  ...customerPrivacy,
285
+ // Note: this method is not used by the privacy-banner,
286
+ // it bundles its own setTrackingConsent.
185
287
  setTrackingConsent: overrideCustomerPrivacySetTrackingConsent(
186
288
  { customerPrivacy, config }
187
289
  )
@@ -191,7 +293,6 @@ function useCustomerPrivacy(props) {
191
293
  customerPrivacy: customCustomerPrivacy
192
294
  };
193
295
  setLoaded.customerPrivacy();
194
- emitCustomerPrivacyApiLoaded();
195
296
  }
196
297
  }
197
298
  });
@@ -203,6 +304,24 @@ function useCustomerPrivacy(props) {
203
304
  overrideCustomerPrivacySetTrackingConsent,
204
305
  setLoaded.customerPrivacy
205
306
  ]);
307
+ useEffect(() => {
308
+ if (!apisLoaded || !cookiesReady) return;
309
+ const customerPrivacy = getCustomerPrivacy();
310
+ if (customerPrivacy && !customerPrivacy.cachedConsent) {
311
+ const trackingValues = getTrackingValues();
312
+ if (trackingValues.consent) {
313
+ customerPrivacy.cachedConsent = trackingValues.consent;
314
+ }
315
+ }
316
+ if (withPrivacyBanner) {
317
+ const privacyBanner = getPrivacyBanner();
318
+ if (privacyBanner) {
319
+ privacyBanner.loadBanner(config);
320
+ }
321
+ }
322
+ emitCustomerPrivacyApiLoaded();
323
+ onReady?.();
324
+ }, [apisLoaded, cookiesReady]);
206
325
  const result = {
207
326
  customerPrivacy: getCustomerPrivacy()
208
327
  };
@@ -218,15 +337,12 @@ function emitCustomerPrivacyApiLoaded() {
218
337
  const event = new CustomEvent("shopifyCustomerPrivacyApiLoaded");
219
338
  document.dispatchEvent(event);
220
339
  }
221
- function useApisLoaded({
222
- withPrivacyBanner,
223
- onLoaded
224
- }) {
340
+ function useApisLoaded({ withPrivacyBanner }) {
225
341
  const observing = useRef({ customerPrivacy: false, privacyBanner: false });
226
- const [apisLoaded, setApisLoaded] = useState(
342
+ const [apisLoadedArray, setApisLoaded] = useState(
227
343
  withPrivacyBanner ? [false, false] : [false]
228
344
  );
229
- const loaded = apisLoaded.every(Boolean);
345
+ const apisLoaded = apisLoadedArray.every(Boolean);
230
346
  const setLoaded = {
231
347
  customerPrivacy: () => {
232
348
  if (withPrivacyBanner) {
@@ -242,16 +358,11 @@ function useApisLoaded({
242
358
  setApisLoaded((prev) => [prev[0], true]);
243
359
  }
244
360
  };
245
- useEffect(() => {
246
- if (loaded && onLoaded) {
247
- onLoaded();
248
- }
249
- }, [loaded, onLoaded]);
250
- return { observing, setLoaded };
361
+ return { observing, setLoaded, apisLoaded };
251
362
  }
252
363
  function parseStoreDomain(checkoutDomain) {
253
364
  if (typeof window === "undefined") return;
254
- const host = window.document.location.host;
365
+ const host = window.location.host;
255
366
  const checkoutDomainParts = checkoutDomain.split(".").reverse();
256
367
  const currentDomainParts = host.split(".").reverse();
257
368
  const sameDomainParts = [];
@@ -260,7 +371,7 @@ function parseStoreDomain(checkoutDomain) {
260
371
  sameDomainParts.push(part);
261
372
  }
262
373
  });
263
- return sameDomainParts.reverse().join(".");
374
+ return sameDomainParts.reverse().join(".") || void 0;
264
375
  }
265
376
  function overrideCustomerPrivacySetTrackingConsent({
266
377
  customerPrivacy,
@@ -318,7 +429,7 @@ function getPrivacyBanner() {
318
429
  }
319
430
 
320
431
  // package.json
321
- var version = "2025.4.0";
432
+ var version = "2025.4.2";
322
433
 
323
434
  // src/analytics-manager/ShopifyAnalytics.tsx
324
435
  function getCustomerPrivacyRequired() {
@@ -338,6 +449,7 @@ function ShopifyAnalytics({
338
449
  const { subscribe: subscribe2, register: register2, canTrack } = useAnalytics();
339
450
  const [shopifyReady, setShopifyReady] = useState(false);
340
451
  const [privacyReady, setPrivacyReady] = useState(false);
452
+ const [collectedConsent, setCollectedConsent] = useState("");
341
453
  const init = useRef(false);
342
454
  const { checkoutDomain, storefrontAccessToken, language } = consent;
343
455
  const { ready: shopifyAnalyticsReady } = register2("Internal_Shopify_Analytics");
@@ -346,14 +458,31 @@ function ShopifyAnalytics({
346
458
  locale: language,
347
459
  checkoutDomain: !checkoutDomain ? "mock.shop" : checkoutDomain,
348
460
  storefrontAccessToken: !storefrontAccessToken ? "abcdefghijklmnopqrstuvwxyz123456" : storefrontAccessToken,
349
- onVisitorConsentCollected: () => setPrivacyReady(true),
350
- onReady: () => setPrivacyReady(true)
461
+ // If we use privacy banner, we should wait until consent is collected.
462
+ // Otherwise, we can consider privacy ready immediately:
463
+ onReady: () => !consent.withPrivacyBanner && setPrivacyReady(true),
464
+ onVisitorConsentCollected: (consent2) => {
465
+ try {
466
+ setCollectedConsent(JSON.stringify(consent2));
467
+ } catch (e) {
468
+ }
469
+ setPrivacyReady(true);
470
+ }
351
471
  });
472
+ const hasUserConsent = useMemo(
473
+ // must be initialized with true to avoid removing cookies too early
474
+ () => privacyReady ? canTrack() : true,
475
+ // Make this value depend on collectedConsent to re-run `canTrack()` when consent changes
476
+ [privacyReady, canTrack, collectedConsent]
477
+ );
352
478
  useShopifyCookies({
353
- hasUserConsent: privacyReady ? canTrack() : true,
354
- // must be initialized with true
479
+ hasUserConsent,
355
480
  domain,
356
- checkoutDomain
481
+ checkoutDomain,
482
+ // Already done inside useCustomerPrivacy
483
+ fetchTrackingValues: false,
484
+ // Avoid creating local cookies too early
485
+ ignoreDeprecatedCookies: !privacyReady
357
486
  });
358
487
  useEffect(() => {
359
488
  if (init.current) return;
@@ -403,11 +532,11 @@ function prepareBasePageViewPayload(payload) {
403
532
  ...payload.shop,
404
533
  hasUserConsent,
405
534
  ...getClientBrowserParameters(),
406
- ccpaEnforced: !customerPrivacy.saleOfDataAllowed(),
407
- gdprEnforced: !(customerPrivacy.marketingAllowed() && customerPrivacy.analyticsProcessingAllowed()),
408
535
  analyticsAllowed: customerPrivacy.analyticsProcessingAllowed(),
409
536
  marketingAllowed: customerPrivacy.marketingAllowed(),
410
- saleOfDataAllowed: customerPrivacy.saleOfDataAllowed()
537
+ saleOfDataAllowed: customerPrivacy.saleOfDataAllowed(),
538
+ ccpaEnforced: !customerPrivacy.saleOfDataAllowed(),
539
+ gdprEnforced: !(customerPrivacy.marketingAllowed() && customerPrivacy.analyticsProcessingAllowed())
411
540
  };
412
541
  return eventPayload;
413
542
  }
@@ -840,11 +969,11 @@ function AnalyticsProvider({
840
969
  shop: shopProp = null,
841
970
  cookieDomain
842
971
  }) {
843
- const listenerSet = useRef(false);
844
972
  const { shop } = useShopAnalytics(shopProp);
845
973
  const [analyticsLoaded, setAnalyticsLoaded] = useState(
846
974
  customCanTrack ? true : false
847
975
  );
976
+ const [consentCollected, setConsentCollected] = useState(false);
848
977
  const [carts, setCarts] = useState({ cart: null, prevCart: null });
849
978
  const [canTrack, setCanTrack] = useState(
850
979
  customCanTrack ? () => customCanTrack : () => shopifyCanTrack
@@ -912,21 +1041,21 @@ function AnalyticsProvider({
912
1041
  children,
913
1042
  !!shop && /* @__PURE__ */ jsx(AnalyticsPageView, {}),
914
1043
  !!shop && !!currentCart && /* @__PURE__ */ jsx(CartAnalytics, { cart: currentCart, setCarts }),
915
- !!shop && consent.checkoutDomain && /* @__PURE__ */ jsx(
1044
+ !!shop && /* @__PURE__ */ jsx(
916
1045
  ShopifyAnalytics,
917
1046
  {
918
1047
  consent,
919
1048
  onReady: () => {
920
- listenerSet.current = true;
921
1049
  setAnalyticsLoaded(true);
922
1050
  setCanTrack(
923
1051
  customCanTrack ? () => customCanTrack : () => shopifyCanTrack
924
1052
  );
1053
+ setConsentCollected(true);
925
1054
  },
926
1055
  domain: cookieDomain
927
1056
  }
928
1057
  ),
929
- !!shop && /* @__PURE__ */ jsx(PerfKit, { shop })
1058
+ !!shop && consentCollected && /* @__PURE__ */ jsx(PerfKit, { shop })
930
1059
  ] });
931
1060
  }
932
1061
  function useAnalytics() {
@@ -1005,6 +1134,21 @@ function getDebugHeaders(request) {
1005
1134
  purpose: request ? getHeader(request, "purpose") : void 0
1006
1135
  };
1007
1136
  }
1137
+ var SFAPI_RE = /^\/api\/(unstable|2\d{3}-\d{2})\/graphql\.json$/;
1138
+ var getSafePathname = (url) => {
1139
+ try {
1140
+ return new URL(url, "http://e.c").pathname;
1141
+ } catch {
1142
+ return "/";
1143
+ }
1144
+ };
1145
+ function extractHeaders(extract, keys) {
1146
+ return keys.reduce((acc, key) => {
1147
+ const forwardedValue = extract(key);
1148
+ if (forwardedValue) acc.push([key, forwardedValue]);
1149
+ return acc;
1150
+ }, []);
1151
+ }
1008
1152
 
1009
1153
  // src/utils/callsites.ts
1010
1154
  function withSyncStack(promise, options = {}) {
@@ -1374,13 +1518,16 @@ async function runWithCache(cacheKey, actionFn, {
1374
1518
  }
1375
1519
 
1376
1520
  // src/cache/server-fetch.ts
1521
+ var excludedHeaders = ["set-cookie", "server-timing"];
1377
1522
  function toSerializableResponse(body, response) {
1378
1523
  return [
1379
1524
  body,
1380
1525
  {
1381
1526
  status: response.status,
1382
1527
  statusText: response.statusText,
1383
- headers: Array.from(response.headers.entries())
1528
+ headers: [...response.headers].filter(
1529
+ ([key]) => !excludedHeaders.includes(key.toLowerCase())
1530
+ )
1384
1531
  }
1385
1532
  ];
1386
1533
  }
@@ -1393,7 +1540,8 @@ async function fetchWithServerCache(url, requestInit, {
1393
1540
  cacheKey = [url, requestInit],
1394
1541
  shouldCacheResponse,
1395
1542
  waitUntil,
1396
- debugInfo
1543
+ debugInfo,
1544
+ onRawHeaders
1397
1545
  }) {
1398
1546
  if (!cacheOptions && (!requestInit.method || requestInit.method === "GET")) {
1399
1547
  cacheOptions = CacheShort();
@@ -1402,6 +1550,7 @@ async function fetchWithServerCache(url, requestInit, {
1402
1550
  cacheKey,
1403
1551
  async () => {
1404
1552
  const response = await fetch(url, requestInit);
1553
+ onRawHeaders?.(response.headers);
1405
1554
  if (!response.ok) {
1406
1555
  return response;
1407
1556
  }
@@ -1624,13 +1773,6 @@ var cartSetIdDefault = (cookieOptions) => {
1624
1773
  };
1625
1774
  };
1626
1775
 
1627
- // src/constants.ts
1628
- var STOREFRONT_REQUEST_GROUP_ID_HEADER = "Custom-Storefront-Request-Group-ID";
1629
- var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
1630
- var SDK_VARIANT_HEADER = "X-SDK-Variant";
1631
- var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
1632
- var SDK_VERSION_HEADER = "X-SDK-Version";
1633
-
1634
1776
  // src/utils/uuid.ts
1635
1777
  function generateUUID() {
1636
1778
  if (typeof crypto !== "undefined" && !!crypto.randomUUID) {
@@ -1641,7 +1783,7 @@ function generateUUID() {
1641
1783
  }
1642
1784
 
1643
1785
  // src/version.ts
1644
- var LIB_VERSION = "2025.4.0";
1786
+ var LIB_VERSION = "2025.4.2";
1645
1787
 
1646
1788
  // src/utils/graphql.ts
1647
1789
  function minifyQuery(string) {
@@ -1802,16 +1944,34 @@ function createStorefrontClient(options) {
1802
1944
  contentType: "json",
1803
1945
  buyerIp: storefrontHeaders?.buyerIp || ""
1804
1946
  });
1947
+ if (storefrontHeaders?.buyerIp) {
1948
+ defaultHeaders[SHOPIFY_CLIENT_IP_HEADER] = storefrontHeaders.buyerIp;
1949
+ }
1950
+ if (storefrontHeaders?.buyerIpSig) {
1951
+ defaultHeaders[SHOPIFY_CLIENT_IP_SIG_HEADER] = storefrontHeaders.buyerIpSig;
1952
+ }
1805
1953
  defaultHeaders[STOREFRONT_REQUEST_GROUP_ID_HEADER] = storefrontHeaders?.requestGroupId || generateUUID();
1806
1954
  if (storefrontId) defaultHeaders[SHOPIFY_STOREFRONT_ID_HEADER] = storefrontId;
1807
1955
  defaultHeaders["user-agent"] = `Hydrogen ${LIB_VERSION}`;
1808
- if (storefrontHeaders && storefrontHeaders.cookie) {
1809
- const cookies = getShopifyCookies(storefrontHeaders.cookie ?? "");
1810
- if (cookies[SHOPIFY_Y])
1811
- defaultHeaders[SHOPIFY_STOREFRONT_Y_HEADER] = cookies[SHOPIFY_Y];
1812
- if (cookies[SHOPIFY_S])
1813
- defaultHeaders[SHOPIFY_STOREFRONT_S_HEADER] = cookies[SHOPIFY_S];
1956
+ const requestCookie = storefrontHeaders?.cookie ?? "";
1957
+ if (requestCookie) defaultHeaders["cookie"] = requestCookie;
1958
+ let uniqueToken;
1959
+ let visitToken;
1960
+ if (!/\b_shopify_(analytics|marketing)=/.test(requestCookie)) {
1961
+ const legacyUniqueToken = requestCookie.match(/\b_shopify_y=([^;]+)/)?.[1];
1962
+ const legacyVisitToken = requestCookie.match(/\b_shopify_s=([^;]+)/)?.[1];
1963
+ if (legacyUniqueToken) {
1964
+ defaultHeaders[SHOPIFY_STOREFRONT_Y_HEADER] = legacyUniqueToken;
1965
+ }
1966
+ if (legacyVisitToken) {
1967
+ defaultHeaders[SHOPIFY_STOREFRONT_S_HEADER] = legacyVisitToken;
1968
+ }
1969
+ uniqueToken = legacyUniqueToken ?? generateUUID();
1970
+ visitToken = legacyVisitToken ?? generateUUID();
1971
+ defaultHeaders[SHOPIFY_UNIQUE_TOKEN_HEADER] = uniqueToken;
1972
+ defaultHeaders[SHOPIFY_VISIT_TOKEN_HEADER] = visitToken;
1814
1973
  }
1974
+ let collectedSubrequestHeaders;
1815
1975
  const cacheKeyHeader = JSON.stringify({
1816
1976
  "content-type": defaultHeaders["content-type"],
1817
1977
  "user-agent": defaultHeaders["user-agent"],
@@ -1873,6 +2033,13 @@ function createStorefrontClient(options) {
1873
2033
  stackInfo,
1874
2034
  graphql: graphqlData,
1875
2035
  purpose: storefrontHeaders?.purpose
2036
+ },
2037
+ onRawHeaders: (headers2) => {
2038
+ collectedSubrequestHeaders ??= {
2039
+ // `getSetCookie` may not be available in all environments (e.g., classic Remix compiler)
2040
+ setCookie: typeof headers2.getSetCookie === "function" ? headers2.getSetCookie() : [],
2041
+ serverTiming: headers2.get("server-timing") ?? ""
2042
+ };
1876
2043
  }
1877
2044
  });
1878
2045
  const errorOptions = {
@@ -1971,9 +2138,90 @@ function createStorefrontClient(options) {
1971
2138
  generateCacheControlHeader,
1972
2139
  getPublicTokenHeaders,
1973
2140
  getPrivateTokenHeaders,
2141
+ getHeaders: () => ({ ...defaultHeaders }),
1974
2142
  getShopifyDomain,
1975
2143
  getApiUrl: getStorefrontApiUrl,
1976
- i18n: i18n ?? defaultI18n
2144
+ i18n: i18n ?? defaultI18n,
2145
+ /**
2146
+ * Checks if the request is targeting the Storefront API endpoint.
2147
+ */
2148
+ isStorefrontApiUrl(request) {
2149
+ return SFAPI_RE.test(getSafePathname(request.url ?? ""));
2150
+ },
2151
+ /**
2152
+ * Forwards the request to the Storefront API.
2153
+ */
2154
+ async forward(request, options2) {
2155
+ const forwardedHeaders = new Headers([
2156
+ // Forward only a selected set of headers to the Storefront API
2157
+ // to avoid getting 403 errors due to unexpected headers.
2158
+ ...extractHeaders(
2159
+ (key) => request.headers.get(key),
2160
+ [
2161
+ "accept",
2162
+ "accept-encoding",
2163
+ "accept-language",
2164
+ // Access-Control headers are used for CORS preflight requests.
2165
+ "access-control-request-headers",
2166
+ "access-control-request-method",
2167
+ "content-type",
2168
+ "content-length",
2169
+ "cookie",
2170
+ "origin",
2171
+ "referer",
2172
+ "user-agent",
2173
+ STOREFRONT_ACCESS_TOKEN_HEADER,
2174
+ SHOPIFY_UNIQUE_TOKEN_HEADER,
2175
+ SHOPIFY_VISIT_TOKEN_HEADER
2176
+ ]
2177
+ ),
2178
+ // Add some headers to help with geolocalization and debugging
2179
+ ...extractHeaders(
2180
+ (key) => defaultHeaders[key],
2181
+ [
2182
+ SHOPIFY_CLIENT_IP_HEADER,
2183
+ SHOPIFY_CLIENT_IP_SIG_HEADER,
2184
+ SHOPIFY_STOREFRONT_ID_HEADER,
2185
+ STOREFRONT_REQUEST_GROUP_ID_HEADER
2186
+ ]
2187
+ )
2188
+ ]);
2189
+ if (storefrontHeaders?.buyerIp) {
2190
+ forwardedHeaders.set("x-forwarded-for", storefrontHeaders.buyerIp);
2191
+ }
2192
+ const storefrontApiVersion = options2?.storefrontApiVersion ?? getSafePathname(request.url).match(SFAPI_RE)?.[1];
2193
+ const sfapiResponse = await fetch(
2194
+ getStorefrontApiUrl({ storefrontApiVersion }),
2195
+ {
2196
+ method: request.method,
2197
+ body: request.body,
2198
+ headers: forwardedHeaders
2199
+ }
2200
+ );
2201
+ return new Response(sfapiResponse.body, sfapiResponse);
2202
+ },
2203
+ setCollectedSubrequestHeaders: (response) => {
2204
+ if (collectedSubrequestHeaders) {
2205
+ for (const value of collectedSubrequestHeaders.setCookie) {
2206
+ response.headers.append("Set-Cookie", value);
2207
+ }
2208
+ }
2209
+ const serverTiming = extractServerTimingHeader(
2210
+ collectedSubrequestHeaders?.serverTiming
2211
+ );
2212
+ const isDocumentResponse = response.headers.get("content-type")?.startsWith("text/html");
2213
+ const fallbackValues = isDocumentResponse ? { _y: uniqueToken, _s: visitToken } : void 0;
2214
+ appendServerTimingHeader(response, {
2215
+ ...fallbackValues,
2216
+ ...serverTiming
2217
+ });
2218
+ if (isDocumentResponse && collectedSubrequestHeaders && // _shopify_essential cookie is always set, but we need more than that
2219
+ collectedSubrequestHeaders.setCookie.length > 1 && serverTiming?._y && serverTiming?._s && serverTiming?._cmp) {
2220
+ appendServerTimingHeader(response, {
2221
+ [HYDROGEN_SERVER_TRACKING_KEY]: "1"
2222
+ });
2223
+ }
2224
+ }
1977
2225
  }
1978
2226
  };
1979
2227
  }
@@ -2770,7 +3018,8 @@ function createCartHandler(options) {
2770
3018
  storefront,
2771
3019
  customerAccount,
2772
3020
  cartQueryFragment,
2773
- cartMutateFragment
3021
+ cartMutateFragment,
3022
+ buyerIdentity
2774
3023
  } = options;
2775
3024
  let cartId = _getCartId();
2776
3025
  const getCartId = () => cartId || _getCartId();
@@ -2782,6 +3031,10 @@ function createCartHandler(options) {
2782
3031
  };
2783
3032
  const _cartCreate = cartCreateDefault(mutateOptions);
2784
3033
  const cartCreate = async function(...args) {
3034
+ args[0].buyerIdentity = {
3035
+ ...buyerIdentity,
3036
+ ...args[0].buyerIdentity
3037
+ };
2785
3038
  const result = await _cartCreate(...args);
2786
3039
  cartId = result?.cart?.id;
2787
3040
  return result;
@@ -2805,7 +3058,7 @@ function createCartHandler(options) {
2805
3058
  sellingPlanId: line.sellingPlanId
2806
3059
  };
2807
3060
  });
2808
- return cartId || optionalParams?.cartId ? await cartLinesAddDefault(mutateOptions)(lines, optionalParams) : await cartCreate({ lines }, optionalParams);
3061
+ return cartId || optionalParams?.cartId ? await cartLinesAddDefault(mutateOptions)(lines, optionalParams) : await cartCreate({ lines, buyerIdentity }, optionalParams);
2809
3062
  },
2810
3063
  updateLines: cartLinesUpdateDefault(mutateOptions),
2811
3064
  removeLines: cartLinesRemoveDefault(mutateOptions),
@@ -2821,11 +3074,11 @@ function createCartHandler(options) {
2821
3074
  optionalParams
2822
3075
  ) : await cartCreate({ giftCardCodes }, optionalParams);
2823
3076
  },
2824
- updateBuyerIdentity: async (buyerIdentity, optionalParams) => {
3077
+ updateBuyerIdentity: async (buyerIdentity2, optionalParams) => {
2825
3078
  return cartId || optionalParams?.cartId ? await cartBuyerIdentityUpdateDefault(mutateOptions)(
2826
- buyerIdentity,
3079
+ buyerIdentity2,
2827
3080
  optionalParams
2828
- ) : await cartCreate({ buyerIdentity }, optionalParams);
3081
+ ) : await cartCreate({ buyerIdentity: buyerIdentity2 }, optionalParams);
2829
3082
  },
2830
3083
  updateNote: async (note, optionalParams) => {
2831
3084
  return cartId || optionalParams?.cartId ? await cartNoteUpdateDefault(mutateOptions)(note, optionalParams) : await cartCreate({ note }, optionalParams);
@@ -3720,7 +3973,8 @@ function createHydrogenContext(options) {
3720
3973
  logErrors,
3721
3974
  storefront: storefrontOptions = {},
3722
3975
  customerAccount: customerAccountOptions,
3723
- cart: cartOptions = {}
3976
+ cart: cartOptions = {},
3977
+ buyerIdentity
3724
3978
  } = options;
3725
3979
  if (!session) {
3726
3980
  console.warn(
@@ -3770,6 +4024,7 @@ function createHydrogenContext(options) {
3770
4024
  cartQueryFragment: cartOptions.queryFragment,
3771
4025
  cartMutateFragment: cartOptions.mutateFragment,
3772
4026
  customMethods: cartOptions.customMethods,
4027
+ buyerIdentity,
3773
4028
  // defaults
3774
4029
  storefront,
3775
4030
  customerAccount
@@ -3787,8 +4042,63 @@ function getStorefrontHeaders(request) {
3787
4042
  return {
3788
4043
  requestGroupId: getHeader(request, "request-id"),
3789
4044
  buyerIp: getHeader(request, "oxygen-buyer-ip"),
4045
+ buyerIpSig: getHeader(request, "X-Shopify-Client-IP-Sig"),
3790
4046
  cookie: getHeader(request, "cookie"),
3791
- purpose: getHeader(request, "purpose")
4047
+ purpose: getHeader(request, "sec-purpose") || getHeader(request, "purpose")
4048
+ };
4049
+ }
4050
+ function createRequestHandler({
4051
+ build,
4052
+ mode,
4053
+ poweredByHeader = true,
4054
+ getLoadContext,
4055
+ collectTrackingInformation = true,
4056
+ proxyStandardRoutes = true
4057
+ }) {
4058
+ const handleRequest = createRequestHandler$1(build, mode);
4059
+ const appendPoweredByHeader = poweredByHeader ? (response) => response.headers.append("powered-by", "Shopify, Hydrogen") : void 0;
4060
+ return async (request) => {
4061
+ const method = request.method;
4062
+ if ((method === "GET" || method === "HEAD") && request.body) {
4063
+ return new Response(`${method} requests cannot have a body`, {
4064
+ status: 400
4065
+ });
4066
+ }
4067
+ const url = new URL(request.url);
4068
+ if (url.pathname.includes("//")) {
4069
+ return new Response(null, {
4070
+ status: 301,
4071
+ headers: {
4072
+ location: url.pathname.replace(/\/+/g, "/")
4073
+ }
4074
+ });
4075
+ }
4076
+ const context = getLoadContext ? await getLoadContext(request) : void 0;
4077
+ const storefront = context?.storefront;
4078
+ if (proxyStandardRoutes) {
4079
+ if (!storefront) {
4080
+ warnOnce(
4081
+ "[h2:createRequestHandler] Storefront instance is required to proxy standard routes."
4082
+ );
4083
+ }
4084
+ if (storefront?.isStorefrontApiUrl(request)) {
4085
+ const response2 = await storefront.forward(request);
4086
+ appendPoweredByHeader?.(response2);
4087
+ return response2;
4088
+ }
4089
+ }
4090
+ const response = await handleRequest(request, context);
4091
+ if (storefront && proxyStandardRoutes) {
4092
+ if (collectTrackingInformation) {
4093
+ storefront.setCollectedSubrequestHeaders(response);
4094
+ }
4095
+ const fetchDest = request.headers.get("sec-fetch-dest");
4096
+ if (fetchDest && fetchDest === "document" || request.headers.get("accept")?.includes("text/html")) {
4097
+ appendServerTimingHeader(response, { [HYDROGEN_SFAPI_PROXY_KEY]: "1" });
4098
+ }
4099
+ }
4100
+ appendPoweredByHeader?.(response);
4101
+ return response;
3792
4102
  };
3793
4103
  }
3794
4104
  var NonceContext = createContext(void 0);
@@ -5782,6 +6092,6 @@ var QUERIES = {
5782
6092
  //! @see: https://shopify.dev/docs/api/storefront/latest/mutations/cartDeliveryAddressesRemove
5783
6093
  //! @see: https://shopify.dev/docs/api/storefront/latest/mutations/cartDeliveryAddressesUpdate
5784
6094
 
5785
- export { Analytics, AnalyticsEvent, CacheCustom, CacheLong, CacheNone, CacheShort, CartForm, InMemoryCache, OptimisticInput, Pagination, RichText, Script, Seo, ShopPayButton, VariantSelector, cartAttributesUpdateDefault, cartBuyerIdentityUpdateDefault, cartCreateDefault, cartDiscountCodesUpdateDefault, cartGetDefault, cartGetIdDefault, cartGiftCardCodesUpdateDefault, cartLinesAddDefault, cartLinesRemoveDefault, cartLinesUpdateDefault, cartMetafieldDeleteDefault, cartMetafieldsSetDefault, cartNoteUpdateDefault, cartSelectedDeliveryOptionsUpdateDefault, cartSetIdDefault, changelogHandler, createCartHandler, createContentSecurityPolicy, createCustomerAccountClient, createHydrogenContext, createStorefrontClient, createWithCache, formatAPIResult, generateCacheControlHeader, getPaginationVariables, getSelectedProductOptions, getSeoMeta, getShopAnalytics, getSitemap, getSitemapIndex, graphiqlLoader, hydrogenRoutes, storefrontRedirect, useAnalytics, useCustomerPrivacy, useNonce, useOptimisticCart, useOptimisticData, useOptimisticVariant };
6095
+ export { Analytics, AnalyticsEvent, CacheCustom, CacheLong, CacheNone, CacheShort, CartForm, InMemoryCache, OptimisticInput, Pagination, RichText, Script, Seo, ShopPayButton, VariantSelector, cartAttributesUpdateDefault, cartBuyerIdentityUpdateDefault, cartCreateDefault, cartDiscountCodesUpdateDefault, cartGetDefault, cartGetIdDefault, cartGiftCardCodesUpdateDefault, cartLinesAddDefault, cartLinesRemoveDefault, cartLinesUpdateDefault, cartMetafieldDeleteDefault, cartMetafieldsSetDefault, cartNoteUpdateDefault, cartSelectedDeliveryOptionsUpdateDefault, cartSetIdDefault, changelogHandler, createCartHandler, createContentSecurityPolicy, createCustomerAccountClient, createHydrogenContext, createRequestHandler, createStorefrontClient, createWithCache, formatAPIResult, generateCacheControlHeader, getPaginationVariables, getSelectedProductOptions, getSeoMeta, getShopAnalytics, getSitemap, getSitemapIndex, graphiqlLoader, hydrogenRoutes, storefrontRedirect, useAnalytics, useCustomerPrivacy, useNonce, useOptimisticCart, useOptimisticData, useOptimisticVariant };
5786
6096
  //# sourceMappingURL=index.js.map
5787
6097
  //# sourceMappingURL=index.js.map