@shopify/hydrogen 2026.4.5 → 2026.4.6
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.
- package/README.md +1 -1
- package/dist/development/index.cjs +222 -369
- package/dist/development/index.cjs.map +1 -1
- package/dist/development/index.js +225 -372
- package/dist/development/index.js.map +1 -1
- package/dist/oxygen/index.d.ts +39 -7
- package/dist/production/index.cjs +51 -51
- package/dist/production/index.cjs.map +1 -1
- package/dist/production/index.d.cts +46 -20
- package/dist/production/index.d.ts +46 -20
- package/dist/production/index.js +51 -51
- package/dist/production/index.js.map +1 -1
- package/dist/vite/request-events.d.ts +39 -7
- package/package.json +2 -2
|
@@ -235,6 +235,25 @@ function AnalyticsCustomView(props) {
|
|
|
235
235
|
return /* @__PURE__ */ jsxRuntime.jsx(AnalyticsView, { ...props });
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// src/analytics-manager/consent.ts
|
|
239
|
+
function hasAnalyticsConsent() {
|
|
240
|
+
try {
|
|
241
|
+
const privacy = window.Shopify?.customerPrivacy;
|
|
242
|
+
if (privacy?.consentStatus !== "loaded") return false;
|
|
243
|
+
if (privacy.currentVisitorConsent?.().analytics === "no") return false;
|
|
244
|
+
return privacy.analyticsProcessingAllowed?.() ?? false;
|
|
245
|
+
} catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function shouldWaitForPrivacyBanner(privacy) {
|
|
250
|
+
if (!privacy.shouldShowBanner()) return false;
|
|
251
|
+
const consent = privacy.currentVisitorConsent();
|
|
252
|
+
return ![consent.marketing, consent.analytics, consent.preferences].some(
|
|
253
|
+
(value) => value === "yes" || value === "no"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
238
257
|
// src/analytics-manager/events.ts
|
|
239
258
|
var AnalyticsEvent = {
|
|
240
259
|
// Views
|
|
@@ -250,61 +269,6 @@ var AnalyticsEvent = {
|
|
|
250
269
|
// Custom
|
|
251
270
|
CUSTOM_EVENT: `custom_`
|
|
252
271
|
};
|
|
253
|
-
|
|
254
|
-
// src/constants.ts
|
|
255
|
-
var STOREFRONT_REQUEST_GROUP_ID_HEADER = "Custom-Storefront-Request-Group-ID";
|
|
256
|
-
var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
|
|
257
|
-
var SDK_VARIANT_HEADER = "X-SDK-Variant";
|
|
258
|
-
var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
|
|
259
|
-
var SDK_VERSION_HEADER = "X-SDK-Version";
|
|
260
|
-
var SHOPIFY_CLIENT_IP_HEADER = "X-Shopify-Client-IP";
|
|
261
|
-
var SHOPIFY_CLIENT_IP_SIG_HEADER = "X-Shopify-Client-IP-Sig";
|
|
262
|
-
var HYDROGEN_SFAPI_PROXY_KEY = "_sfapi_proxy";
|
|
263
|
-
var HYDROGEN_SERVER_TRACKING_KEY = "_server_tracking";
|
|
264
|
-
|
|
265
|
-
// src/utils/server-timing.ts
|
|
266
|
-
function buildServerTimingHeader(values) {
|
|
267
|
-
return Object.entries(values).map(([key, value]) => value ? `${key};desc=${value}` : void 0).filter(Boolean).join(", ");
|
|
268
|
-
}
|
|
269
|
-
function appendServerTimingHeader(response, values) {
|
|
270
|
-
const header = typeof values === "string" ? values : buildServerTimingHeader(values);
|
|
271
|
-
if (header) {
|
|
272
|
-
response.headers.append("Server-Timing", header);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
var trackedTimings = ["_y", "_s", "_cmp"];
|
|
276
|
-
function extractServerTimingHeader(serverTimingHeader) {
|
|
277
|
-
const values = {};
|
|
278
|
-
if (!serverTimingHeader) return values;
|
|
279
|
-
const re = new RegExp(
|
|
280
|
-
`\\b(${trackedTimings.join("|")});desc="?([^",]+)"?`,
|
|
281
|
-
"g"
|
|
282
|
-
);
|
|
283
|
-
let match;
|
|
284
|
-
while ((match = re.exec(serverTimingHeader)) !== null) {
|
|
285
|
-
values[match[1]] = match[2];
|
|
286
|
-
}
|
|
287
|
-
return values;
|
|
288
|
-
}
|
|
289
|
-
function hasServerTimingInNavigationEntry(key) {
|
|
290
|
-
if (typeof window === "undefined") return false;
|
|
291
|
-
try {
|
|
292
|
-
const navigationEntry = window.performance.getEntriesByType(
|
|
293
|
-
"navigation"
|
|
294
|
-
)[0];
|
|
295
|
-
return !!navigationEntry?.serverTiming?.some((entry) => entry.name === key);
|
|
296
|
-
} catch (e) {
|
|
297
|
-
return false;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
function isSfapiProxyEnabled() {
|
|
301
|
-
return hasServerTimingInNavigationEntry(HYDROGEN_SFAPI_PROXY_KEY);
|
|
302
|
-
}
|
|
303
|
-
function hasServerReturnedTrackingValues() {
|
|
304
|
-
return hasServerTimingInNavigationEntry(HYDROGEN_SERVER_TRACKING_KEY);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// src/customer-privacy/ShopifyCustomerPrivacy.tsx
|
|
308
272
|
var CONSENT_API = "https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.2/consent-tracking-api.js";
|
|
309
273
|
var CONSENT_API_WITH_BANNER = "https://cdn.shopify.com/shopifycloud/privacy-banner/storefront-banner.js";
|
|
310
274
|
function logMissingConfig(fieldName) {
|
|
@@ -312,6 +276,11 @@ function logMissingConfig(fieldName) {
|
|
|
312
276
|
`[h2:error:useCustomerPrivacy] Unable to setup Customer Privacy API: Missing consent.${fieldName} configuration.`
|
|
313
277
|
);
|
|
314
278
|
}
|
|
279
|
+
function logIgnoredCrossDomainConsent() {
|
|
280
|
+
console.warn(
|
|
281
|
+
"[h2:warn:useCustomerPrivacy] `sameDomainForStorefrontApi: false` is ignored. Consent and analytics require the same-origin Storefront API proxy included in Hydrogen's `createRequestHandler`, so consent requests always use it. Remove this option. See https://shopify.dev/changelog/posts/tracking-cookie-deprecation-hydrogen"
|
|
282
|
+
);
|
|
283
|
+
}
|
|
315
284
|
function useCustomerPrivacy(props) {
|
|
316
285
|
const {
|
|
317
286
|
withPrivacyBanner = false,
|
|
@@ -323,27 +292,15 @@ function useCustomerPrivacy(props) {
|
|
|
323
292
|
locale,
|
|
324
293
|
sameDomainForStorefrontApi
|
|
325
294
|
} = props;
|
|
326
|
-
const hasSfapiProxy = react.useMemo(
|
|
327
|
-
() => sameDomainForStorefrontApi ?? isSfapiProxyEnabled(),
|
|
328
|
-
[sameDomainForStorefrontApi]
|
|
329
|
-
);
|
|
330
|
-
const fetchTrackingValuesFromBrowser = react.useMemo(
|
|
331
|
-
() => hasSfapiProxy && !hasServerReturnedTrackingValues(),
|
|
332
|
-
[hasSfapiProxy]
|
|
333
|
-
);
|
|
334
|
-
const cookiesReady = hydrogenReact.useShopifyCookies({
|
|
335
|
-
fetchTrackingValues: fetchTrackingValuesFromBrowser,
|
|
336
|
-
storefrontAccessToken,
|
|
337
|
-
ignoreDeprecatedCookies: true
|
|
338
|
-
});
|
|
339
|
-
const initialTrackingValues = react.useMemo(hydrogenReact.getTrackingValues, [cookiesReady]);
|
|
340
295
|
const { revalidate } = reactRouter.useRevalidator();
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
296
|
+
const callbacks = react.useRef({ onReady, onVisitorConsentCollected, revalidate });
|
|
297
|
+
callbacks.current = { onReady, onVisitorConsentCollected, revalidate };
|
|
298
|
+
const notifiedReady = react.useRef(false);
|
|
299
|
+
const previousTrackingValues = react.useRef();
|
|
300
|
+
const [apis, setApis] = react.useState(() => ({
|
|
301
|
+
customerPrivacy: getCustomerPrivacy(),
|
|
302
|
+
privacyBanner: getPrivacyBanner()
|
|
303
|
+
}));
|
|
347
304
|
const config = react.useMemo(() => {
|
|
348
305
|
if (!checkoutDomain) logMissingConfig("checkoutDomain");
|
|
349
306
|
if (!storefrontAccessToken) logMissingConfig("storefrontAccessToken");
|
|
@@ -352,196 +309,155 @@ function useCustomerPrivacy(props) {
|
|
|
352
309
|
`[h2:error:useCustomerPrivacy] It looks like you passed a private access token, make sure to use the public token`
|
|
353
310
|
);
|
|
354
311
|
}
|
|
312
|
+
if (sameDomainForStorefrontApi === false) logIgnoredCrossDomainConsent();
|
|
355
313
|
const commonAncestorDomain = parseStoreDomain(checkoutDomain);
|
|
356
|
-
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
// This domain is used to send requests to SFAPI for setting and getting consent.
|
|
363
|
-
checkoutRootDomain: sfapiDomain,
|
|
364
|
-
// Prefix with a dot to ensure this domain is different from checkoutRootDomain.
|
|
365
|
-
// This will ensure old cookies are set for a cross-subdomain checkout setup
|
|
366
|
-
// so that we keep backward compatibility until new cookies are rolled out.
|
|
367
|
-
// Once consent-tracking-api is updated to not rely on cookies anymore, we can remove this.
|
|
314
|
+
return {
|
|
315
|
+
// Always the same-origin proxy: cross-origin consent requests carry no
|
|
316
|
+
// cookies, so consent and visitor tokens would reset on every page load
|
|
317
|
+
// (and an opt-out would be forgotten). The server render never sends
|
|
318
|
+
// consent requests, so it keeps the checkout domain.
|
|
319
|
+
checkoutRootDomain: typeof window !== "undefined" ? window.location.host : checkoutDomain,
|
|
368
320
|
storefrontRootDomain: commonAncestorDomain ? "." + commonAncestorDomain : void 0,
|
|
369
321
|
storefrontAccessToken,
|
|
370
322
|
country,
|
|
371
323
|
locale
|
|
372
324
|
};
|
|
373
|
-
return config2;
|
|
374
325
|
}, [
|
|
375
|
-
|
|
326
|
+
sameDomainForStorefrontApi,
|
|
376
327
|
checkoutDomain,
|
|
377
328
|
storefrontAccessToken,
|
|
378
329
|
country,
|
|
379
330
|
locale
|
|
380
331
|
]);
|
|
381
332
|
react.useEffect(() => {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
333
|
+
let active = true;
|
|
334
|
+
const shopify = window.Shopify ??= {};
|
|
335
|
+
const privacy = shopify.customerPrivacy ??= {};
|
|
336
|
+
privacy.config = {
|
|
337
|
+
...privacy.config,
|
|
338
|
+
isHeadless: true,
|
|
339
|
+
asyncConsent: true,
|
|
340
|
+
asyncVisitorState: true,
|
|
341
|
+
consentDomain: config.checkoutRootDomain,
|
|
342
|
+
storefrontAccessToken: config.storefrontAccessToken,
|
|
343
|
+
debug: {
|
|
344
|
+
...privacy.config?.debug,
|
|
345
|
+
hydrogen: HYDROGEN_DEBUG_METADATA
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
if (config.country) shopify.country = config.country;
|
|
349
|
+
if (config.locale) shopify.locale = config.locale.toLowerCase();
|
|
350
|
+
const updateTrackingValues = () => {
|
|
351
|
+
const latest = hydrogenReact.getTrackingValues();
|
|
352
|
+
const previous = previousTrackingValues.current;
|
|
353
|
+
previousTrackingValues.current = latest;
|
|
354
|
+
if (previous && (previous.visitToken !== latest.visitToken || previous.uniqueToken !== latest.uniqueToken)) {
|
|
355
|
+
callbacks.current.revalidate().catch(() => {
|
|
386
356
|
console.warn(
|
|
387
357
|
"[h2:warn:useCustomerPrivacy] Revalidation failed after consent change."
|
|
388
358
|
);
|
|
389
359
|
});
|
|
390
360
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
onVisitorConsentCollected(event.detail);
|
|
361
|
+
};
|
|
362
|
+
const checkReady = () => {
|
|
363
|
+
if (!active) return false;
|
|
364
|
+
const customerPrivacy = getCustomerPrivacy();
|
|
365
|
+
const privacyBanner = getPrivacyBanner();
|
|
366
|
+
if (customerPrivacy) configureCustomerPrivacy(customerPrivacy, config);
|
|
367
|
+
if (withPrivacyBanner && privacyBanner) {
|
|
368
|
+
configurePrivacyBanner(privacyBanner, config);
|
|
404
369
|
}
|
|
370
|
+
setApis(
|
|
371
|
+
(previous) => previous.customerPrivacy === customerPrivacy && previous.privacyBanner === privacyBanner ? previous : { customerPrivacy, privacyBanner }
|
|
372
|
+
);
|
|
373
|
+
if (customerPrivacy?.consentStatus !== "loaded" || withPrivacyBanner && !privacyBanner) {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
if (!notifiedReady.current) {
|
|
377
|
+
notifiedReady.current = true;
|
|
378
|
+
updateTrackingValues();
|
|
379
|
+
emitCustomerPrivacyApiLoaded(customerPrivacy);
|
|
380
|
+
callbacks.current.onReady?.();
|
|
381
|
+
}
|
|
382
|
+
return true;
|
|
405
383
|
};
|
|
384
|
+
const consentCollectedHandler = (event) => {
|
|
385
|
+
if (!checkReady()) return;
|
|
386
|
+
updateTrackingValues();
|
|
387
|
+
callbacks.current.onVisitorConsentCollected?.(event.detail);
|
|
388
|
+
};
|
|
389
|
+
document.addEventListener("consentTrackingApiLoaded", checkReady);
|
|
406
390
|
document.addEventListener(
|
|
407
391
|
"visitorConsentCollected",
|
|
408
392
|
consentCollectedHandler
|
|
409
393
|
);
|
|
394
|
+
if (!checkReady()) {
|
|
395
|
+
void loadScript.loadScript(
|
|
396
|
+
withPrivacyBanner ? CONSENT_API_WITH_BANNER : CONSENT_API,
|
|
397
|
+
{
|
|
398
|
+
attributes: { id: "customer-privacy-api" }
|
|
399
|
+
}
|
|
400
|
+
).then(checkReady, () => {
|
|
401
|
+
if (active) {
|
|
402
|
+
console.error(
|
|
403
|
+
"[h2:error:useCustomerPrivacy] Unable to load the Customer Privacy API."
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
}
|
|
410
408
|
return () => {
|
|
409
|
+
active = false;
|
|
410
|
+
document.removeEventListener("consentTrackingApiLoaded", checkReady);
|
|
411
411
|
document.removeEventListener(
|
|
412
412
|
"visitorConsentCollected",
|
|
413
413
|
consentCollectedHandler
|
|
414
414
|
);
|
|
415
415
|
};
|
|
416
|
-
}, [
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
let customPrivacyBanner = window.privacyBanner || void 0;
|
|
421
|
-
const privacyBannerWatcher = {
|
|
422
|
-
configurable: true,
|
|
423
|
-
get() {
|
|
424
|
-
return customPrivacyBanner;
|
|
425
|
-
},
|
|
426
|
-
set(value) {
|
|
427
|
-
if (typeof value === "object" && value !== null && "showPreferences" in value && "loadBanner" in value) {
|
|
428
|
-
customPrivacyBanner = overridePrivacyBannerMethods({
|
|
429
|
-
privacyBanner: value,
|
|
430
|
-
config
|
|
431
|
-
});
|
|
432
|
-
setLoaded.privacyBanner();
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
};
|
|
436
|
-
Object.defineProperty(window, "privacyBanner", privacyBannerWatcher);
|
|
437
|
-
}, [
|
|
438
|
-
withPrivacyBanner,
|
|
439
|
-
config,
|
|
440
|
-
overridePrivacyBannerMethods,
|
|
441
|
-
setLoaded.privacyBanner
|
|
442
|
-
]);
|
|
443
|
-
react.useEffect(() => {
|
|
444
|
-
if (observing.current.customerPrivacy) return;
|
|
445
|
-
observing.current.customerPrivacy = true;
|
|
446
|
-
let backendConsentStub = null;
|
|
447
|
-
let fullCustomerPrivacy = null;
|
|
448
|
-
let customShopify = window.Shopify || void 0;
|
|
449
|
-
Object.defineProperty(window, "Shopify", {
|
|
450
|
-
configurable: true,
|
|
451
|
-
get() {
|
|
452
|
-
return customShopify;
|
|
453
|
-
},
|
|
454
|
-
set(value) {
|
|
455
|
-
if (typeof value === "object" && value !== null && Object.keys(value).length === 0) {
|
|
456
|
-
customShopify = value;
|
|
457
|
-
backendConsentStub = { backendConsentEnabled: true };
|
|
458
|
-
Object.defineProperty(window.Shopify, "customerPrivacy", {
|
|
459
|
-
configurable: true,
|
|
460
|
-
get() {
|
|
461
|
-
return fullCustomerPrivacy ?? backendConsentStub;
|
|
462
|
-
},
|
|
463
|
-
set(value2) {
|
|
464
|
-
if (typeof value2 === "object" && value2 !== null && "setTrackingConsent" in value2) {
|
|
465
|
-
const customerPrivacy = value2;
|
|
466
|
-
fullCustomerPrivacy = {
|
|
467
|
-
...customerPrivacy,
|
|
468
|
-
// Note: this method is not used by the privacy-banner,
|
|
469
|
-
// it bundles its own setTrackingConsent.
|
|
470
|
-
setTrackingConsent: overrideCustomerPrivacySetTrackingConsent(
|
|
471
|
-
{ customerPrivacy, config }
|
|
472
|
-
)
|
|
473
|
-
};
|
|
474
|
-
customShopify = {
|
|
475
|
-
...customShopify,
|
|
476
|
-
customerPrivacy: fullCustomerPrivacy
|
|
477
|
-
};
|
|
478
|
-
setLoaded.customerPrivacy();
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
});
|
|
485
|
-
}, [
|
|
486
|
-
config,
|
|
487
|
-
overrideCustomerPrivacySetTrackingConsent,
|
|
488
|
-
setLoaded.customerPrivacy
|
|
489
|
-
]);
|
|
490
|
-
react.useEffect(() => {
|
|
491
|
-
if (!apisLoaded || !cookiesReady) return;
|
|
492
|
-
const customerPrivacy = getCustomerPrivacy();
|
|
493
|
-
if (customerPrivacy && !customerPrivacy.cachedConsent) {
|
|
494
|
-
const trackingValues = hydrogenReact.getTrackingValues();
|
|
495
|
-
if (trackingValues.consent) {
|
|
496
|
-
customerPrivacy.cachedConsent = trackingValues.consent;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
if (withPrivacyBanner) {
|
|
500
|
-
const privacyBanner = getPrivacyBanner();
|
|
501
|
-
if (privacyBanner) {
|
|
502
|
-
privacyBanner.loadBanner(config);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
emitCustomerPrivacyApiLoaded();
|
|
506
|
-
onReady?.();
|
|
507
|
-
}, [apisLoaded, cookiesReady]);
|
|
508
|
-
const result = {
|
|
509
|
-
customerPrivacy: getCustomerPrivacy()
|
|
416
|
+
}, [config, withPrivacyBanner]);
|
|
417
|
+
return {
|
|
418
|
+
customerPrivacy: apis.customerPrivacy,
|
|
419
|
+
...withPrivacyBanner ? { privacyBanner: apis.privacyBanner } : {}
|
|
510
420
|
};
|
|
511
|
-
if (withPrivacyBanner) {
|
|
512
|
-
result.privacyBanner = getPrivacyBanner();
|
|
513
|
-
}
|
|
514
|
-
return result;
|
|
515
421
|
}
|
|
516
|
-
var
|
|
517
|
-
function emitCustomerPrivacyApiLoaded() {
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
setApisLoaded((prev) => [prev[0], true]);
|
|
542
|
-
}
|
|
422
|
+
var readyApis = /* @__PURE__ */ new WeakSet();
|
|
423
|
+
function emitCustomerPrivacyApiLoaded(customerPrivacy) {
|
|
424
|
+
if (readyApis.has(customerPrivacy)) return;
|
|
425
|
+
readyApis.add(customerPrivacy);
|
|
426
|
+
document.dispatchEvent(new CustomEvent("shopifyCustomerPrivacyApiLoaded"));
|
|
427
|
+
}
|
|
428
|
+
var HYDROGEN_DEBUG_METADATA = { generation: 2, serverTiming: false };
|
|
429
|
+
var configuredCustomerPrivacy = /* @__PURE__ */ new WeakMap();
|
|
430
|
+
function configureCustomerPrivacy(customerPrivacy, config) {
|
|
431
|
+
const existing = configuredCustomerPrivacy.get(customerPrivacy);
|
|
432
|
+
if (existing) {
|
|
433
|
+
existing.config = config;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const state = { config };
|
|
437
|
+
configuredCustomerPrivacy.set(customerPrivacy, state);
|
|
438
|
+
const original = customerPrivacy.setTrackingConsent;
|
|
439
|
+
customerPrivacy.setTrackingConsent = (consent, callback) => {
|
|
440
|
+
const { locale, country, ...headlessConfig } = state.config;
|
|
441
|
+
const params = {
|
|
442
|
+
...headlessConfig,
|
|
443
|
+
headlessStorefront: true,
|
|
444
|
+
...consent
|
|
445
|
+
};
|
|
446
|
+
original.call(customerPrivacy, params, callback);
|
|
543
447
|
};
|
|
544
|
-
|
|
448
|
+
}
|
|
449
|
+
var configuredPrivacyBanners = /* @__PURE__ */ new WeakMap();
|
|
450
|
+
function configurePrivacyBanner(privacyBanner, config) {
|
|
451
|
+
const existing = configuredPrivacyBanners.get(privacyBanner);
|
|
452
|
+
if (existing) {
|
|
453
|
+
existing.config = config;
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const state = { config };
|
|
457
|
+
configuredPrivacyBanners.set(privacyBanner, state);
|
|
458
|
+
const { loadBanner, showPreferences } = privacyBanner;
|
|
459
|
+
privacyBanner.loadBanner = (options) => loadBanner.call(privacyBanner, { ...state.config, ...options });
|
|
460
|
+
privacyBanner.showPreferences = (options) => showPreferences.call(privacyBanner, { ...state.config, ...options });
|
|
545
461
|
}
|
|
546
462
|
function parseStoreDomain(checkoutDomain) {
|
|
547
463
|
if (typeof window === "undefined") return;
|
|
@@ -556,64 +472,25 @@ function parseStoreDomain(checkoutDomain) {
|
|
|
556
472
|
});
|
|
557
473
|
return sameDomainParts.reverse().join(".") || void 0;
|
|
558
474
|
}
|
|
559
|
-
function overrideCustomerPrivacySetTrackingConsent({
|
|
560
|
-
customerPrivacy,
|
|
561
|
-
config
|
|
562
|
-
}) {
|
|
563
|
-
const original = customerPrivacy.setTrackingConsent;
|
|
564
|
-
const { locale, country, ...rest } = config;
|
|
565
|
-
function updatedSetTrackingConsent(consent, callback) {
|
|
566
|
-
original(
|
|
567
|
-
{
|
|
568
|
-
...rest,
|
|
569
|
-
headlessStorefront: true,
|
|
570
|
-
...consent
|
|
571
|
-
},
|
|
572
|
-
callback
|
|
573
|
-
);
|
|
574
|
-
}
|
|
575
|
-
return updatedSetTrackingConsent;
|
|
576
|
-
}
|
|
577
|
-
function overridePrivacyBannerMethods({
|
|
578
|
-
privacyBanner,
|
|
579
|
-
config
|
|
580
|
-
}) {
|
|
581
|
-
const originalLoadBanner = privacyBanner.loadBanner;
|
|
582
|
-
const originalShowPreferences = privacyBanner.showPreferences;
|
|
583
|
-
function loadBanner(userConfig) {
|
|
584
|
-
if (typeof userConfig === "object") {
|
|
585
|
-
originalLoadBanner({ ...config, ...userConfig });
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
originalLoadBanner(config);
|
|
589
|
-
}
|
|
590
|
-
function showPreferences(userConfig) {
|
|
591
|
-
if (typeof userConfig === "object") {
|
|
592
|
-
originalShowPreferences({ ...config, ...userConfig });
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
originalShowPreferences(config);
|
|
596
|
-
}
|
|
597
|
-
return { loadBanner, showPreferences };
|
|
598
|
-
}
|
|
599
475
|
function getCustomerPrivacy() {
|
|
600
476
|
try {
|
|
601
477
|
const cp = window.Shopify?.customerPrivacy;
|
|
602
|
-
return cp
|
|
478
|
+
return typeof cp?.setTrackingConsent === "function" ? cp : null;
|
|
603
479
|
} catch (e) {
|
|
604
480
|
return null;
|
|
605
481
|
}
|
|
606
482
|
}
|
|
607
483
|
function getPrivacyBanner() {
|
|
608
484
|
try {
|
|
609
|
-
|
|
485
|
+
const banner = window.privacyBanner;
|
|
486
|
+
return typeof banner?.loadBanner === "function" && typeof banner.showPreferences === "function" ? banner : null;
|
|
610
487
|
} catch (e) {
|
|
611
488
|
return null;
|
|
612
489
|
}
|
|
613
490
|
}
|
|
614
491
|
|
|
615
492
|
// package.json
|
|
616
|
-
var version = "2026.4.
|
|
493
|
+
var version = "2026.4.6";
|
|
617
494
|
|
|
618
495
|
// src/analytics-manager/ShopifyAnalytics.tsx
|
|
619
496
|
function getCustomerPrivacyRequired() {
|
|
@@ -627,47 +504,13 @@ function getCustomerPrivacyRequired() {
|
|
|
627
504
|
}
|
|
628
505
|
function ShopifyAnalytics({
|
|
629
506
|
consent,
|
|
630
|
-
|
|
631
|
-
domain
|
|
507
|
+
onConsentChange
|
|
632
508
|
}) {
|
|
633
|
-
const { subscribe: subscribe2, register: register2
|
|
634
|
-
const [shopifyReady, setShopifyReady] = react.useState(false);
|
|
509
|
+
const { subscribe: subscribe2, register: register2 } = useAnalytics();
|
|
635
510
|
const [privacyReady, setPrivacyReady] = react.useState(false);
|
|
636
|
-
const [collectedConsent, setCollectedConsent] = react.useState("");
|
|
637
511
|
const init = react.useRef(false);
|
|
638
512
|
const { checkoutDomain, storefrontAccessToken, language } = consent;
|
|
639
513
|
const { ready: shopifyAnalyticsReady } = register2("Internal_Shopify_Analytics");
|
|
640
|
-
useCustomerPrivacy({
|
|
641
|
-
...consent,
|
|
642
|
-
locale: language,
|
|
643
|
-
checkoutDomain: !checkoutDomain ? "mock.shop" : checkoutDomain,
|
|
644
|
-
storefrontAccessToken: !storefrontAccessToken ? "abcdefghijklmnopqrstuvwxyz123456" : storefrontAccessToken,
|
|
645
|
-
// If we use privacy banner, we should wait until consent is collected.
|
|
646
|
-
// Otherwise, we can consider privacy ready immediately:
|
|
647
|
-
onReady: () => !consent.withPrivacyBanner && setPrivacyReady(true),
|
|
648
|
-
onVisitorConsentCollected: (consent2) => {
|
|
649
|
-
try {
|
|
650
|
-
setCollectedConsent(JSON.stringify(consent2));
|
|
651
|
-
} catch (e) {
|
|
652
|
-
}
|
|
653
|
-
setPrivacyReady(true);
|
|
654
|
-
}
|
|
655
|
-
});
|
|
656
|
-
const hasUserConsent = react.useMemo(
|
|
657
|
-
// must be initialized with true to avoid removing cookies too early
|
|
658
|
-
() => privacyReady ? canTrack() : true,
|
|
659
|
-
// Make this value depend on collectedConsent to re-run `canTrack()` when consent changes
|
|
660
|
-
[privacyReady, canTrack, collectedConsent]
|
|
661
|
-
);
|
|
662
|
-
hydrogenReact.useShopifyCookies({
|
|
663
|
-
hasUserConsent,
|
|
664
|
-
domain,
|
|
665
|
-
checkoutDomain,
|
|
666
|
-
// Already done inside useCustomerPrivacy
|
|
667
|
-
fetchTrackingValues: false,
|
|
668
|
-
// Avoid creating local cookies too early
|
|
669
|
-
ignoreDeprecatedCookies: !privacyReady
|
|
670
|
-
});
|
|
671
514
|
react.useEffect(() => {
|
|
672
515
|
if (init.current) return;
|
|
673
516
|
init.current = true;
|
|
@@ -676,14 +519,28 @@ function ShopifyAnalytics({
|
|
|
676
519
|
subscribe2(AnalyticsEvent.COLLECTION_VIEWED, collectionViewHandler);
|
|
677
520
|
subscribe2(AnalyticsEvent.SEARCH_VIEWED, searchViewHandler);
|
|
678
521
|
subscribe2(AnalyticsEvent.PRODUCT_ADD_TO_CART, productAddedToCartHandler);
|
|
679
|
-
setShopifyReady(true);
|
|
680
522
|
}, [subscribe2]);
|
|
523
|
+
const updatePrivacyReadiness = () => {
|
|
524
|
+
if (privacyReady) {
|
|
525
|
+
onConsentChange();
|
|
526
|
+
} else if (!consent.withPrivacyBanner || !shouldWaitForPrivacyBanner(getCustomerPrivacyRequired())) {
|
|
527
|
+
setPrivacyReady(true);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
useCustomerPrivacy({
|
|
531
|
+
...consent,
|
|
532
|
+
locale: language,
|
|
533
|
+
checkoutDomain: !checkoutDomain ? "mock.shop" : checkoutDomain,
|
|
534
|
+
storefrontAccessToken: !storefrontAccessToken ? "abcdefghijklmnopqrstuvwxyz123456" : storefrontAccessToken,
|
|
535
|
+
onReady: updatePrivacyReadiness,
|
|
536
|
+
onVisitorConsentCollected: updatePrivacyReadiness
|
|
537
|
+
});
|
|
681
538
|
react.useEffect(() => {
|
|
682
|
-
if (
|
|
539
|
+
if (privacyReady) {
|
|
683
540
|
shopifyAnalyticsReady();
|
|
684
|
-
|
|
541
|
+
onConsentChange();
|
|
685
542
|
}
|
|
686
|
-
}, [
|
|
543
|
+
}, [privacyReady, onConsentChange]);
|
|
687
544
|
return null;
|
|
688
545
|
}
|
|
689
546
|
function logMissingConfig2(fieldName) {
|
|
@@ -693,7 +550,8 @@ function logMissingConfig2(fieldName) {
|
|
|
693
550
|
}
|
|
694
551
|
function prepareBasePageViewPayload(payload) {
|
|
695
552
|
const customerPrivacy = getCustomerPrivacyRequired();
|
|
696
|
-
const hasUserConsent =
|
|
553
|
+
const hasUserConsent = hasAnalyticsConsent();
|
|
554
|
+
if (!hasUserConsent) return;
|
|
697
555
|
if (!payload?.shop?.shopId) {
|
|
698
556
|
logMissingConfig2("shopId");
|
|
699
557
|
return;
|
|
@@ -1134,13 +992,6 @@ function register(key) {
|
|
|
1134
992
|
}
|
|
1135
993
|
};
|
|
1136
994
|
}
|
|
1137
|
-
function shopifyCanTrack() {
|
|
1138
|
-
try {
|
|
1139
|
-
return window.Shopify.customerPrivacy?.analyticsProcessingAllowed?.() ?? false;
|
|
1140
|
-
} catch (e) {
|
|
1141
|
-
}
|
|
1142
|
-
return false;
|
|
1143
|
-
}
|
|
1144
995
|
function messageOnError(field, envVar) {
|
|
1145
996
|
return `[h2:error:Analytics.Provider] - ${field} is required. Make sure ${envVar} is defined in your environment variables. See https://h2o.fyi/analytics/consent to learn how to setup environment variables in the Shopify admin.`;
|
|
1146
997
|
}
|
|
@@ -1150,18 +1001,17 @@ function AnalyticsProvider({
|
|
|
1150
1001
|
children,
|
|
1151
1002
|
consent,
|
|
1152
1003
|
customData = {},
|
|
1153
|
-
shop: shopProp = null
|
|
1154
|
-
cookieDomain
|
|
1004
|
+
shop: shopProp = null
|
|
1155
1005
|
}) {
|
|
1156
1006
|
const { shop } = useShopAnalytics(shopProp);
|
|
1157
|
-
const [
|
|
1158
|
-
|
|
1159
|
-
);
|
|
1160
|
-
const [consentCollected, setConsentCollected] = react.useState(false);
|
|
1007
|
+
const [consentVersion, setConsentVersion] = react.useState(0);
|
|
1008
|
+
const [privacyReady, setPrivacyReady] = react.useState(false);
|
|
1161
1009
|
const [carts, setCarts] = react.useState({ cart: null, prevCart: null });
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1010
|
+
const canTrack = customCanTrack ?? hasAnalyticsConsent;
|
|
1011
|
+
const onConsentChange = react.useCallback(() => {
|
|
1012
|
+
setPrivacyReady(true);
|
|
1013
|
+
setConsentVersion((version2) => version2 + 1);
|
|
1014
|
+
}, []);
|
|
1165
1015
|
if (!!shop) {
|
|
1166
1016
|
if (/\/68817551382$/.test(shop.shopId)) {
|
|
1167
1017
|
warnOnce(
|
|
@@ -1207,7 +1057,7 @@ function AnalyticsProvider({
|
|
|
1207
1057
|
privacyBanner: getPrivacyBanner()
|
|
1208
1058
|
};
|
|
1209
1059
|
}, [
|
|
1210
|
-
|
|
1060
|
+
consentVersion,
|
|
1211
1061
|
canTrack,
|
|
1212
1062
|
carts,
|
|
1213
1063
|
carts.cart?.updatedAt,
|
|
@@ -1225,21 +1075,8 @@ function AnalyticsProvider({
|
|
|
1225
1075
|
children,
|
|
1226
1076
|
!!shop && /* @__PURE__ */ jsxRuntime.jsx(AnalyticsPageView, {}),
|
|
1227
1077
|
!!shop && !!currentCart && /* @__PURE__ */ jsxRuntime.jsx(CartAnalytics, { cart: currentCart, setCarts }),
|
|
1228
|
-
!!shop && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1229
|
-
|
|
1230
|
-
{
|
|
1231
|
-
consent,
|
|
1232
|
-
onReady: () => {
|
|
1233
|
-
setAnalyticsLoaded(true);
|
|
1234
|
-
setCanTrack(
|
|
1235
|
-
customCanTrack ? () => customCanTrack : () => shopifyCanTrack
|
|
1236
|
-
);
|
|
1237
|
-
setConsentCollected(true);
|
|
1238
|
-
},
|
|
1239
|
-
domain: cookieDomain
|
|
1240
|
-
}
|
|
1241
|
-
),
|
|
1242
|
-
!!shop && consentCollected && /* @__PURE__ */ jsxRuntime.jsx(PerfKit, { shop })
|
|
1078
|
+
!!shop && /* @__PURE__ */ jsxRuntime.jsx(ShopifyAnalytics, { consent, onConsentChange }),
|
|
1079
|
+
!!shop && privacyReady && /* @__PURE__ */ jsxRuntime.jsx(PerfKit, { shop })
|
|
1243
1080
|
] });
|
|
1244
1081
|
}
|
|
1245
1082
|
function useAnalytics() {
|
|
@@ -1304,6 +1141,17 @@ var Analytics = {
|
|
|
1304
1141
|
SearchView: AnalyticsSearchView
|
|
1305
1142
|
};
|
|
1306
1143
|
|
|
1144
|
+
// src/constants.ts
|
|
1145
|
+
var STOREFRONT_REQUEST_GROUP_ID_HEADER = "Custom-Storefront-Request-Group-ID";
|
|
1146
|
+
var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
|
|
1147
|
+
var SDK_VARIANT_HEADER = "X-SDK-Variant";
|
|
1148
|
+
var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
|
|
1149
|
+
var SDK_VERSION_HEADER = "X-SDK-Version";
|
|
1150
|
+
var SHOPIFY_CLIENT_IP_HEADER = "X-Shopify-Client-IP";
|
|
1151
|
+
var SHOPIFY_CLIENT_IP_SIG_HEADER = "X-Shopify-Client-IP-Sig";
|
|
1152
|
+
var HYDROGEN_SFAPI_PROXY_KEY = "_sfapi_proxy";
|
|
1153
|
+
var STOREFRONT_CONSENT_MANAGEMENT_HEADER = "Shopify-Storefront-Consent-Management";
|
|
1154
|
+
|
|
1307
1155
|
// src/utils/request.ts
|
|
1308
1156
|
function getHeader(request, key) {
|
|
1309
1157
|
return getHeaderValue(request.headers, key);
|
|
@@ -2009,7 +1857,7 @@ function generateUUID() {
|
|
|
2009
1857
|
}
|
|
2010
1858
|
|
|
2011
1859
|
// src/version.ts
|
|
2012
|
-
var LIB_VERSION = "2026.4.
|
|
1860
|
+
var LIB_VERSION = "2026.4.6";
|
|
2013
1861
|
|
|
2014
1862
|
// src/utils/graphql.ts
|
|
2015
1863
|
function minifyQuery(string) {
|
|
@@ -2270,8 +2118,7 @@ function createStorefrontClient(options) {
|
|
|
2270
2118
|
streamConfig,
|
|
2271
2119
|
onRawHeaders: (headers2) => {
|
|
2272
2120
|
collectedSubrequestHeaders ??= {
|
|
2273
|
-
setCookie: headers2.getSetCookie()
|
|
2274
|
-
serverTiming: headers2.get("server-timing") ?? ""
|
|
2121
|
+
setCookie: headers2.getSetCookie()
|
|
2275
2122
|
};
|
|
2276
2123
|
}
|
|
2277
2124
|
});
|
|
@@ -2404,6 +2251,10 @@ function createStorefrontClient(options) {
|
|
|
2404
2251
|
"origin",
|
|
2405
2252
|
"referer",
|
|
2406
2253
|
"user-agent",
|
|
2254
|
+
// The marker identifies headless consent-management traffic
|
|
2255
|
+
// to the backend, so the proxy forwards it upstream with the
|
|
2256
|
+
// rest of the allowlisted set.
|
|
2257
|
+
STOREFRONT_CONSENT_MANAGEMENT_HEADER,
|
|
2407
2258
|
STOREFRONT_ACCESS_TOKEN_HEADER,
|
|
2408
2259
|
hydrogenReact.SHOPIFY_UNIQUE_TOKEN_HEADER,
|
|
2409
2260
|
hydrogenReact.SHOPIFY_VISIT_TOKEN_HEADER
|
|
@@ -2432,7 +2283,9 @@ function createStorefrontClient(options) {
|
|
|
2432
2283
|
headers: forwardedHeaders
|
|
2433
2284
|
}
|
|
2434
2285
|
);
|
|
2435
|
-
|
|
2286
|
+
const proxyResponse = new Response(sfapiResponse.body, sfapiResponse);
|
|
2287
|
+
proxyResponse.headers.delete("server-timing");
|
|
2288
|
+
return proxyResponse;
|
|
2436
2289
|
},
|
|
2437
2290
|
/**
|
|
2438
2291
|
* Checks if the request is targeting the Storefront MCP endpoint.
|
|
@@ -2481,7 +2334,9 @@ function createStorefrontClient(options) {
|
|
|
2481
2334
|
body: request.body,
|
|
2482
2335
|
headers: forwardedHeaders
|
|
2483
2336
|
});
|
|
2484
|
-
|
|
2337
|
+
const proxyResponse = new Response(mcpResponse.body, mcpResponse);
|
|
2338
|
+
proxyResponse.headers.delete("server-timing");
|
|
2339
|
+
return proxyResponse;
|
|
2485
2340
|
} catch (error) {
|
|
2486
2341
|
const JSON_RPC_INTERNAL_ERROR = -32603;
|
|
2487
2342
|
const message = error instanceof Error ? error.message : "Internal proxy error";
|
|
@@ -2504,21 +2359,6 @@ function createStorefrontClient(options) {
|
|
|
2504
2359
|
response.headers.append("Set-Cookie", value);
|
|
2505
2360
|
}
|
|
2506
2361
|
}
|
|
2507
|
-
const serverTiming = extractServerTimingHeader(
|
|
2508
|
-
collectedSubrequestHeaders?.serverTiming
|
|
2509
|
-
);
|
|
2510
|
-
const isDocumentResponse = response.headers.get("content-type")?.startsWith("text/html");
|
|
2511
|
-
const fallbackValues = isDocumentResponse ? { _y: uniqueToken, _s: visitToken } : void 0;
|
|
2512
|
-
appendServerTimingHeader(response, {
|
|
2513
|
-
...fallbackValues,
|
|
2514
|
-
...serverTiming
|
|
2515
|
-
});
|
|
2516
|
-
if (isDocumentResponse && collectedSubrequestHeaders && // _shopify_essential cookie is always set, but we need more than that
|
|
2517
|
-
collectedSubrequestHeaders.setCookie.length > 1 && serverTiming?._y && serverTiming?._s && serverTiming?._cmp) {
|
|
2518
|
-
appendServerTimingHeader(response, {
|
|
2519
|
-
[HYDROGEN_SERVER_TRACKING_KEY]: "1"
|
|
2520
|
-
});
|
|
2521
|
-
}
|
|
2522
2362
|
}
|
|
2523
2363
|
}
|
|
2524
2364
|
};
|
|
@@ -4680,6 +4520,19 @@ function createHydrogenContext(options, additionalContext) {
|
|
|
4680
4520
|
});
|
|
4681
4521
|
return hybridProvider;
|
|
4682
4522
|
}
|
|
4523
|
+
|
|
4524
|
+
// src/utils/server-timing.ts
|
|
4525
|
+
function buildServerTimingHeader(values) {
|
|
4526
|
+
return Object.entries(values).map(([key, value]) => value ? `${key};desc=${value}` : void 0).filter(Boolean).join(", ");
|
|
4527
|
+
}
|
|
4528
|
+
function appendServerTimingHeader(response, values) {
|
|
4529
|
+
const header = typeof values === "string" ? values : buildServerTimingHeader(values);
|
|
4530
|
+
if (header) {
|
|
4531
|
+
response.headers.append("Server-Timing", header);
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
|
|
4535
|
+
// src/createRequestHandler.ts
|
|
4683
4536
|
function createRequestHandler({
|
|
4684
4537
|
build,
|
|
4685
4538
|
mode,
|
|
@@ -6194,7 +6047,7 @@ function renderTitle(template, title) {
|
|
|
6194
6047
|
return template.replace("%s", title ?? "");
|
|
6195
6048
|
}
|
|
6196
6049
|
function inferMimeType(url) {
|
|
6197
|
-
const ext = url && url.split(".").pop();
|
|
6050
|
+
const ext = url && url.split(/[?#]/)[0].split(".").pop()?.toLowerCase();
|
|
6198
6051
|
switch (ext) {
|
|
6199
6052
|
case "svg":
|
|
6200
6053
|
return "image/svg+xml";
|