@shopify/hydrogen 2025.1.3 → 2025.1.5

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.1.3";
530
+ var version = "2025.1.5";
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
  }
@@ -1674,7 +1823,10 @@ CartForm.ACTIONS = {
1674
1823
  NoteUpdate: "NoteUpdate",
1675
1824
  SelectedDeliveryOptionsUpdate: "SelectedDeliveryOptionsUpdate",
1676
1825
  MetafieldsSet: "MetafieldsSet",
1677
- MetafieldDelete: "MetafieldDelete"
1826
+ MetafieldDelete: "MetafieldDelete",
1827
+ DeliveryAddressesAdd: "DeliveryAddressesAdd",
1828
+ DeliveryAddressesUpdate: "DeliveryAddressesUpdate",
1829
+ DeliveryAddressesRemove: "DeliveryAddressesRemove"
1678
1830
  };
1679
1831
  function getFormInput(formData) {
1680
1832
  const data = {};
@@ -1682,6 +1834,11 @@ function getFormInput(formData) {
1682
1834
  const key = pair[0];
1683
1835
  const values = formData.getAll(key);
1684
1836
  data[key] = values.length > 1 ? values : pair[1];
1837
+ if (data[key] === "on") {
1838
+ data[key] = true;
1839
+ } else if (data[key] === "off") {
1840
+ data[key] = false;
1841
+ }
1685
1842
  }
1686
1843
  const { cartFormInput, ...otherData } = data;
1687
1844
  const { action, inputs } = cartFormInput ? JSON.parse(String(cartFormInput)) : {};
@@ -1714,13 +1871,6 @@ var cartSetIdDefault = (cookieOptions) => {
1714
1871
  };
1715
1872
  };
1716
1873
 
1717
- // src/constants.ts
1718
- var STOREFRONT_REQUEST_GROUP_ID_HEADER = "Custom-Storefront-Request-Group-ID";
1719
- var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
1720
- var SDK_VARIANT_HEADER = "X-SDK-Variant";
1721
- var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
1722
- var SDK_VERSION_HEADER = "X-SDK-Version";
1723
-
1724
1874
  // src/utils/uuid.ts
1725
1875
  function generateUUID() {
1726
1876
  if (typeof crypto !== "undefined" && !!crypto.randomUUID) {
@@ -1731,7 +1881,7 @@ function generateUUID() {
1731
1881
  }
1732
1882
 
1733
1883
  // src/version.ts
1734
- var LIB_VERSION = "2025.1.3";
1884
+ var LIB_VERSION = "2025.1.5";
1735
1885
 
1736
1886
  // src/utils/graphql.ts
1737
1887
  function minifyQuery(string) {
@@ -1892,16 +2042,34 @@ function createStorefrontClient(options) {
1892
2042
  contentType: "json",
1893
2043
  buyerIp: storefrontHeaders?.buyerIp || ""
1894
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
+ }
1895
2051
  defaultHeaders[STOREFRONT_REQUEST_GROUP_ID_HEADER] = storefrontHeaders?.requestGroupId || generateUUID();
1896
2052
  if (storefrontId) defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_ID_HEADER] = storefrontId;
1897
2053
  defaultHeaders["user-agent"] = `Hydrogen ${LIB_VERSION}`;
1898
- if (storefrontHeaders && storefrontHeaders.cookie) {
1899
- const cookies = hydrogenReact.getShopifyCookies(storefrontHeaders.cookie ?? "");
1900
- if (cookies[hydrogenReact.SHOPIFY_Y])
1901
- defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_Y_HEADER] = cookies[hydrogenReact.SHOPIFY_Y];
1902
- if (cookies[hydrogenReact.SHOPIFY_S])
1903
- 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;
1904
2071
  }
2072
+ let collectedSubrequestHeaders;
1905
2073
  const cacheKeyHeader = JSON.stringify({
1906
2074
  "content-type": defaultHeaders["content-type"],
1907
2075
  "user-agent": defaultHeaders["user-agent"],
@@ -1963,6 +2131,13 @@ function createStorefrontClient(options) {
1963
2131
  stackInfo,
1964
2132
  graphql: graphqlData,
1965
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
+ };
1966
2141
  }
1967
2142
  });
