@shopify/hydrogen 2026.4.4 → 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 +265 -382
- package/dist/development/index.cjs.map +1 -1
- package/dist/development/index.js +268 -385
- package/dist/development/index.js.map +1 -1
- package/dist/oxygen/index.d.ts +39 -7
- package/dist/production/index.cjs +50 -50
- 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 +50 -50
- 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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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);
|
|
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;
|
|
404
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
|
};
|
|
@@ -3780,11 +3620,13 @@ async function refreshToken({
|
|
|
3780
3620
|
const customerAccount = session.get(CUSTOMER_ACCOUNT_SESSION_KEY);
|
|
3781
3621
|
const refreshToken2 = customerAccount?.refreshToken;
|
|
3782
3622
|
const idToken = customerAccount?.idToken;
|
|
3783
|
-
if (!refreshToken2)
|
|
3623
|
+
if (!refreshToken2) {
|
|
3624
|
+
clearSession(session);
|
|
3784
3625
|
throw new BadRequest(
|
|
3785
3626
|
"Unauthorized",
|
|
3786
3627
|
"No refreshToken found in the session. Make sure your session is configured correctly and passed to `createCustomerAccountClient`."
|
|
3787
3628
|
);
|
|
3629
|
+
}
|
|
3788
3630
|
newBody.append("grant_type", "refresh_token");
|
|
3789
3631
|
newBody.append("refresh_token", refreshToken2);
|
|
3790
3632
|
newBody.append("client_id", customerAccountId);
|
|
@@ -3809,6 +3651,9 @@ async function refreshToken({
|
|
|
3809
3651
|
});
|
|
3810
3652
|
if (!response.ok) {
|
|
3811
3653
|
const text = await response.text();
|
|
3654
|
+
if (getOAuthError(text) === "invalid_grant") {
|
|
3655
|
+
clearSession(session);
|
|
3656
|
+
}
|
|
3812
3657
|
throw new Response(text, {
|
|
3813
3658
|
status: response.status,
|
|
3814
3659
|
headers: {
|
|
@@ -3819,9 +3664,13 @@ async function refreshToken({
|
|
|
3819
3664
|
const {
|
|
3820
3665
|
access_token,
|
|
3821
3666
|
expires_in,
|
|
3822
|
-
refresh_token
|
|
3667
|
+
refresh_token,
|
|
3668
|
+
error
|
|
3823
3669
|
} = await response.json();
|
|
3824
3670
|
if (!access_token || access_token.length === 0) {
|
|
3671
|
+
if (error === "invalid_grant") {
|
|
3672
|
+
clearSession(session);
|
|
3673
|
+
}
|
|
3825
3674
|
throw new BadRequest("Unauthorized", "Invalid access token received.");
|
|
3826
3675
|
}
|
|
3827
3676
|
session.set(CUSTOMER_ACCOUNT_SESSION_KEY, {
|
|
@@ -3858,7 +3707,6 @@ async function checkExpires({
|
|
|
3858
3707
|
await locks.refresh;
|
|
3859
3708
|
delete locks.refresh;
|
|
3860
3709
|
} catch (error) {
|
|
3861
|
-
clearSession(session);
|
|
3862
3710
|
if (error && error.status !== 401) {
|
|
3863
3711
|
throw error;
|
|
3864
3712
|
} else {
|
|
@@ -3870,6 +3718,14 @@ async function checkExpires({
|
|
|
3870
3718
|
}
|
|
3871
3719
|
}
|
|
3872
3720
|
}
|
|
3721
|
+
function getOAuthError(body) {
|
|
3722
|
+
try {
|
|
3723
|
+
const data = JSON.parse(body);
|
|
3724
|
+
return typeof data?.error === "string" ? data.error : void 0;
|
|
3725
|
+
} catch {
|
|
3726
|
+
return void 0;
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3873
3729
|
function generateCodeVerifier() {
|
|
3874
3730
|
const rando = generateRandomCode();
|
|
3875
3731
|
return base64UrlEncode(rando);
|
|
@@ -3897,9 +3753,7 @@ function convertBufferToString(hash) {
|
|
|
3897
3753
|
return String.fromCharCode(...numberArray);
|
|
3898
3754
|
}
|
|
3899
3755
|
function generateState() {
|
|
3900
|
-
|
|
3901
|
-
const randomString = Math.random().toString(36).substring(2);
|
|
3902
|
-
return timestamp + randomString;
|
|
3756
|
+
return base64UrlEncode(generateRandomCode());
|
|
3903
3757
|
}
|
|
3904
3758
|
async function exchangeAccessToken(authAccessToken, customerAccountId, customerAccountTokenExchangeUrl, httpsOrigin, debugInfo) {
|
|
3905
3759
|
const clientId = customerAccountId;
|
|
@@ -4053,6 +3907,8 @@ function createCustomerAccountHelper(customerApiVersion, shopId) {
|
|
|
4053
3907
|
|
|
4054
3908
|
// src/customer/customer.ts
|
|
4055
3909
|
var HYDROGEN_TUNNEL_DOMAIN_SUFFIX = ".tryhydrogen.dev";
|
|
3910
|
+
var OAUTH_RECOVERY_PARAM = "oauth_recovery";
|
|
3911
|
+
var OAUTH_RECOVERY_STATE_PREFIX = "oauth-recovery.";
|
|
4056
3912
|
function checkTunnelDomain(hostname, useCustomAuthDomain, redirectUri) {
|
|
4057
3913
|
if (hostname.endsWith(HYDROGEN_TUNNEL_DOMAIN_SUFFIX)) return;
|
|
4058
3914
|
if (useCustomAuthDomain) {
|
|
@@ -4284,7 +4140,8 @@ function createCustomerAccountClient({
|
|
|
4284
4140
|
ensureTunnel(requestUrl.hostname);
|
|
4285
4141
|
ifInvalidCredentialThrowError();
|
|
4286
4142
|
const loginUrl = new URL(getCustomerAccountUrl("AUTH" /* AUTH */));
|
|
4287
|
-
const
|
|
4143
|
+
const isOAuthRecovery = requestUrl.searchParams.get(OAUTH_RECOVERY_PARAM) === "true";
|
|
4144
|
+
const state = `${isOAuthRecovery ? OAUTH_RECOVERY_STATE_PREFIX : ""}${generateState()}`;
|
|
4288
4145
|
const nonce = generateNonce();
|
|
4289
4146
|
loginUrl.searchParams.set("client_id", customerAccountId);
|
|
4290
4147
|
loginUrl.searchParams.set("scope", "openid email");
|
|
@@ -4373,17 +4230,30 @@ function createCustomerAccountClient({
|
|
|
4373
4230
|
const code = requestUrl.searchParams.get("code");
|
|
4374
4231
|
const state = requestUrl.searchParams.get("state");
|
|
4375
4232
|
if (!code || !state) {
|
|
4376
|
-
clearSession(session);
|
|
4377
4233
|
throw new BadRequest(
|
|
4378
4234
|
"Unauthorized",
|
|
4379
4235
|
"No code or state parameter found in the redirect URL."
|
|
4380
4236
|
);
|
|
4381
4237
|
}
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4238
|
+
const customerAccountSession = session.get(CUSTOMER_ACCOUNT_SESSION_KEY);
|
|
4239
|
+
if (customerAccountSession?.state !== state) {
|
|
4240
|
+
if (state.startsWith(OAUTH_RECOVERY_STATE_PREFIX)) {
|
|
4241
|
+
throw new BadRequest(
|
|
4242
|
+
"Unable to complete sign in. Please try again.",
|
|
4243
|
+
"The session state does not match the state parameter. Make sure that the session is configured correctly and passed to `createCustomerAccountClient`."
|
|
4244
|
+
);
|
|
4245
|
+
}
|
|
4246
|
+
warnOnce(
|
|
4247
|
+
"[h2:warn:customerAccount] OAuth callback state mismatch. Restarting customer login once."
|
|
4248
|
+
);
|
|
4249
|
+
const recoveryLoginUrl = new URL(loginPath, request.url);
|
|
4250
|
+
recoveryLoginUrl.searchParams.set(
|
|
4251
|
+
"return_to",
|
|
4252
|
+
customerAccountSession?.redirectPath ?? defaultRedirectPath
|
|
4253
|
+
);
|
|
4254
|
+
recoveryLoginUrl.searchParams.set(OAUTH_RECOVERY_PARAM, "true");
|
|
4255
|
+
return redirect(
|
|
4256
|
+
`${recoveryLoginUrl.pathname}${recoveryLoginUrl.search}`
|
|
4387
4257
|
);
|
|
4388
4258
|
}
|
|
4389
4259
|
const clientId = customerAccountId;
|
|
@@ -4650,6 +4520,19 @@ function createHydrogenContext(options, additionalContext) {
|
|
|
4650
4520
|
});
|
|
4651
4521
|
return hybridProvider;
|
|
4652
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
|
|
4653
4536
|
function createRequestHandler({
|
|
4654
4537
|
build,
|
|
4655
4538
|
mode,
|
|
@@ -6164,7 +6047,7 @@ function renderTitle(template, title) {
|
|
|
6164
6047
|
return template.replace("%s", title ?? "");
|
|
6165
6048
|
}
|
|
6166
6049
|
function inferMimeType(url) {
|
|
6167
|
-
const ext = url && url.split(".").pop();
|
|
6050
|
+
const ext = url && url.split(/[?#]/)[0].split(".").pop()?.toLowerCase();
|
|
6168
6051
|
switch (ext) {
|
|
6169
6052
|
case "svg":
|
|
6170
6053
|
return "image/svg+xml";
|