@shopify/hydrogen 2026.4.5 → 2026.4.7
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 +242 -372
- package/dist/development/index.cjs.map +1 -1
- package/dist/development/index.js +245 -375
- package/dist/development/index.js.map +1 -1
- package/dist/oxygen/index.d.ts +39 -7
- package/dist/production/index.cjs +42 -42
- 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 +42 -42
- 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;
|
|
383
|
+
};
|
|
384
|
+
const consentCollectedHandler = (event) => {
|
|
385
|
+
if (!checkReady()) return;
|
|
386
|
+
updateTrackingValues();
|
|
387
|
+
callbacks.current.onVisitorConsentCollected?.(event.detail);
|
|
405
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.7";
|
|
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,8 @@ function register(key) {
|
|
|
1134
992
|
}
|
|
1135
993
|
};
|
|
1136
994
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
return window.Shopify.customerPrivacy?.analyticsProcessingAllowed?.() ?? false;
|
|
1140
|
-
} catch (e) {
|
|
1141
|
-
}
|
|
1142
|
-
return false;
|
|
1143
|
-
}
|
|
995
|
+
var noopPublish = () => {
|
|
996
|
+
};
|
|
1144
997
|
function messageOnError(field, envVar) {
|
|
1145
998
|
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
999
|
}
|
|
@@ -1150,18 +1003,31 @@ function AnalyticsProvider({
|
|
|
1150
1003
|
children,
|
|
1151
1004
|
consent,
|
|
1152
1005
|
customData = {},
|
|
1153
|
-
shop: shopProp = null
|
|
1154
|
-
cookieDomain
|
|
1006
|
+
shop: shopProp = null
|
|
1155
1007
|
}) {
|
|
1156
1008
|
const { shop } = useShopAnalytics(shopProp);
|
|
1157
|
-
const [
|
|
1158
|
-
|
|
1009
|
+
const [consentVersion, setConsentVersion] = react.useState(0);
|
|
1010
|
+
const [privacyReady, setPrivacyReady] = react.useState(false);
|
|
1011
|
+
const [carts, setCartsState] = react.useState({ cart: null, prevCart: null });
|
|
1012
|
+
const setCarts = react.useCallback(
|
|
1013
|
+
(update) => react.startTransition(() => setCartsState(update)),
|
|
1014
|
+
[]
|
|
1159
1015
|
);
|
|
1160
|
-
const
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
|
|
1016
|
+
const canTrack = customCanTrack ?? hasAnalyticsConsent;
|
|
1017
|
+
const canTrackRef = react.useRef(canTrack);
|
|
1018
|
+
canTrackRef.current = canTrack;
|
|
1019
|
+
const guardedPublish = react.useCallback(
|
|
1020
|
+
(event, payload) => {
|
|
1021
|
+
if (canTrackRef.current()) publish(event, payload);
|
|
1022
|
+
},
|
|
1023
|
+
[]
|
|
1164
1024
|
);
|
|
1025
|
+
const onConsentChange = react.useCallback(() => {
|
|
1026
|
+
react.startTransition(() => {
|
|
1027
|
+
setPrivacyReady(true);
|
|
1028
|
+
setConsentVersion((version2) => version2 + 1);
|
|
1029
|
+
});
|
|
1030
|
+
}, []);
|
|
1165
1031
|
if (!!shop) {
|
|
1166
1032
|
if (/\/68817551382$/.test(shop.shopId)) {
|
|
1167
1033
|
warnOnce(
|
|
@@ -1198,8 +1064,7 @@ function AnalyticsProvider({
|
|
|
1198
1064
|
canTrack,
|
|
1199
1065
|
...carts,
|
|
1200
1066
|
customData,
|
|
1201
|
-
publish: canTrack() ?
|
|
1202
|
-
},
|
|
1067
|
+
publish: canTrack() ? guardedPublish : noopPublish,
|
|
1203
1068
|
shop,
|
|
1204
1069
|
subscribe,
|
|
1205
1070
|
register,
|
|
@@ -1207,12 +1072,12 @@ function AnalyticsProvider({
|
|
|
1207
1072
|
privacyBanner: getPrivacyBanner()
|
|
1208
1073
|
};
|
|
1209
1074
|
}, [
|
|
1210
|
-
|
|
1075
|
+
consentVersion,
|
|
1211
1076
|
canTrack,
|
|
1212
1077
|
carts,
|
|
1213
1078
|
carts.cart?.updatedAt,
|
|
1214
1079
|
carts.prevCart,
|
|
1215
|
-
|
|
1080
|
+
guardedPublish,
|
|
1216
1081
|
subscribe,
|
|
1217
1082
|
customData,
|
|
1218
1083
|
shop,
|
|
@@ -1225,21 +1090,8 @@ function AnalyticsProvider({
|
|
|
1225
1090
|
children,
|
|
1226
1091
|
!!shop && /* @__PURE__ */ jsxRuntime.jsx(AnalyticsPageView, {}),
|
|
1227
1092
|
!!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 })
|
|
1093
|
+
!!shop && /* @__PURE__ */ jsxRuntime.jsx(ShopifyAnalytics, { consent, onConsentChange }),
|
|
1094
|
+
!!shop && privacyReady && /* @__PURE__ */ jsxRuntime.jsx(PerfKit, { shop })
|
|
1243
1095
|
] });
|
|
1244
1096
|
}
|
|
1245
1097
|
function useAnalytics() {
|
|
@@ -1254,7 +1106,9 @@ function useAnalytics() {
|
|
|
1254
1106
|
function useShopAnalytics(shopProp) {
|
|
1255
1107
|
const [shop, setShop] = react.useState(null);
|
|
1256
1108
|
react.useEffect(() => {
|
|
1257
|
-
Promise.resolve(shopProp).then(
|
|
1109
|
+
Promise.resolve(shopProp).then((resolvedShop) => {
|
|
1110
|
+
react.startTransition(() => setShop(resolvedShop));
|
|
1111
|
+
});
|
|
1258
1112
|
return () => {
|
|
1259
1113
|
};
|
|
1260
1114
|
}, [setShop, shopProp]);
|
|
@@ -1304,6 +1158,17 @@ var Analytics = {
|
|
|
1304
1158
|
SearchView: AnalyticsSearchView
|
|
1305
1159
|
};
|
|
1306
1160
|
|
|
1161
|
+
// src/constants.ts
|
|
1162
|
+
var STOREFRONT_REQUEST_GROUP_ID_HEADER = "Custom-Storefront-Request-Group-ID";
|
|
1163
|
+
var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
|
|
1164
|
+
var SDK_VARIANT_HEADER = "X-SDK-Variant";
|
|
1165
|
+
var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
|
|
1166
|
+
var SDK_VERSION_HEADER = "X-SDK-Version";
|
|
1167
|
+
var SHOPIFY_CLIENT_IP_HEADER = "X-Shopify-Client-IP";
|
|
1168
|
+
var SHOPIFY_CLIENT_IP_SIG_HEADER = "X-Shopify-Client-IP-Sig";
|
|
1169
|
+
var HYDROGEN_SFAPI_PROXY_KEY = "_sfapi_proxy";
|
|
1170
|
+
var STOREFRONT_CONSENT_MANAGEMENT_HEADER = "Shopify-Storefront-Consent-Management";
|
|
1171
|
+
|
|
1307
1172
|
// src/utils/request.ts
|
|
1308
1173
|
function getHeader(request, key) {
|
|
1309
1174
|
return getHeaderValue(request.headers, key);
|
|
@@ -2009,7 +1874,7 @@ function generateUUID() {
|
|
|
2009
1874
|
}
|
|
2010
1875
|
|
|
2011
1876
|
// src/version.ts
|
|
2012
|
-
var LIB_VERSION = "2026.4.
|
|
1877
|
+
var LIB_VERSION = "2026.4.7";
|
|
2013
1878
|
|
|
2014
1879
|
// src/utils/graphql.ts
|
|
2015
1880
|
function minifyQuery(string) {
|
|
@@ -2270,8 +2135,7 @@ function createStorefrontClient(options) {
|
|
|
2270
2135
|
streamConfig,
|
|
2271
2136
|
onRawHeaders: (headers2) => {
|
|
2272
2137
|
collectedSubrequestHeaders ??= {
|
|
2273
|
-
setCookie: headers2.getSetCookie()
|
|
2274
|
-
serverTiming: headers2.get("server-timing") ?? ""
|
|
2138
|
+
setCookie: headers2.getSetCookie()
|
|
2275
2139
|
};
|
|
2276
2140
|
}
|
|
2277
2141
|
});
|
|
@@ -2404,6 +2268,10 @@ function createStorefrontClient(options) {
|
|
|
2404
2268
|
"origin",
|
|
2405
2269
|
"referer",
|
|
2406
2270
|
"user-agent",
|
|
2271
|
+
// The marker identifies headless consent-management traffic
|
|
2272
|
+
// to the backend, so the proxy forwards it upstream with the
|
|
2273
|
+
// rest of the allowlisted set.
|
|
2274
|
+
STOREFRONT_CONSENT_MANAGEMENT_HEADER,
|
|
2407
2275
|
STOREFRONT_ACCESS_TOKEN_HEADER,
|
|
2408
2276
|
hydrogenReact.SHOPIFY_UNIQUE_TOKEN_HEADER,
|
|
2409
2277
|
hydrogenReact.SHOPIFY_VISIT_TOKEN_HEADER
|
|
@@ -2432,7 +2300,9 @@ function createStorefrontClient(options) {
|
|
|
2432
2300
|
headers: forwardedHeaders
|
|
2433
2301
|
}
|
|
2434
2302
|
);
|
|
2435
|
-
|
|
2303
|
+
const proxyResponse = new Response(sfapiResponse.body, sfapiResponse);
|
|
2304
|
+
proxyResponse.headers.delete("server-timing");
|
|
2305
|
+
return proxyResponse;
|
|
2436
2306
|
},
|
|
2437
2307
|
/**
|
|
2438
2308
|
* Checks if the request is targeting the Storefront MCP endpoint.
|
|
@@ -2481,7 +2351,9 @@ function createStorefrontClient(options) {
|
|
|
2481
2351
|
body: request.body,
|
|
2482
2352
|
headers: forwardedHeaders
|
|
2483
2353
|
});
|
|
2484
|
-
|
|
2354
|
+
const proxyResponse = new Response(mcpResponse.body, mcpResponse);
|
|
2355
|
+
proxyResponse.headers.delete("server-timing");
|
|
2356
|
+
return proxyResponse;
|
|
2485
2357
|
} catch (error) {
|
|
2486
2358
|
const JSON_RPC_INTERNAL_ERROR = -32603;
|
|
2487
2359
|
const message = error instanceof Error ? error.message : "Internal proxy error";
|
|
@@ -2504,21 +2376,6 @@ function createStorefrontClient(options) {
|
|
|
2504
2376
|
response.headers.append("Set-Cookie", value);
|
|
2505
2377
|
}
|
|
2506
2378
|
}
|
|
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
2379
|
}
|
|
2523
2380
|
}
|
|
2524
2381
|
};
|
|
@@ -4680,6 +4537,19 @@ function createHydrogenContext(options, additionalContext) {
|
|
|
4680
4537
|
});
|
|
4681
4538
|
return hybridProvider;
|
|
4682
4539
|
}
|
|
4540
|
+
|
|
4541
|
+
// src/utils/server-timing.ts
|
|
4542
|
+
function buildServerTimingHeader(values) {
|
|
4543
|
+
return Object.entries(values).map(([key, value]) => value ? `${key};desc=${value}` : void 0).filter(Boolean).join(", ");
|
|
4544
|
+
}
|
|
4545
|
+
function appendServerTimingHeader(response, values) {
|
|
4546
|
+
const header = typeof values === "string" ? values : buildServerTimingHeader(values);
|
|
4547
|
+
if (header) {
|
|
4548
|
+
response.headers.append("Server-Timing", header);
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
// src/createRequestHandler.ts
|
|
4683
4553
|
function createRequestHandler({
|
|
4684
4554
|
build,
|
|
4685
4555
|
mode,
|
|
@@ -6194,7 +6064,7 @@ function renderTitle(template, title) {
|
|
|
6194
6064
|
return template.replace("%s", title ?? "");
|
|
6195
6065
|
}
|
|
6196
6066
|
function inferMimeType(url) {
|
|
6197
|
-
const ext = url && url.split(".").pop();
|
|
6067
|
+
const ext = url && url.split(/[?#]/)[0].split(".").pop()?.toLowerCase();
|
|
6198
6068
|
switch (ext) {
|
|
6199
6069
|
case "svg":
|
|
6200
6070
|
return "image/svg+xml";
|