1968
2143
  const errorOptions = {
@@ -2061,9 +2236,90 @@ function createStorefrontClient(options) {
2061
2236
  generateCacheControlHeader,
2062
2237
  getPublicTokenHeaders,
2063
2238
  getPrivateTokenHeaders,
2239
+ getHeaders: () => ({ ...defaultHeaders }),
2064
2240
  getShopifyDomain,
2065
2241
  getApiUrl: getStorefrontApiUrl,
2066
- 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
+ }
2067
2323
  }
2068
2324
  };
2069
2325
  }
@@ -2741,6 +2997,117 @@ var CART_GIFT_CARD_CODE_UPDATE_MUTATION = (cartFragment = MINIMAL_CART_FRAGMENT)
2741
2997
  ${CART_WARNING_FRAGMENT}
2742
2998
  `;
2743
2999
 
3000
+ // src/cart/queries/cartDeliveryAddressesAddDefault.tsx
3001
+ function cartDeliveryAddressesAddDefault(options) {
3002
+ return async (addresses, optionalParams) => {
3003
+ const { cartDeliveryAddressesAdd, errors: errors2 } = await options.storefront.mutate(CART_DELIVERY_ADDRESSES_ADD_MUTATION(options.cartFragment), {
3004
+ variables: {
3005
+ cartId: options.getCartId(),
3006
+ addresses,
3007
+ ...optionalParams
3008
+ }
3009
+ });
3010
+ return formatAPIResult(cartDeliveryAddressesAdd, errors2);
3011
+ };
3012
+ }
3013
+ var CART_DELIVERY_ADDRESSES_ADD_MUTATION = (cartFragment = MINIMAL_CART_FRAGMENT) => `#graphql
3014
+ mutation cartDeliveryAddressesAdd(
3015
+ $cartId: ID!
3016
+ $addresses: [CartSelectableAddressInput!]!,
3017
+ $country: CountryCode = ZZ
3018
+ $language: LanguageCode
3019
+ ) @inContext(country: $country, language: $language) {
3020
+ cartDeliveryAddressesAdd(addresses: $addresses, cartId: $cartId) {
3021
+ cart {
3022
+ ...CartApiMutation
3023
+ }
3024
+ userErrors {
3025
+ ...CartApiError
3026
+ }
3027
+ warnings {
3028
+ ...CartApiWarning
3029
+ }
3030
+ }
3031
+ }
3032
+ ${cartFragment}
3033
+ ${USER_ERROR_FRAGMENT}
3034
+ ${CART_WARNING_FRAGMENT}
3035
+ `;
3036
+
3037
+ // src/cart/queries/cartDeliveryAddressesRemoveDefault.tsx
3038
+ function cartDeliveryAddressesRemoveDefault(options) {
3039
+ return async (addressIds, optionalParams) => {
3040
+ const { cartDeliveryAddressesRemove, errors: errors2 } = await options.storefront.mutate(CART_DELIVERY_ADDRESSES_REMOVE_MUTATION(options.cartFragment), {
3041
+ variables: {
3042
+ cartId: options.getCartId(),
3043
+ addressIds,
3044
+ ...optionalParams
3045
+ }
3046
+ });
3047
+ return formatAPIResult(cartDeliveryAddressesRemove, errors2);
3048
+ };
3049
+ }
3050
+ var CART_DELIVERY_ADDRESSES_REMOVE_MUTATION = (cartFragment = MINIMAL_CART_FRAGMENT) => `#graphql
3051
+ mutation cartDeliveryAddressesRemove(
3052
+ $cartId: ID!
3053
+ $addressIds: [ID!]!,
3054
+ $country: CountryCode = ZZ
3055
+ $language: LanguageCode
3056
+ ) @inContext(country: $country, language: $language) {
3057
+ cartDeliveryAddressesRemove(addressIds: $addressIds, cartId: $cartId) {
3058
+ cart {
3059
+ ...CartApiMutation
3060
+ }
3061
+ userErrors {
3062
+ ...CartApiError
3063
+ }
3064
+ warnings {
3065
+ ...CartApiWarning
3066
+ }
3067
+ }
3068
+ }
3069
+ ${cartFragment}
3070
+ ${USER_ERROR_FRAGMENT}
3071
+ ${CART_WARNING_FRAGMENT}
3072
+ `;
3073
+
3074
+ // src/cart/queries/cartDeliveryAddressesUpdateDefault.tsx
3075
+ function cartDeliveryAddressesUpdateDefault(options) {
3076
+ return async (addresses, optionalParams) => {
3077
+ const { cartDeliveryAddressesUpdate, errors: errors2 } = await options.storefront.mutate(CART_DELIVERY_ADDRESSES_UPDATE_MUTATION(options.cartFragment), {
3078
+ variables: {
3079
+ cartId: options.getCartId(),
3080
+ addresses,
3081
+ ...optionalParams
3082
+ }
3083
+ });
3084
+ return formatAPIResult(cartDeliveryAddressesUpdate, errors2);
3085
+ };
3086
+ }
3087
+ var CART_DELIVERY_ADDRESSES_UPDATE_MUTATION = (cartFragment = MINIMAL_CART_FRAGMENT) => `#graphql
3088
+ mutation cartDeliveryAddressesUpdate(
3089
+ $cartId: ID!
3090
+ $addresses: [CartSelectableAddressUpdateInput!]!,
3091
+ $country: CountryCode = ZZ
3092
+ $language: LanguageCode
3093
+ ) @inContext(country: $country, language: $language) {
3094
+ cartDeliveryAddressesUpdate(addresses: $addresses, cartId: $cartId) {
3095
+ cart {
3096
+ ...CartApiMutation
3097
+ }
3098
+ userErrors {
3099
+ ...CartApiError
3100
+ }
3101
+ warnings {
3102
+ ...CartApiWarning
3103
+ }
3104
+ }
3105
+ }
3106
+ ${cartFragment}
3107
+ ${USER_ERROR_FRAGMENT}
3108
+ ${CART_WARNING_FRAGMENT}
3109
+ `;
3110
+
2744
3111
  // src/cart/createCartHandler.ts
2745
3112
  function createCartHandler(options) {
2746
3113
  const {
@@ -2822,7 +3189,10 @@ function createCartHandler(options) {
2822
3189
  optionalParams
2823
3190
  ) : await cartCreate({ metafields }, optionalParams);
2824
3191
  },
2825
- deleteMetafield: cartMetafieldDeleteDefault(mutateOptions)
3192
+ deleteMetafield: cartMetafieldDeleteDefault(mutateOptions),
3193
+ addDeliveryAddresses: cartDeliveryAddressesAddDefault(mutateOptions),
3194
+ removeDeliveryAddresses: cartDeliveryAddressesRemoveDefault(mutateOptions),
3195
+ updateDeliveryAddresses: cartDeliveryAddressesUpdateDefault(mutateOptions)
2826
3196
  };
2827
3197
  if ("customMethods" in options) {
2828
3198
  return {
@@ -3503,7 +3873,18 @@ function createCustomerAccountClient({
3503
3873
  ]).toString()}`
