@shopify/hydrogen 2024.7.9 → 2024.7.10

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.
@@ -6,6 +6,7 @@ var react$1 = require('@remix-run/react');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
7
  var cookie = require('worktop/cookie');
8
8
  var cspBuilder = require('content-security-policy-builder');
9
+ var serverRuntime = require('@remix-run/server-runtime');
9
10
 
10
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
12
 
@@ -426,13 +427,16 @@ async function runWithCache(cacheKey, actionFn, {
426
427
  }
427
428
 
428
429
  // src/cache/server-fetch.ts
430
+ var excludedHeaders = ["set-cookie", "server-timing"];
429
431
  function toSerializableResponse(body, response) {
430
432
  return [
431
433
  body,
432
434
  {
433
435
  status: response.status,
434
436
  statusText: response.statusText,
435
- headers: Array.from(response.headers.entries())
437
+ headers: [...response.headers].filter(
438
+ ([key]) => !excludedHeaders.includes(key.toLowerCase())
439
+ )
436
440
  }
437
441
  ];
438
442
  }
@@ -447,7 +451,8 @@ async function fetchWithServerCache(url, requestInit, {
447
451
  shouldCacheResponse = () => true,
448
452
  waitUntil,
449
453
  returnType = "json",
450
- debugInfo
454
+ debugInfo,
455
+ onRawHeaders
451
456
  } = {}) {
452
457
  if (!cacheOptions && (!requestInit.method || requestInit.method === "GET")) {
453
458
  cacheOptions = CacheShort();
@@ -456,6 +461,7 @@ async function fetchWithServerCache(url, requestInit, {
456
461
  cacheKey,
457
462
  async () => {
458
463
  const response = await fetch(url, requestInit);
464
+ onRawHeaders?.(response.headers);
459
465
  let data;
460
466
  try {
461
467
  data = await response[returnType]();
@@ -484,6 +490,10 @@ var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
484
490
  var SDK_VARIANT_HEADER = "X-SDK-Variant";
485
491
  var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
486
492
  var SDK_VERSION_HEADER = "X-SDK-Version";
493
+ var SHOPIFY_CLIENT_IP_HEADER = "X-Shopify-Client-IP";
494
+ var SHOPIFY_CLIENT_IP_SIG_HEADER = "X-Shopify-Client-IP-Sig";
495
+ var HYDROGEN_SFAPI_PROXY_KEY = "_sfapi_proxy";
496
+ var HYDROGEN_SERVER_TRACKING_KEY = "_server_tracking";
487
497
 
488
498
  // src/utils/uuid.ts
489
499
  function generateUUID() {
@@ -511,7 +521,7 @@ var errorOnce = (string) => {
511
521
  };
512
522
 
513
523
  // src/version.ts
514
- var LIB_VERSION = "2024.7.9";
524
+ var LIB_VERSION = "2024.7.10";
515
525
 
516
526
  // src/utils/graphql.ts
517
527
  function minifyQuery(string) {
@@ -687,6 +697,78 @@ var getCallerStackLine = (stackOffset = 0) => {
687
697
  return stackInfo;
688
698
  } ;
689
699
 
700
+ // src/utils/request.ts
701
+ function getHeader(request, key) {
702
+ return getHeaderValue(request.headers, key);
703
+ }
704
+ function getHeaderValue(headers, key) {
705
+ const value = headers?.get?.(key) ?? headers?.[key];
706
+ return typeof value === "string" ? value : null;
707
+ }
708
+ function getDebugHeaders(request) {
709
+ return {
710
+ requestId: request ? getHeader(request, "request-id") : void 0,
711
+ purpose: request ? getHeader(request, "purpose") : void 0
712
+ };
713
+ }
714
+ var SFAPI_RE = /^\/api\/(unstable|2\d{3}-\d{2})\/graphql\.json$/;
715
+ var getSafePathname = (url) => {
716
+ try {
717
+ return new URL(url, "http://e.c").pathname;
718
+ } catch {
719
+ return "/";
720
+ }
721
+ };
722
+ function extractHeaders(extract, keys) {
723
+ return keys.reduce((acc, key) => {
724
+ const forwardedValue = extract(key);
725
+ if (forwardedValue) acc.push([key, forwardedValue]);
726
+ return acc;
727
+ }, []);
728
+ }
729
+
730
+ // src/utils/server-timing.ts
731
+ function buildServerTimingHeader(values) {
732
+ return Object.entries(values).map(([key, value]) => value ? `${key};desc=${value}` : void 0).filter(Boolean).join(", ");
733
+ }
734
+ function appendServerTimingHeader(response, values) {
735
+ const header = typeof values === "string" ? values : buildServerTimingHeader(values);
736
+ if (header) {
737
+ response.headers.append("Server-Timing", header);
738
+ }
739
+ }
740
+ var trackedTimings = ["_y", "_s", "_cmp"];
741
+ function extractServerTimingHeader(serverTimingHeader) {
742
+ const values = {};
743
+ if (!serverTimingHeader) return values;
744
+ const re = new RegExp(
745
+ `\\b(${trackedTimings.join("|")});desc="?([^",]+)"?`,
746
+ "g"
747
+ );
748
+ let match;
749
+ while ((match = re.exec(serverTimingHeader)) !== null) {
750
+ values[match[1]] = match[2];
751
+ }
752
+ return values;
753
+ }
754
+ function hasServerTimingInNavigationEntry(key) {
755
+ if (typeof window === "undefined") return false;
756
+ try {
757
+ const navigationEntry = window.performance.getEntriesByType(
758
+ "navigation"
759
+ )[0];
760
+ return !!navigationEntry?.serverTiming?.some((entry) => entry.name === key);
761
+ } catch (e) {
762
+ return false;
763
+ }
764
+ }
765
+ function isSfapiProxyEnabled() {
766
+ return hasServerTimingInNavigationEntry(HYDROGEN_SFAPI_PROXY_KEY);
767
+ }
768
+ function hasServerReturnedTrackingValues() {
769
+ return hasServerTimingInNavigationEntry(HYDROGEN_SERVER_TRACKING_KEY);
770
+ }
771
+
690
772
  // src/storefront.ts
691
773
  var defaultI18n = { language: "EN", country: "US" };
692
774
  function createStorefrontClient(options) {
@@ -716,16 +798,34 @@ function createStorefrontClient(options) {
716
798
  contentType: "json",
717
799
  buyerIp: storefrontHeaders?.buyerIp || ""
718
800
  });
801
+ if (storefrontHeaders?.buyerIp) {
802
+ defaultHeaders[SHOPIFY_CLIENT_IP_HEADER] = storefrontHeaders.buyerIp;
803
+ }
804
+ if (storefrontHeaders?.buyerIpSig) {
805
+ defaultHeaders[SHOPIFY_CLIENT_IP_SIG_HEADER] = storefrontHeaders.buyerIpSig;
806
+ }
719
807
  defaultHeaders[STOREFRONT_REQUEST_GROUP_ID_HEADER] = storefrontHeaders?.requestGroupId || generateUUID();
720
808
  if (storefrontId) defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_ID_HEADER] = storefrontId;
721
809
  defaultHeaders["user-agent"] = `Hydrogen ${LIB_VERSION}`;
722
- if (storefrontHeaders && storefrontHeaders.cookie) {
723
- const cookies = hydrogenReact.getShopifyCookies(storefrontHeaders.cookie ?? "");
724
- if (cookies[hydrogenReact.SHOPIFY_Y])
725
- defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_Y_HEADER] = cookies[hydrogenReact.SHOPIFY_Y];
726
- if (cookies[hydrogenReact.SHOPIFY_S])
727
- defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_S_HEADER] = cookies[hydrogenReact.SHOPIFY_S];
810
+ const requestCookie = storefrontHeaders?.cookie ?? "";
811
+ if (requestCookie) defaultHeaders["cookie"] = requestCookie;
812
+ let uniqueToken;
813
+ let visitToken;
814
+ if (!/\b_shopify_(analytics|marketing)=/.test(requestCookie)) {
815
+ const legacyUniqueToken = requestCookie.match(/\b_shopify_y=([^;]+)/)?.[1];
816
+ const legacyVisitToken = requestCookie.match(/\b_shopify_s=([^;]+)/)?.[1];
817
+ if (legacyUniqueToken) {
818
+ defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_Y_HEADER] = legacyUniqueToken;
819
+ }
820
+ if (legacyVisitToken) {
821
+ defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_S_HEADER] = legacyVisitToken;
822
+ }
823
+ uniqueToken = legacyUniqueToken ?? generateUUID();
824
+ visitToken = legacyVisitToken ?? generateUUID();
825
+ defaultHeaders[hydrogenReact.SHOPIFY_UNIQUE_TOKEN_HEADER] = uniqueToken;
826
+ defaultHeaders[hydrogenReact.SHOPIFY_VISIT_TOKEN_HEADER] = visitToken;
728
827
  }
828
+ let collectedSubrequestHeaders;
729
829
  const cacheKeyHeader = JSON.stringify({
730
830
  "content-type": defaultHeaders["content-type"],
731
831
  "user-agent": defaultHeaders["user-agent"],
@@ -784,6 +884,13 @@ function createStorefrontClient(options) {
784
884
  stackInfo,
785
885
  graphql: graphqlData,
786
886
  purpose: storefrontHeaders?.purpose
887
+ },
888
+ onRawHeaders: (headers2) => {
889
+ collectedSubrequestHeaders ??= {
890
+ // `getSetCookie` may not be available in all environments (e.g., classic Remix compiler)
891
+ setCookie: typeof headers2.getSetCookie === "function" ? headers2.getSetCookie() : [],
892
+ serverTiming: headers2.get("server-timing") ?? ""
893
+ };
787
894
  }
788
895
  });
789
896
  const errorOptions = {
@@ -878,9 +985,90 @@ function createStorefrontClient(options) {
878
985
  generateCacheControlHeader,
879
986
  getPublicTokenHeaders,
880
987
  getPrivateTokenHeaders,
988
+ getHeaders: () => ({ ...defaultHeaders }),
881
989
  getShopifyDomain,
882
990
  getApiUrl: getStorefrontApiUrl,
883
- i18n: i18n ?? defaultI18n
991
+ i18n: i18n ?? defaultI18n,
992
+ /**
993
+ * Checks if the request is targeting the Storefront API endpoint.
994
+ */
995
+ isStorefrontApiUrl(request) {
996
+ return SFAPI_RE.test(getSafePathname(request.url ?? ""));
997
+ },
998
+ /**
999
+ * Forwards the request to the Storefront API.
1000
+ */
1001
+ async forward(request, options2) {
1002
+ const forwardedHeaders = new Headers([
1003
+ // Forward only a selected set of headers to the Storefront API
1004
+ // to avoid getting 403 errors due to unexpected headers.
1005
+ ...extractHeaders(
1006
+ (key) => request.headers.get(key),
1007
+ [
1008
+ "accept",
1009
+ "accept-encoding",
1010
+ "accept-language",
1011
+ // Access-Control headers are used for CORS preflight requests.
1012
+ "access-control-request-headers",
1013
+ "access-control-request-method",
1014
+ "content-type",
1015
+ "content-length",
1016
+ "cookie",
1017
+ "origin",
1018
+ "referer",
1019
+ "user-agent",
1020
+ STOREFRONT_ACCESS_TOKEN_HEADER,
1021
+ hydrogenReact.SHOPIFY_UNIQUE_TOKEN_HEADER,
1022
+ hydrogenReact.SHOPIFY_VISIT_TOKEN_HEADER
1023
+ ]
1024
+ ),
1025
+ // Add some headers to help with geolocalization and debugging
1026
+ ...extractHeaders(
1027
+ (key) => defaultHeaders[key],
1028
+ [
1029
+ SHOPIFY_CLIENT_IP_HEADER,
1030
+ SHOPIFY_CLIENT_IP_SIG_HEADER,
1031
+ hydrogenReact.SHOPIFY_STOREFRONT_ID_HEADER,
1032
+ STOREFRONT_REQUEST_GROUP_ID_HEADER
1033
+ ]
1034
+ )
1035
+ ]);
1036
+ if (storefrontHeaders?.buyerIp) {
1037
+ forwardedHeaders.set("x-forwarded-for", storefrontHeaders.buyerIp);
1038
+ }
1039
+ const storefrontApiVersion = options2?.storefrontApiVersion ?? getSafePathname(request.url).match(SFAPI_RE)?.[1];
1040
+ const sfapiResponse = await fetch(
1041
+ getStorefrontApiUrl({ storefrontApiVersion }),
1042
+ {
1043
+ method: request.method,
1044
+ body: request.body,
1045
+ headers: forwardedHeaders
1046
+ }
1047
+ );
1048
+ return new Response(sfapiResponse.body, sfapiResponse);
1049
+ },
1050
+ setCollectedSubrequestHeaders: (response) => {
1051
+ if (collectedSubrequestHeaders) {
1052
+ for (const value of collectedSubrequestHeaders.setCookie) {
1053
+ response.headers.append("Set-Cookie", value);
1054
+ }
1055
+ }
1056
+ const serverTiming = extractServerTimingHeader(
1057
+ collectedSubrequestHeaders?.serverTiming
1058
+ );
1059
+ const isDocumentResponse = response.headers.get("content-type")?.startsWith("text/html");
1060
+ const fallbackValues = isDocumentResponse ? { _y: uniqueToken, _s: visitToken } : void 0;
1061
+ appendServerTimingHeader(response, {
1062
+ ...fallbackValues,
1063
+ ...serverTiming
1064
+ });
1065
+ if (isDocumentResponse && collectedSubrequestHeaders && // _shopify_essential cookie is always set, but we need more than that
1066
+ collectedSubrequestHeaders.setCookie.length > 1 && serverTiming?._y && serverTiming?._s && serverTiming?._cmp) {
1067
+ appendServerTimingHeader(response, {
1068
+ [HYDROGEN_SERVER_TRACKING_KEY]: "1"
1069
+ });
1070
+ }
1071
+ }
884
1072
  }
885
1073
  };
886
1074
  }
@@ -898,21 +1086,6 @@ function formatAPIResult(data, errors2) {
898
1086
  };
899
1087
  }
900
1088
 
901
- // src/utils/request.ts
902
- function getHeader(request, key) {
903
- return getHeaderValue(request.headers, key);
904
- }
905
- function getHeaderValue(headers, key) {
906
- const value = headers?.get?.(key) ?? headers?.[key];
907
- return typeof value === "string" ? value : null;
908
- }
909
- function getDebugHeaders(request) {
910
- return {
911
- requestId: request ? getHeader(request, "request-id") : void 0,
912
- purpose: request ? getHeader(request, "purpose") : void 0
913
- };
914
- }
915
-
916
1089
  // src/cache/create-with-cache.ts
917
1090
  function createWithCache(cacheOptions) {
918
1091
  const { cache, waitUntil, request } = cacheOptions;
@@ -4222,7 +4395,7 @@ var AnalyticsEvent = {
4222
4395
  // Custom
4223
4396
  CUSTOM_EVENT: `custom_`
4224
4397
  };
4225
- var CONSENT_API = "https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.1/consent-tracking-api.js";
4398
+ var CONSENT_API = "https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.2/consent-tracking-api.js";
4226
4399
  var CONSENT_API_WITH_BANNER = "https://cdn.shopify.com/shopifycloud/privacy-banner/storefront-banner.js";
4227
4400
  function logMissingConfig(fieldName) {
4228
4401
  console.error(
@@ -4234,19 +4407,34 @@ function useCustomerPrivacy(props) {
4234
4407
  withPrivacyBanner = true,
4235
4408
  onVisitorConsentCollected,
4236
4409
  onReady,
4237
- ...consentConfig
4410
+ checkoutDomain,
4411
+ storefrontAccessToken,
4412
+ country,
4413
+ locale,
4414
+ sameDomainForStorefrontApi
4238
4415
  } = props;
4416
+ const hasSfapiProxy = react.useMemo(
4417
+ () => sameDomainForStorefrontApi ?? isSfapiProxyEnabled(),
4418
+ [sameDomainForStorefrontApi]
4419
+ );
4420
+ const fetchTrackingValuesFromBrowser = react.useMemo(
4421
+ () => hasSfapiProxy && !hasServerReturnedTrackingValues(),
4422
+ [hasSfapiProxy]
4423
+ );
4424
+ const cookiesReady = hydrogenReact.useShopifyCookies({
4425
+ fetchTrackingValues: fetchTrackingValuesFromBrowser,
4426
+ storefrontAccessToken,
4427
+ ignoreDeprecatedCookies: true
4428
+ });
4429
+ const initialTrackingValues = react.useMemo(hydrogenReact.getTrackingValues, [cookiesReady]);
4430
+ const { revalidate } = react$1.useRevalidator();
4239
4431
  hydrogenReact.useLoadScript(withPrivacyBanner ? CONSENT_API_WITH_BANNER : CONSENT_API, {
4240
4432
  attributes: {
4241
4433
  id: "customer-privacy-api"
4242
4434
  }
4243
4435
  });
4244
- const { observing, setLoaded } = useApisLoaded({
4245
- withPrivacyBanner,
4246
- onLoaded: onReady
4247
- });
4436
+ const { observing, setLoaded, apisLoaded } = useApisLoaded({ withPrivacyBanner });
4248
4437
  const config = react.useMemo(() => {
4249
- const { checkoutDomain, storefrontAccessToken } = consentConfig;
4250
4438
  if (!checkoutDomain) logMissingConfig("checkoutDomain");
4251
4439
  if (!storefrontAccessToken) logMissingConfig("storefrontAccessToken");
4252
4440
  if (storefrontAccessToken.startsWith("shpat_") || storefrontAccessToken.length !== 32) {
@@ -4254,18 +4442,50 @@ function useCustomerPrivacy(props) {
4254
4442
  `[h2:error:useCustomerPrivacy] It looks like you passed a private access token, make sure to use the public token`
4255
4443
  );
4256
4444
  }
4445
+ const commonAncestorDomain = parseStoreDomain(checkoutDomain);
4446
+ const sfapiDomain = (
4447
+ // Check if standard route proxy is enabled in Hydrogen server
4448
+ // to use it instead of doing a cross-origin request to checkout.
4449
+ hasSfapiProxy && typeof window !== "undefined" ? window.location.host : checkoutDomain
4450
+ );
4257
4451
  const config2 = {
4258
- checkoutRootDomain: checkoutDomain,
4452
+ // This domain is used to send requests to SFAPI for setting and getting consent.
4453
+ checkoutRootDomain: sfapiDomain,
4454
+ // Prefix with a dot to ensure this domain is different from checkoutRootDomain.
4455
+ // This will ensure old cookies are set for a cross-subdomain checkout setup
4456
+ // so that we keep backward compatibility until new cookies are rolled out.
4457
+ // Once consent-tracking-api is updated to not rely on cookies anymore, we can remove this.
4458
+ storefrontRootDomain: commonAncestorDomain ? "." + commonAncestorDomain : void 0,
4259
4459
  storefrontAccessToken,
4260
- storefrontRootDomain: parseStoreDomain(checkoutDomain),
4261
- country: consentConfig.country,
4262
- locale: consentConfig.locale
4460
+ country,
4461
+ locale
4263
4462
  };
4264
4463
  return config2;
4265
- }, [consentConfig, parseStoreDomain, logMissingConfig]);
4464
+ }, [
4465
+ logMissingConfig,
4466
+ checkoutDomain,
4467
+ storefrontAccessToken,
4468
+ country,
4469
+ locale
4470
+ ]);
4266
4471
  react.useEffect(() => {
4267
4472
  const consentCollectedHandler = (event) => {
4473
+ const latestTrackingValues = hydrogenReact.getTrackingValues();
4474
+ if (initialTrackingValues.visitToken !== latestTrackingValues.visitToken || initialTrackingValues.uniqueToken !== latestTrackingValues.uniqueToken) {
4475
+ revalidate();
4476
+ }
4268
4477
  if (onVisitorConsentCollected) {
4478
+ const customerPrivacy = getCustomerPrivacy();
4479
+ if (customerPrivacy?.shouldShowBanner()) {
4480
+ const consentValues = customerPrivacy.currentVisitorConsent();
4481
+ if (consentValues) {
4482
+ const NO_VALUE = "";
4483
+ const noInteraction = consentValues.marketing === NO_VALUE && consentValues.analytics === NO_VALUE && consentValues.preferences === NO_VALUE;
4484
+ if (noInteraction) {
4485
+ return;
4486
+ }
4487
+ }
4488
+ }
4269
4489
  onVisitorConsentCollected(event.detail);
4270
4490
  }
4271
4491
  };
@@ -4291,14 +4511,11 @@ function useCustomerPrivacy(props) {
4291
4511
  },
4292
4512
  set(value) {
4293
4513
  if (typeof value === "object" && value !== null && "showPreferences" in value && "loadBanner" in value) {
4294
- const privacyBanner = value;
4295
- privacyBanner.loadBanner(config);
4296
4514
  customPrivacyBanner = overridePrivacyBannerMethods({
4297
- privacyBanner,
4515
+ privacyBanner: value,
4298
4516
  config
4299
4517
  });
4300
4518
  setLoaded.privacyBanner();
4301
- emitCustomerPrivacyApiLoaded();
4302
4519
  }
4303
4520
  }
4304
4521
  };
@@ -4332,6 +4549,8 @@ function useCustomerPrivacy(props) {
4332
4549
  const customerPrivacy = value2;
4333
4550
  customCustomerPrivacy = {
4334
4551
  ...customerPrivacy,
4552
+ // Note: this method is not used by the privacy-banner,
4553
+ // it bundles its own setTrackingConsent.
4335
4554
  setTrackingConsent: overrideCustomerPrivacySetTrackingConsent(
4336
4555
  { customerPrivacy, config }
4337
4556
  )
@@ -4341,7 +4560,6 @@ function useCustomerPrivacy(props) {
4341
4560
  customerPrivacy: customCustomerPrivacy
4342
4561
  };
4343
4562
  setLoaded.customerPrivacy();
4344
- emitCustomerPrivacyApiLoaded();
4345
4563
  }
4346
4564
  }
4347
4565
  });
@@ -4353,6 +4571,24 @@ function useCustomerPrivacy(props) {
4353
4571
  overrideCustomerPrivacySetTrackingConsent,
4354
4572
  setLoaded.customerPrivacy
4355
4573
  ]);
4574
+ react.useEffect(() => {
4575
+ if (!apisLoaded || !cookiesReady) return;
4576
+ const customerPrivacy = getCustomerPrivacy();
4577
+ if (customerPrivacy && !customerPrivacy.cachedConsent) {
4578
+ const trackingValues = hydrogenReact.getTrackingValues();
4579
+ if (trackingValues.consent) {
4580
+ customerPrivacy.cachedConsent = trackingValues.consent;
4581
+ }
4582
+ }
4583
+ if (withPrivacyBanner) {
4584
+ const privacyBanner = getPrivacyBanner();
4585
+ if (privacyBanner) {
4586
+ privacyBanner.loadBanner(config);
4587
+ }
4588
+ }
4589
+ emitCustomerPrivacyApiLoaded();
4590
+ onReady?.();
4591
+ }, [apisLoaded, cookiesReady]);
4356
4592
  const result = {
4357
4593
  customerPrivacy: getCustomerPrivacy()
4358
4594
  };
@@ -4368,15 +4604,12 @@ function emitCustomerPrivacyApiLoaded() {
4368
4604
  const event = new CustomEvent("shopifyCustomerPrivacyApiLoaded");
4369
4605
  document.dispatchEvent(event);
4370
4606
  }
4371
- function useApisLoaded({
4372
- withPrivacyBanner,
4373
- onLoaded
4374
- }) {
4607
+ function useApisLoaded({ withPrivacyBanner }) {
4375
4608
  const observing = react.useRef({ customerPrivacy: false, privacyBanner: false });
4376
- const [apisLoaded, setApisLoaded] = react.useState(
4609
+ const [apisLoadedArray, setApisLoaded] = react.useState(
4377
4610
  withPrivacyBanner ? [false, false] : [false]
4378
4611
  );
4379
- const loaded = apisLoaded.every(Boolean);
4612
+ const apisLoaded = apisLoadedArray.every(Boolean);
4380
4613
  const setLoaded = {
4381
4614
  customerPrivacy: () => {
4382
4615
  if (withPrivacyBanner) {
@@ -4392,16 +4625,11 @@ function useApisLoaded({
4392
4625
  setApisLoaded((prev) => [prev[0], true]);
4393
4626
  }
4394
4627
  };
4395
- react.useEffect(() => {
4396
- if (loaded && onLoaded) {
4397
- onLoaded();
4398
- }
4399
- }, [loaded, onLoaded]);
4400
- return { observing, setLoaded };
4628
+ return { observing, setLoaded, apisLoaded };
4401
4629
  }
4402
4630
  function parseStoreDomain(checkoutDomain) {
4403
4631
  if (typeof window === "undefined") return;
4404
- const host = window.document.location.host;
4632
+ const host = window.location.host;
4405
4633
  const checkoutDomainParts = checkoutDomain.split(".").reverse();
4406
4634
  const currentDomainParts = host.split(".").reverse();
4407
4635
  const sameDomainParts = [];
@@ -4410,7 +4638,7 @@ function parseStoreDomain(checkoutDomain) {
4410
4638
  sameDomainParts.push(part);
4411
4639
  }
4412
4640
  });
4413
- return sameDomainParts.reverse().join(".");
4641
+ return sameDomainParts.reverse().join(".") || void 0;
4414
4642
  }
4415
4643
  function overrideCustomerPrivacySetTrackingConsent({
4416
4644
  customerPrivacy,
@@ -4468,7 +4696,7 @@ function getPrivacyBanner() {
4468
4696
  }
4469
4697
 
4470
4698
  // package.json
4471
- var version = "2024.7.9";
4699
+ var version = "2024.7.10";
4472
4700
 
4473
4701
  // src/analytics-manager/ShopifyAnalytics.tsx
4474
4702
  function getCustomerPrivacyRequired() {
@@ -4488,6 +4716,7 @@ function ShopifyAnalytics({
4488
4716
  const { subscribe: subscribe2, register: register2, canTrack } = useAnalytics();
4489
4717
  const [shopifyReady, setShopifyReady] = react.useState(false);
4490
4718
  const [privacyReady, setPrivacyReady] = react.useState(false);
4719
+ const [collectedConsent, setCollectedConsent] = react.useState("");
4491
4720
  const init = react.useRef(false);
4492
4721
  const { checkoutDomain, storefrontAccessToken, language } = consent;
4493
4722
  const { ready: shopifyAnalyticsReady } = register2("Internal_Shopify_Analytics");
@@ -4496,14 +4725,31 @@ function ShopifyAnalytics({
4496
4725
  locale: language,
4497
4726
  checkoutDomain: !checkoutDomain ? "mock.shop" : checkoutDomain,
4498
4727
  storefrontAccessToken: !storefrontAccessToken ? "abcdefghijklmnopqrstuvwxyz123456" : storefrontAccessToken,
4499
- onVisitorConsentCollected: () => setPrivacyReady(true),
4500
- onReady: () => setPrivacyReady(true)
4728
+ // If we use privacy banner, we should wait until consent is collected.
4729
+ // Otherwise, we can consider privacy ready immediately:
4730
+ onReady: () => !consent.withPrivacyBanner && setPrivacyReady(true),
4731
+ onVisitorConsentCollected: (consent2) => {
4732
+ try {
4733
+ setCollectedConsent(JSON.stringify(consent2));
4734
+ } catch (e) {
4735
+ }
4736
+ setPrivacyReady(true);
4737
+ }
4501
4738
  });
4739
+ const hasUserConsent = react.useMemo(
4740
+ // must be initialized with true to avoid removing cookies too early
4741
+ () => privacyReady ? canTrack() : true,
4742
+ // Make this value depend on collectedConsent to re-run `canTrack()` when consent changes
4743
+ [privacyReady, canTrack, collectedConsent]
4744
+ );
4502
4745
  hydrogenReact.useShopifyCookies({
4503
- hasUserConsent: privacyReady ? canTrack() : true,
4504
- // must be initialized with true
4746
+ hasUserConsent,
4505
4747
  domain,
4506
- checkoutDomain
4748
+ checkoutDomain,
4749
+ // Already done inside useCustomerPrivacy
4750
+ fetchTrackingValues: false,
4751
+ // Avoid creating local cookies too early
4752
+ ignoreDeprecatedCookies: !privacyReady
4507
4753
  });
4508
4754
  react.useEffect(() => {
4509
4755
  if (init.current) return;
@@ -4553,11 +4799,11 @@ function prepareBasePageViewPayload(payload) {
4553
4799
  ...payload.shop,
4554
4800
  hasUserConsent,
4555
4801
  ...hydrogenReact.getClientBrowserParameters(),
4556
- ccpaEnforced: !customerPrivacy.saleOfDataAllowed(),
4557
- gdprEnforced: !(customerPrivacy.marketingAllowed() && customerPrivacy.analyticsProcessingAllowed()),
4558
4802
  analyticsAllowed: customerPrivacy.analyticsProcessingAllowed(),
4559
4803
  marketingAllowed: customerPrivacy.marketingAllowed(),
4560
- saleOfDataAllowed: customerPrivacy.saleOfDataAllowed()
4804
+ saleOfDataAllowed: customerPrivacy.saleOfDataAllowed(),
4805
+ ccpaEnforced: !customerPrivacy.saleOfDataAllowed(),
4806
+ gdprEnforced: !(customerPrivacy.marketingAllowed() && customerPrivacy.analyticsProcessingAllowed())
4561
4807
  };
4562
4808
  return eventPayload;
4563
4809
  }
@@ -4974,11 +5220,11 @@ function AnalyticsProvider({
4974
5220
  shop: shopProp = null,
4975
5221
  cookieDomain
4976
5222
  }) {
4977
- const listenerSet = react.useRef(false);
4978
5223
  const { shop } = useShopAnalytics(shopProp);
4979
5224
  const [analyticsLoaded, setAnalyticsLoaded] = react.useState(
4980
5225
  customCanTrack ? true : false
4981
5226
  );
5227
+ const [consentCollected, setConsentCollected] = react.useState(false);
4982
5228
  const [carts, setCarts] = react.useState({ cart: null, prevCart: null });
4983
5229
  const [canTrack, setCanTrack] = react.useState(
4984
5230
  customCanTrack ? () => customCanTrack : () => shopifyCanTrack
@@ -5046,21 +5292,21 @@ function AnalyticsProvider({
5046
5292
  children,
5047
5293
  !!shop && /* @__PURE__ */ jsxRuntime.jsx(AnalyticsPageView, {}),
5048
5294
  !!shop && !!currentCart && /* @__PURE__ */ jsxRuntime.jsx(CartAnalytics, { cart: currentCart, setCarts }),
5049
- !!shop && consent.checkoutDomain && /* @__PURE__ */ jsxRuntime.jsx(
5295
+ !!shop && /* @__PURE__ */ jsxRuntime.jsx(
5050
5296
  ShopifyAnalytics,
5051
5297
  {
5052
5298
  consent,
5053
5299
  onReady: () => {
5054
- listenerSet.current = true;
5055
5300
  setAnalyticsLoaded(true);
5056
5301
  setCanTrack(
5057
5302
  customCanTrack ? () => customCanTrack : () => shopifyCanTrack
5058
5303
  );
5304
+ setConsentCollected(true);
5059
5305
  },
5060
5306
  domain: cookieDomain
5061
5307
  }
5062
5308
  ),
5063
- !!shop && /* @__PURE__ */ jsxRuntime.jsx(PerfKit, { shop })
5309
+ !!shop && consentCollected && /* @__PURE__ */ jsxRuntime.jsx(PerfKit, { shop })
5064
5310
  ] });
5065
5311
  }
5066
5312
  function useAnalytics() {
@@ -5221,8 +5467,63 @@ function getStorefrontHeaders(request) {
5221
5467
  return {
5222
5468
  requestGroupId: getHeader(request, "request-id"),
5223
5469
  buyerIp: getHeader(request, "oxygen-buyer-ip"),
5470
+ buyerIpSig: getHeader(request, "X-Shopify-Client-IP-Sig"),
5224
5471
  cookie: getHeader(request, "cookie"),
5225
- purpose: getHeader(request, "purpose")
5472
+ purpose: getHeader(request, "sec-purpose") || getHeader(request, "purpose")
5473
+ };
5474
+ }
5475
+ function createRequestHandler({
5476
+ build,
5477
+ mode,
5478
+ poweredByHeader = true,
5479
+ getLoadContext,
5480
+ collectTrackingInformation = true,
5481
+ proxyStandardRoutes = true
5482
+ }) {
5483
+ const handleRequest = serverRuntime.createRequestHandler(build, mode);
5484
+ const appendPoweredByHeader = poweredByHeader ? (response) => response.headers.append("powered-by", "Shopify, Hydrogen") : void 0;
5485
+ return async (request) => {
5486
+ const method = request.method;
5487
+ if ((method === "GET" || method === "HEAD") && request.body) {
5488
+ return new Response(`${method} requests cannot have a body`, {
5489
+ status: 400
5490
+ });
5491
+ }
5492
+ const url = new URL(request.url);
5493
+ if (url.pathname.includes("//")) {
5494
+ return new Response(null, {
5495
+ status: 301,
5496
+ headers: {
5497
+ location: url.pathname.replace(/\/+/g, "/")
5498
+ }
5499
+ });
5500
+ }
5501
+ const context = getLoadContext ? await getLoadContext(request) : void 0;
5502
+ const storefront = context?.storefront;
5503
+ if (proxyStandardRoutes) {
5504
+ if (!storefront) {
5505
+ warnOnce(
5506
+ "[h2:createRequestHandler] Storefront instance is required to proxy standard routes."
5507
+ );
5508
+ }
5509
+ if (storefront?.isStorefrontApiUrl(request)) {
5510
+ const response2 = await storefront.forward(request);
5511
+ appendPoweredByHeader?.(response2);
5512
+ return response2;
5513
+ }
5514
+ }
5515
+ const response = await handleRequest(request, context);
5516
+ if (storefront && proxyStandardRoutes) {
5517
+ if (collectTrackingInformation) {
5518
+ storefront.setCollectedSubrequestHeaders(response);
5519
+ }
5520
+ const fetchDest = request.headers.get("sec-fetch-dest");
5521
+ if (fetchDest && fetchDest === "document" || request.headers.get("accept")?.includes("text/html")) {
5522
+ appendServerTimingHeader(response, { [HYDROGEN_SFAPI_PROXY_KEY]: "1" });
5523
+ }
5524
+ }
5525
+ appendPoweredByHeader?.(response);
5526
+ return response;
5226
5527
  };
5227
5528
  }
5228
5529
 
@@ -5565,6 +5866,10 @@ Object.defineProperty(exports, "getShopifyCookies", {
5565
5866
  enumerable: true,
5566
5867
  get: function () { return hydrogenReact.getShopifyCookies; }
5567
5868
  });
5869
+ Object.defineProperty(exports, "getTrackingValues", {
5870
+ enumerable: true,
5871
+ get: function () { return hydrogenReact.getTrackingValues; }
5872
+ });
5568
5873
  Object.defineProperty(exports, "parseGid", {
5569
5874
  enumerable: true,
5570
5875
  get: function () { return hydrogenReact.parseGid; }
@@ -5628,6 +5933,7 @@ exports.createCartHandler = createCartHandler;
5628
5933
  exports.createContentSecurityPolicy = createContentSecurityPolicy;
5629
5934
  exports.createCustomerAccountClient = createCustomerAccountClient;
5630
5935
  exports.createHydrogenContext = createHydrogenContext;
5936
+ exports.createRequestHandler = createRequestHandler;
5631
5937
  exports.createStorefrontClient = createStorefrontClient;
5632
5938
  exports.createWithCache = createWithCache;
5633
5939
  exports.formatAPIResult = formatAPIResult;