3504
3874
  ).toString() : postLogoutRedirectUri;
3505
3875
  clearSession(session);
3506
- return redirect(logoutUrl, { headers: options?.headers || {} });
3876
+ const headers = options?.headers instanceof Headers ? options?.headers : new Headers(options?.headers);
3877
+ if (!options?.keepSession) {
3878
+ if (session.destroy) {
3879
+ headers.set("Set-Cookie", await session.destroy());
3880
+ } else {
3881
+ console.warn(
3882
+ "[h2:warn:customerAccount] session.destroy is not available on your session implementation. All session data might not be cleared on logout."
3883
+ );
3884
+ }
3885
+ session.isPending = false;
3886
+ }
3887
+ return redirect(logoutUrl, { headers });
3507
3888
  },
3508
3889
  isLoggedIn,
3509
3890
  handleAuthStatus,
@@ -3752,8 +4133,63 @@ function getStorefrontHeaders(request) {
3752
4133
  return {
3753
4134
  requestGroupId: getHeader(request, "request-id"),
3754
4135
  buyerIp: getHeader(request, "oxygen-buyer-ip"),
4136
+ buyerIpSig: getHeader(request, "X-Shopify-Client-IP-Sig"),
3755
4137
  cookie: getHeader(request, "cookie"),
3756
- purpose: getHeader(request, "purpose")
4138
+ purpose: getHeader(request, "sec-purpose") || getHeader(request, "purpose")
4139
+ };
4140
+ }
4141
+ function createRequestHandler({
4142
+ build,
4143
+ mode,
4144
+ poweredByHeader = true,
4145
+ getLoadContext,
4146
+ collectTrackingInformation = true,
4147
+ proxyStandardRoutes = true
4148
+ }) {
4149
+ const handleRequest = serverRuntime.createRequestHandler(build, mode);
4150
+ const appendPoweredByHeader = poweredByHeader ? (response) => response.headers.append("powered-by", "Shopify, Hydrogen") : void 0;
4151
+ return async (request) => {
4152
+ const method = request.method;
4153
+ if ((method === "GET" || method === "HEAD") && request.body) {
4154
+ return new Response(`${method} requests cannot have a body`, {
4155
+ status: 400
4156
+ });
4157
+ }
4158
+ const url = new URL(request.url);
4159
+ if (url.pathname.includes("//")) {
4160
+ return new Response(null, {
4161
+ status: 301,
4162
+ headers: {
4163
+ location: url.pathname.replace(/\/+/g, "/")
4164
+ }
4165
+ });
4166
+ }
4167
+ const context = getLoadContext ? await getLoadContext(request) : void 0;
4168
+ const storefront = context?.storefront;
4169
+ if (proxyStandardRoutes) {
4170
+ if (!storefront) {
4171
+ warnOnce(
4172
+ "[h2:createRequestHandler] Storefront instance is required to proxy standard routes."
4173
+ );
4174
+ }
4175
+ if (storefront?.isStorefrontApiUrl(request)) {
4176
+ const response2 = await storefront.forward(request);
4177
+ appendPoweredByHeader?.(response2);
4178
+ return response2;
4179
+ }
4180
+ }
4181
+ const response = await handleRequest(request, context);
4182
+ if (storefront && proxyStandardRoutes) {
4183
+ if (collectTrackingInformation) {
4184
+ storefront.setCollectedSubrequestHeaders(response);
4185
+ }
4186
+ const fetchDest = request.headers.get("sec-fetch-dest");
4187
+ if (fetchDest && fetchDest === "document" || request.headers.get("accept")?.includes("text/html")) {
4188
+ appendServerTimingHeader(response, { [HYDROGEN_SFAPI_PROXY_KEY]: "1" });
4189
+ }
4190
+ }
4191
+ appendPoweredByHeader?.(response);
4192
+ return response;
3757
4193
  };
3758
4194
  }
3759
4195
  var NonceContext = react.createContext(void 0);
@@ -5741,6 +6177,9 @@ var QUERIES = {
5741
6177
  //! @see https://shopify.dev/docs/api/storefront/latest/mutations/cartMetafieldsSet
5742
6178
  //! @see https://shopify.dev/docs/api/storefront/2025-01/mutations/cartMetafieldDelete
5743
6179
  //! @see https://shopify.dev/docs/api/storefront/latest/mutations/cartGiftCardCodesUpdate
6180
+ //! @see: https://shopify.dev/docs/api/storefront/latest/mutations/cartDeliveryAddressesAdd
6181
+ //! @see: https://shopify.dev/docs/api/storefront/latest/mutations/cartDeliveryAddressesRemove
6182
+ //! @see: https://shopify.dev/docs/api/storefront/latest/mutations/cartDeliveryAddressesUpdate
5744
6183
 
5745
6184
  Object.defineProperty(exports, "AnalyticsEventName", {
5746
6185
  enumerable: true,
@@ -5810,6 +6249,10 @@ Object.defineProperty(exports, "getShopifyCookies", {
5810
6249
  enumerable: true,
5811
6250
  get: function () { return hydrogenReact.getShopifyCookies; }
5812
6251
  });
6252
+ Object.defineProperty(exports, "getTrackingValues", {
6253
+ enumerable: true,
6254
+ get: function () { return hydrogenReact.getTrackingValues; }
6255
+ });
5813
6256
  Object.defineProperty(exports, "isOptionValueCombinationInEncodedVariant", {
5814
6257
  enumerable: true,
5815
6258
  get: function () { return hydrogenReact.isOptionValueCombinationInEncodedVariant; }
@@ -5885,6 +6328,7 @@ exports.createCartHandler = createCartHandler;
5885
6328
  exports.createContentSecurityPolicy = createContentSecurityPolicy;
5886
6329
  exports.createCustomerAccountClient = createCustomerAccountClient;
5887
6330
  exports.createHydrogenContext = createHydrogenContext;
6331
+ exports.createRequestHandler = createRequestHandler;
5888
6332
  exports.createStorefrontClient = createStorefrontClient;
5889
6333
  exports.createWithCache = createWithCache;
5890
6334
  exports.formatAPIResult = formatAPIResult;