@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
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { createContext, forwardRef, lazy, useContext, useMemo, useEffect,
|
|
1
|
+
import { createContext, forwardRef, lazy, useContext, useRef, useState, useMemo, useEffect, useCallback, createElement, Fragment as Fragment$1, Suspense } from 'react';
|
|
2
2
|
import { createContext as createContext$1, useRevalidator, useFetcher, useFetchers, RouterContextProvider, createRequestHandler as createRequestHandler$1, useNavigation, useLocation, useNavigate, Link, useMatches } from 'react-router';
|
|
3
3
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
4
|
-
import { useLoadScript,
|
|
4
|
+
import { useLoadScript, createStorefrontClient as createStorefrontClient$1, SHOPIFY_STOREFRONT_ID_HEADER, SHOPIFY_STOREFRONT_Y_HEADER, SHOPIFY_STOREFRONT_S_HEADER, SHOPIFY_UNIQUE_TOKEN_HEADER, SHOPIFY_VISIT_TOKEN_HEADER, flattenConnection, RichText as RichText$1, ShopPayButton as ShopPayButton$1, parseGid, getTrackingValues, sendShopifyAnalytics, AnalyticsEventName, AnalyticsPageType, getClientBrowserParameters } from '@shopify/hydrogen-react';
|
|
5
5
|
export { AnalyticsEventName, AnalyticsPageType, ExternalVideo, IMAGE_FRAGMENT, Image, MediaFile, ModelViewer, Money, ShopifySalesChannel, Video, customerAccountApiCustomScalars, decodeEncodedVariant, flattenConnection, getAdjacentAndFirstAvailableVariants, getClientBrowserParameters, getProductOptions, getShopifyCookies, getTrackingValues, isOptionValueCombinationInEncodedVariant, mapSelectedProductOptionToObject, parseGid, parseMetafield, sendShopifyAnalytics, storefrontApiCustomScalars, useLoadScript, useMoney, useSelectedOptionInUrlParam, useShopifyCookies } from '@shopify/hydrogen-react';
|
|
6
|
-
import {
|
|
6
|
+
import { loadScript } from '@shopify/hydrogen-react/load-script';
|
|
7
7
|
import { createGraphQLClient } from '@shopify/graphql-client';
|
|
8
8
|
import { parse, stringify } from 'worktop/cookie';
|
|
9
9
|
import cspBuilder from 'content-security-policy-builder';
|
|
@@ -59,6 +59,25 @@ function AnalyticsCustomView(props) {
|
|
|
59
59
|
return /* @__PURE__ */ jsx(AnalyticsView, { ...props });
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// src/analytics-manager/consent.ts
|
|
63
|
+
function hasAnalyticsConsent() {
|
|
64
|
+
try {
|
|
65
|
+
const privacy = window.Shopify?.customerPrivacy;
|
|
66
|
+
if (privacy?.consentStatus !== "loaded") return false;
|
|
67
|
+
if (privacy.currentVisitorConsent?.().analytics === "no") return false;
|
|
68
|
+
return privacy.analyticsProcessingAllowed?.() ?? false;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function shouldWaitForPrivacyBanner(privacy) {
|
|
74
|
+
if (!privacy.shouldShowBanner()) return false;
|
|
75
|
+
const consent = privacy.currentVisitorConsent();
|
|
76
|
+
return ![consent.marketing, consent.analytics, consent.preferences].some(
|
|
77
|
+
(value) => value === "yes" || value === "no"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
// src/analytics-manager/events.ts
|
|
63
82
|
var AnalyticsEvent = {
|
|
64
83
|
// Views
|
|
@@ -74,61 +93,6 @@ var AnalyticsEvent = {
|
|
|
74
93
|
// Custom
|
|
75
94
|
CUSTOM_EVENT: `custom_`
|
|
76
95
|
};
|
|
77
|
-
|
|
78
|
-
// src/constants.ts
|
|
79
|
-
var STOREFRONT_REQUEST_GROUP_ID_HEADER = "Custom-Storefront-Request-Group-ID";
|
|
80
|
-
var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
|
|
81
|
-
var SDK_VARIANT_HEADER = "X-SDK-Variant";
|
|
82
|
-
var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
|
|
83
|
-
var SDK_VERSION_HEADER = "X-SDK-Version";
|
|
84
|
-
var SHOPIFY_CLIENT_IP_HEADER = "X-Shopify-Client-IP";
|
|
85
|
-
var SHOPIFY_CLIENT_IP_SIG_HEADER = "X-Shopify-Client-IP-Sig";
|
|
86
|
-
var HYDROGEN_SFAPI_PROXY_KEY = "_sfapi_proxy";
|
|
87
|
-
var HYDROGEN_SERVER_TRACKING_KEY = "_server_tracking";
|
|
88
|
-
|
|
89
|
-
// src/utils/server-timing.ts
|
|
90
|
-
function buildServerTimingHeader(values) {
|
|
91
|
-
return Object.entries(values).map(([key, value]) => value ? `${key};desc=${value}` : void 0).filter(Boolean).join(", ");
|
|
92
|
-
}
|
|
93
|
-
function appendServerTimingHeader(response, values) {
|
|
94
|
-
const header = typeof values === "string" ? values : buildServerTimingHeader(values);
|
|
95
|
-
if (header) {
|
|
96
|
-
response.headers.append("Server-Timing", header);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
var trackedTimings = ["_y", "_s", "_cmp"];
|
|
100
|
-
function extractServerTimingHeader(serverTimingHeader) {
|
|
101
|
-
const values = {};
|
|
102
|
-
if (!serverTimingHeader) return values;
|
|
103
|
-
const re = new RegExp(
|
|
104
|
-
`\\b(${trackedTimings.join("|")});desc="?([^",]+)"?`,
|
|
105
|
-
"g"
|
|
106
|
-
);
|
|
107
|
-
let match;
|
|
108
|
-
while ((match = re.exec(serverTimingHeader)) !== null) {
|
|
109
|
-
values[match[1]] = match[2];
|
|
110
|
-
}
|
|
111
|
-
return values;
|
|
112
|
-
}
|
|
113
|
-
function hasServerTimingInNavigationEntry(key) {
|
|
114
|
-
if (typeof window === "undefined") return false;
|
|
115
|
-
try {
|
|
116
|
-
const navigationEntry = window.performance.getEntriesByType(
|
|
117
|
-
"navigation"
|
|
118
|
-
)[0];
|
|
119
|
-
return !!navigationEntry?.serverTiming?.some((entry) => entry.name === key);
|
|
120
|
-
} catch (e) {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
function isSfapiProxyEnabled() {
|
|
125
|
-
return hasServerTimingInNavigationEntry(HYDROGEN_SFAPI_PROXY_KEY);
|
|
126
|
-
}
|
|
127
|
-
function hasServerReturnedTrackingValues() {
|
|
128
|
-
return hasServerTimingInNavigationEntry(HYDROGEN_SERVER_TRACKING_KEY);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// src/customer-privacy/ShopifyCustomerPrivacy.tsx
|
|
132
96
|
var CONSENT_API = "https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.2/consent-tracking-api.js";
|
|
133
97
|
var CONSENT_API_WITH_BANNER = "https://cdn.shopify.com/shopifycloud/privacy-banner/storefront-banner.js";
|
|
134
98
|
function logMissingConfig(fieldName) {
|
|
@@ -136,6 +100,11 @@ function logMissingConfig(fieldName) {
|
|
|
136
100
|
`[h2:error:useCustomerPrivacy] Unable to setup Customer Privacy API: Missing consent.${fieldName} configuration.`
|
|
137
101
|
);
|
|
138
102
|
}
|
|
103
|
+
function logIgnoredCrossDomainConsent() {
|
|
104
|
+
console.warn(
|
|
105
|
+
"[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"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
139
108
|
function useCustomerPrivacy(props) {
|
|
140
109
|
const {
|
|
141
110
|
withPrivacyBanner = false,
|
|
@@ -147,27 +116,15 @@ function useCustomerPrivacy(props) {
|
|
|
147
116
|
locale,
|
|
148
117
|
sameDomainForStorefrontApi
|
|
149
118
|
} = props;
|
|
150
|
-
const hasSfapiProxy = useMemo(
|
|
151
|
-
() => sameDomainForStorefrontApi ?? isSfapiProxyEnabled(),
|
|
152
|
-
[sameDomainForStorefrontApi]
|
|
153
|
-
);
|
|
154
|
-
const fetchTrackingValuesFromBrowser = useMemo(
|
|
155
|
-
() => hasSfapiProxy && !hasServerReturnedTrackingValues(),
|
|
156
|
-
[hasSfapiProxy]
|
|
157
|
-
);
|
|
158
|
-
const cookiesReady = useShopifyCookies({
|
|
159
|
-
fetchTrackingValues: fetchTrackingValuesFromBrowser,
|
|
160
|
-
storefrontAccessToken,
|
|
161
|
-
ignoreDeprecatedCookies: true
|
|
162
|
-
});
|
|
163
|
-
const initialTrackingValues = useMemo(getTrackingValues, [cookiesReady]);
|
|
164
119
|
const { revalidate } = useRevalidator();
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
120
|
+
const callbacks = useRef({ onReady, onVisitorConsentCollected, revalidate });
|
|
121
|
+
callbacks.current = { onReady, onVisitorConsentCollected, revalidate };
|
|
122
|
+
const notifiedReady = useRef(false);
|
|
123
|
+
const previousTrackingValues = useRef();
|
|
124
|
+
const [apis, setApis] = useState(() => ({
|
|
125
|
+
customerPrivacy: getCustomerPrivacy(),
|
|
126
|
+
privacyBanner: getPrivacyBanner()
|
|
127
|
+
}));
|
|
171
128
|
const config = useMemo(() => {
|
|
172
129
|
if (!checkoutDomain) logMissingConfig("checkoutDomain");
|
|
173
130
|
if (!storefrontAccessToken) logMissingConfig("storefrontAccessToken");
|
|
@@ -176,196 +133,155 @@ function useCustomerPrivacy(props) {
|
|
|
176
133
|
`[h2:error:useCustomerPrivacy] It looks like you passed a private access token, make sure to use the public token`
|
|
177
134
|
);
|
|
178
135
|
}
|
|
136
|
+
if (sameDomainForStorefrontApi === false) logIgnoredCrossDomainConsent();
|
|
179
137
|
const commonAncestorDomain = parseStoreDomain(checkoutDomain);
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// This domain is used to send requests to SFAPI for setting and getting consent.
|
|
187
|
-
checkoutRootDomain: sfapiDomain,
|
|
188
|
-
// Prefix with a dot to ensure this domain is different from checkoutRootDomain.
|
|
189
|
-
// This will ensure old cookies are set for a cross-subdomain checkout setup
|
|
190
|
-
// so that we keep backward compatibility until new cookies are rolled out.
|
|
191
|
-
// Once consent-tracking-api is updated to not rely on cookies anymore, we can remove this.
|
|
138
|
+
return {
|
|
139
|
+
// Always the same-origin proxy: cross-origin consent requests carry no
|
|
140
|
+
// cookies, so consent and visitor tokens would reset on every page load
|
|
141
|
+
// (and an opt-out would be forgotten). The server render never sends
|
|
142
|
+
// consent requests, so it keeps the checkout domain.
|
|
143
|
+
checkoutRootDomain: typeof window !== "undefined" ? window.location.host : checkoutDomain,
|
|
192
144
|
storefrontRootDomain: commonAncestorDomain ? "." + commonAncestorDomain : void 0,
|
|
193
145
|
storefrontAccessToken,
|
|
194
146
|
country,
|
|
195
147
|
locale
|
|
196
148
|
};
|
|
197
|
-
return config2;
|
|
198
149
|
}, [
|
|
199
|
-
|
|
150
|
+
sameDomainForStorefrontApi,
|
|
200
151
|
checkoutDomain,
|
|
201
152
|
storefrontAccessToken,
|
|
202
153
|
country,
|
|
203
154
|
locale
|
|
204
155
|
]);
|
|
205
156
|
useEffect(() => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
157
|
+
let active = true;
|
|
158
|
+
const shopify = window.Shopify ??= {};
|
|
159
|
+
const privacy = shopify.customerPrivacy ??= {};
|
|
160
|
+
privacy.config = {
|
|
161
|
+
...privacy.config,
|
|
162
|
+
isHeadless: true,
|
|
163
|
+
asyncConsent: true,
|
|
164
|
+
asyncVisitorState: true,
|
|
165
|
+
consentDomain: config.checkoutRootDomain,
|
|
166
|
+
storefrontAccessToken: config.storefrontAccessToken,
|
|
167
|
+
debug: {
|
|
168
|
+
...privacy.config?.debug,
|
|
169
|
+
hydrogen: HYDROGEN_DEBUG_METADATA
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
if (config.country) shopify.country = config.country;
|
|
173
|
+
if (config.locale) shopify.locale = config.locale.toLowerCase();
|
|
174
|
+
const updateTrackingValues = () => {
|
|
175
|
+
const latest = getTrackingValues();
|
|
176
|
+
const previous = previousTrackingValues.current;
|
|
177
|
+
previousTrackingValues.current = latest;
|
|
178
|
+
if (previous && (previous.visitToken !== latest.visitToken || previous.uniqueToken !== latest.uniqueToken)) {
|
|
179
|
+
callbacks.current.revalidate().catch(() => {
|
|
210
180
|
console.warn(
|
|
211
181
|
"[h2:warn:useCustomerPrivacy] Revalidation failed after consent change."
|
|
212
182
|
);
|
|
213
183
|
});
|
|
214
184
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
onVisitorConsentCollected(event.detail);
|
|
185
|
+
};
|
|
186
|
+
const checkReady = () => {
|
|
187
|
+
if (!active) return false;
|
|
188
|
+
const customerPrivacy = getCustomerPrivacy();
|
|
189
|
+
const privacyBanner = getPrivacyBanner();
|
|
190
|
+
if (customerPrivacy) configureCustomerPrivacy(customerPrivacy, config);
|
|
191
|
+
if (withPrivacyBanner && privacyBanner) {
|
|
192
|
+
configurePrivacyBanner(privacyBanner, config);
|
|
228
193
|
}
|
|
194
|
+
setApis(
|
|
195
|
+
(previous) => previous.customerPrivacy === customerPrivacy && previous.privacyBanner === privacyBanner ? previous : { customerPrivacy, privacyBanner }
|
|
196
|
+
);
|
|
197
|
+
if (customerPrivacy?.consentStatus !== "loaded" || withPrivacyBanner && !privacyBanner) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (!notifiedReady.current) {
|
|
201
|
+
notifiedReady.current = true;
|
|
202
|
+
updateTrackingValues();
|
|
203
|
+
emitCustomerPrivacyApiLoaded(customerPrivacy);
|
|
204
|
+
callbacks.current.onReady?.();
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
229
207
|
};
|
|
208
|
+
const consentCollectedHandler = (event) => {
|
|
209
|
+
if (!checkReady()) return;
|
|
210
|
+
updateTrackingValues();
|
|
211
|
+
callbacks.current.onVisitorConsentCollected?.(event.detail);
|
|
212
|
+
};
|
|
213
|
+
document.addEventListener("consentTrackingApiLoaded", checkReady);
|
|
230
214
|
document.addEventListener(
|
|
231
215
|
"visitorConsentCollected",
|
|
232
216
|
consentCollectedHandler
|
|
233
217
|
);
|
|
218
|
+
if (!checkReady()) {
|
|
219
|
+
void loadScript(
|
|
220
|
+
withPrivacyBanner ? CONSENT_API_WITH_BANNER : CONSENT_API,
|
|
221
|
+
{
|
|
222
|
+
attributes: { id: "customer-privacy-api" }
|
|
223
|
+
}
|
|
224
|
+
).then(checkReady, () => {
|
|
225
|
+
if (active) {
|
|
226
|
+
console.error(
|
|
227
|
+
"[h2:error:useCustomerPrivacy] Unable to load the Customer Privacy API."
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
234
232
|
return () => {
|
|
233
|
+
active = false;
|
|
234
|
+
document.removeEventListener("consentTrackingApiLoaded", checkReady);
|
|
235
235
|
document.removeEventListener(
|
|
236
236
|
"visitorConsentCollected",
|
|
237
237
|
consentCollectedHandler
|
|
238
238
|
);
|
|
239
239
|
};
|
|
240
|
-
}, [
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
let customPrivacyBanner = window.privacyBanner || void 0;
|
|
245
|
-
const privacyBannerWatcher = {
|
|
246
|
-
configurable: true,
|
|
247
|
-
get() {
|
|
248
|
-
return customPrivacyBanner;
|
|
249
|
-
},
|
|
250
|
-
set(value) {
|
|
251
|
-
if (typeof value === "object" && value !== null && "showPreferences" in value && "loadBanner" in value) {
|
|
252
|
-
customPrivacyBanner = overridePrivacyBannerMethods({
|
|
253
|
-
privacyBanner: value,
|
|
254
|
-
config
|
|
255
|
-
});
|
|
256
|
-
setLoaded.privacyBanner();
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
Object.defineProperty(window, "privacyBanner", privacyBannerWatcher);
|
|
261
|
-
}, [
|
|
262
|
-
withPrivacyBanner,
|
|
263
|
-
config,
|
|
264
|
-
overridePrivacyBannerMethods,
|
|
265
|
-
setLoaded.privacyBanner
|
|
266
|
-
]);
|
|
267
|
-
useEffect(() => {
|
|
268
|
-
if (observing.current.customerPrivacy) return;
|
|
269
|
-
observing.current.customerPrivacy = true;
|
|
270
|
-
let backendConsentStub = null;
|
|
271
|
-
let fullCustomerPrivacy = null;
|
|
272
|
-
let customShopify = window.Shopify || void 0;
|
|
273
|
-
Object.defineProperty(window, "Shopify", {
|
|
274
|
-
configurable: true,
|
|
275
|
-
get() {
|
|
276
|
-
return customShopify;
|
|
277
|
-
},
|
|
278
|
-
set(value) {
|
|
279
|
-
if (typeof value === "object" && value !== null && Object.keys(value).length === 0) {
|
|
280
|
-
customShopify = value;
|
|
281
|
-
backendConsentStub = { backendConsentEnabled: true };
|
|
282
|
-
Object.defineProperty(window.Shopify, "customerPrivacy", {
|
|
283
|
-
configurable: true,
|
|
284
|
-
get() {
|
|
285
|
-
return fullCustomerPrivacy ?? backendConsentStub;
|
|
286
|
-
},
|
|
287
|
-
set(value2) {
|
|
288
|
-
if (typeof value2 === "object" && value2 !== null && "setTrackingConsent" in value2) {
|
|
289
|
-
const customerPrivacy = value2;
|
|
290
|
-
fullCustomerPrivacy = {
|
|
291
|
-
...customerPrivacy,
|
|
292
|
-
// Note: this method is not used by the privacy-banner,
|
|
293
|
-
// it bundles its own setTrackingConsent.
|
|
294
|
-
setTrackingConsent: overrideCustomerPrivacySetTrackingConsent(
|
|
295
|
-
{ customerPrivacy, config }
|
|
296
|
-
)
|
|
297
|
-
};
|
|
298
|
-
customShopify = {
|
|
299
|
-
...customShopify,
|
|
300
|
-
customerPrivacy: fullCustomerPrivacy
|
|
301
|
-
};
|
|
302
|
-
setLoaded.customerPrivacy();
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
}, [
|
|
310
|
-
config,
|
|
311
|
-
overrideCustomerPrivacySetTrackingConsent,
|
|
312
|
-
setLoaded.customerPrivacy
|
|
313
|
-
]);
|
|
314
|
-
useEffect(() => {
|
|
315
|
-
if (!apisLoaded || !cookiesReady) return;
|
|
316
|
-
const customerPrivacy = getCustomerPrivacy();
|
|
317
|
-
if (customerPrivacy && !customerPrivacy.cachedConsent) {
|
|
318
|
-
const trackingValues = getTrackingValues();
|
|
319
|
-
if (trackingValues.consent) {
|
|
320
|
-
customerPrivacy.cachedConsent = trackingValues.consent;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (withPrivacyBanner) {
|
|
324
|
-
const privacyBanner = getPrivacyBanner();
|
|
325
|
-
if (privacyBanner) {
|
|
326
|
-
privacyBanner.loadBanner(config);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
emitCustomerPrivacyApiLoaded();
|
|
330
|
-
onReady?.();
|
|
331
|
-
}, [apisLoaded, cookiesReady]);
|
|
332
|
-
const result = {
|
|
333
|
-
customerPrivacy: getCustomerPrivacy()
|
|
240
|
+
}, [config, withPrivacyBanner]);
|
|
241
|
+
return {
|
|
242
|
+
customerPrivacy: apis.customerPrivacy,
|
|
243
|
+
...withPrivacyBanner ? { privacyBanner: apis.privacyBanner } : {}
|
|
334
244
|
};
|
|
335
|
-
if (withPrivacyBanner) {
|
|
336
|
-
result.privacyBanner = getPrivacyBanner();
|
|
337
|
-
}
|
|
338
|
-
return result;
|
|
339
245
|
}
|
|
340
|
-
var
|
|
341
|
-
function emitCustomerPrivacyApiLoaded() {
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
setApisLoaded((prev) => [prev[0], true]);
|
|
366
|
-
}
|
|
246
|
+
var readyApis = /* @__PURE__ */ new WeakSet();
|
|
247
|
+
function emitCustomerPrivacyApiLoaded(customerPrivacy) {
|
|
248
|
+
if (readyApis.has(customerPrivacy)) return;
|
|
249
|
+
readyApis.add(customerPrivacy);
|
|
250
|
+
document.dispatchEvent(new CustomEvent("shopifyCustomerPrivacyApiLoaded"));
|
|
251
|
+
}
|
|
252
|
+
var HYDROGEN_DEBUG_METADATA = { generation: 2, serverTiming: false };
|
|
253
|
+
var configuredCustomerPrivacy = /* @__PURE__ */ new WeakMap();
|
|
254
|
+
function configureCustomerPrivacy(customerPrivacy, config) {
|
|
255
|
+
const existing = configuredCustomerPrivacy.get(customerPrivacy);
|
|
256
|
+
if (existing) {
|
|
257
|
+
existing.config = config;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const state = { config };
|
|
261
|
+
configuredCustomerPrivacy.set(customerPrivacy, state);
|
|
262
|
+
const original = customerPrivacy.setTrackingConsent;
|
|
263
|
+
customerPrivacy.setTrackingConsent = (consent, callback) => {
|
|
264
|
+
const { locale, country, ...headlessConfig } = state.config;
|
|
265
|
+
const params = {
|
|
266
|
+
...headlessConfig,
|
|
267
|
+
headlessStorefront: true,
|
|
268
|
+
...consent
|
|
269
|
+
};
|
|
270
|
+
original.call(customerPrivacy, params, callback);
|
|
367
271
|
};
|
|
368
|
-
|
|
272
|
+
}
|
|
273
|
+
var configuredPrivacyBanners = /* @__PURE__ */ new WeakMap();
|
|
274
|
+
function configurePrivacyBanner(privacyBanner, config) {
|
|
275
|
+
const existing = configuredPrivacyBanners.get(privacyBanner);
|
|
276
|
+
if (existing) {
|
|
277
|
+
existing.config = config;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const state = { config };
|
|
281
|
+
configuredPrivacyBanners.set(privacyBanner, state);
|
|
282
|
+
const { loadBanner, showPreferences } = privacyBanner;
|
|
283
|
+
privacyBanner.loadBanner = (options) => loadBanner.call(privacyBanner, { ...state.config, ...options });
|
|
284
|
+
privacyBanner.showPreferences = (options) => showPreferences.call(privacyBanner, { ...state.config, ...options });
|
|
369
285
|
}
|
|
370
286
|
function parseStoreDomain(checkoutDomain) {
|
|
371
287
|
if (typeof window === "undefined") return;
|
|
@@ -380,64 +296,25 @@ function parseStoreDomain(checkoutDomain) {
|
|
|
380
296
|
});
|
|
381
297
|
return sameDomainParts.reverse().join(".") || void 0;
|
|
382
298
|
}
|
|
383
|
-
function overrideCustomerPrivacySetTrackingConsent({
|
|
384
|
-
customerPrivacy,
|
|
385
|
-
config
|
|
386
|
-
}) {
|
|
387
|
-
const original = customerPrivacy.setTrackingConsent;
|
|
388
|
-
const { locale, country, ...rest } = config;
|
|
389
|
-
function updatedSetTrackingConsent(consent, callback) {
|
|
390
|
-
original(
|
|
391
|
-
{
|
|
392
|
-
...rest,
|
|
393
|
-
headlessStorefront: true,
|
|
394
|
-
...consent
|
|
395
|
-
},
|
|
396
|
-
callback
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
return updatedSetTrackingConsent;
|
|
400
|
-
}
|
|
401
|
-
function overridePrivacyBannerMethods({
|
|
402
|
-
privacyBanner,
|
|
403
|
-
config
|
|
404
|
-
}) {
|
|
405
|
-
const originalLoadBanner = privacyBanner.loadBanner;
|
|
406
|
-
const originalShowPreferences = privacyBanner.showPreferences;
|
|
407
|
-
function loadBanner(userConfig) {
|
|
408
|
-
if (typeof userConfig === "object") {
|
|
409
|
-
originalLoadBanner({ ...config, ...userConfig });
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
originalLoadBanner(config);
|
|
413
|
-
}
|
|
414
|
-
function showPreferences(userConfig) {
|
|
415
|
-
if (typeof userConfig === "object") {
|
|
416
|
-
originalShowPreferences({ ...config, ...userConfig });
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
originalShowPreferences(config);
|
|
420
|
-
}
|
|
421
|
-
return { loadBanner, showPreferences };
|
|
422
|
-
}
|
|
423
299
|
function getCustomerPrivacy() {
|
|
424
300
|
try {
|
|
425
301
|
const cp = window.Shopify?.customerPrivacy;
|
|
426
|
-
return cp
|
|
302
|
+
return typeof cp?.setTrackingConsent === "function" ? cp : null;
|
|
427
303
|
} catch (e) {
|
|
428
304
|
return null;
|
|
429
305
|
}
|
|
430
306
|
}
|
|
431
307
|
function getPrivacyBanner() {
|
|
432
308
|
try {
|
|
433
|
-
|
|
309
|
+
const banner = window.privacyBanner;
|
|
310
|
+
return typeof banner?.loadBanner === "function" && typeof banner.showPreferences === "function" ? banner : null;
|
|
434
311
|
} catch (e) {
|
|
435
312
|
return null;
|
|
436
313
|
}
|
|
437
314
|
}
|
|
438
315
|
|
|
439
316
|
// package.json
|
|
440
|
-
var version = "2026.4.
|
|
317
|
+
var version = "2026.4.6";
|
|
441
318
|
|
|
442
319
|
// src/analytics-manager/ShopifyAnalytics.tsx
|
|
443
320
|
function getCustomerPrivacyRequired() {
|
|
@@ -451,47 +328,13 @@ function getCustomerPrivacyRequired() {
|
|
|
451
328
|
}
|
|
452
329
|
function ShopifyAnalytics({
|
|
453
330
|
consent,
|
|
454
|
-
|
|
455
|
-
domain
|
|
331
|
+
onConsentChange
|
|
456
332
|
}) {
|
|
457
|
-
const { subscribe: subscribe2, register: register2
|
|
458
|
-
const [shopifyReady, setShopifyReady] = useState(false);
|
|
333
|
+
const { subscribe: subscribe2, register: register2 } = useAnalytics();
|
|
459
334
|
const [privacyReady, setPrivacyReady] = useState(false);
|
|
460
|
-
const [collectedConsent, setCollectedConsent] = useState("");
|
|
461
335
|
const init = useRef(false);
|
|
462
336
|
const { checkoutDomain, storefrontAccessToken, language } = consent;
|
|
463
337
|
const { ready: shopifyAnalyticsReady } = register2("Internal_Shopify_Analytics");
|
|
464
|
-
useCustomerPrivacy({
|
|
465
|
-
...consent,
|
|
466
|
-
locale: language,
|
|
467
|
-
checkoutDomain: !checkoutDomain ? "mock.shop" : checkoutDomain,
|
|
468
|
-
storefrontAccessToken: !storefrontAccessToken ? "abcdefghijklmnopqrstuvwxyz123456" : storefrontAccessToken,
|
|
469
|
-
// If we use privacy banner, we should wait until consent is collected.
|
|
470
|
-
// Otherwise, we can consider privacy ready immediately:
|
|
471
|
-
onReady: () => !consent.withPrivacyBanner && setPrivacyReady(true),
|
|
472
|
-
onVisitorConsentCollected: (consent2) => {
|
|
473
|
-
try {
|
|
474
|
-
setCollectedConsent(JSON.stringify(consent2));
|
|
475
|
-
} catch (e) {
|
|
476
|
-
}
|
|
477
|
-
setPrivacyReady(true);
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
const hasUserConsent = useMemo(
|
|
481
|
-
// must be initialized with true to avoid removing cookies too early
|
|
482
|
-
() => privacyReady ? canTrack() : true,
|
|
483
|
-
// Make this value depend on collectedConsent to re-run `canTrack()` when consent changes
|
|
484
|
-
[privacyReady, canTrack, collectedConsent]
|
|
485
|
-
);
|
|
486
|
-
useShopifyCookies({
|
|
487
|
-
hasUserConsent,
|
|
488
|
-
domain,
|
|
489
|
-
checkoutDomain,
|
|
490
|
-
// Already done inside useCustomerPrivacy
|
|
491
|
-
fetchTrackingValues: false,
|
|
492
|
-
// Avoid creating local cookies too early
|
|
493
|
-
ignoreDeprecatedCookies: !privacyReady
|
|
494
|
-
});
|
|
495
338
|
useEffect(() => {
|
|
496
339
|
if (init.current) return;
|
|
497
340
|
init.current = true;
|
|
@@ -500,14 +343,28 @@ function ShopifyAnalytics({
|
|
|
500
343
|
subscribe2(AnalyticsEvent.COLLECTION_VIEWED, collectionViewHandler);
|
|
501
344
|
subscribe2(AnalyticsEvent.SEARCH_VIEWED, searchViewHandler);
|
|
502
345
|
subscribe2(AnalyticsEvent.PRODUCT_ADD_TO_CART, productAddedToCartHandler);
|
|
503
|
-
setShopifyReady(true);
|
|
504
346
|
}, [subscribe2]);
|
|
347
|
+
const updatePrivacyReadiness = () => {
|
|
348
|
+
if (privacyReady) {
|
|
349
|
+
onConsentChange();
|
|
350
|
+
} else if (!consent.withPrivacyBanner || !shouldWaitForPrivacyBanner(getCustomerPrivacyRequired())) {
|
|
351
|
+
setPrivacyReady(true);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
useCustomerPrivacy({
|
|
355
|
+
...consent,
|
|
356
|
+
locale: language,
|
|
357
|
+
checkoutDomain: !checkoutDomain ? "mock.shop" : checkoutDomain,
|
|
358
|
+
storefrontAccessToken: !storefrontAccessToken ? "abcdefghijklmnopqrstuvwxyz123456" : storefrontAccessToken,
|
|
359
|
+
onReady: updatePrivacyReadiness,
|
|
360
|
+
onVisitorConsentCollected: updatePrivacyReadiness
|
|
361
|
+
});
|
|
505
362
|
useEffect(() => {
|
|
506
|
-
if (
|
|
363
|
+
if (privacyReady) {
|
|
507
364
|
shopifyAnalyticsReady();
|
|
508
|
-
|
|
365
|
+
onConsentChange();
|
|
509
366
|
}
|
|
510
|
-
}, [
|
|
367
|
+
}, [privacyReady, onConsentChange]);
|
|
511
368
|
return null;
|
|
512
369
|
}
|
|
513
370
|
function logMissingConfig2(fieldName) {
|
|
@@ -517,7 +374,8 @@ function logMissingConfig2(fieldName) {
|
|
|
517
374
|
}
|
|
518
375
|
function prepareBasePageViewPayload(payload) {
|
|
519
376
|
const customerPrivacy = getCustomerPrivacyRequired();
|
|
520
|
-
const hasUserConsent =
|
|
377
|
+
const hasUserConsent = hasAnalyticsConsent();
|
|
378
|
+
if (!hasUserConsent) return;
|
|
521
379
|
if (!payload?.shop?.shopId) {
|
|
522
380
|
logMissingConfig2("shopId");
|
|
523
381
|
return;
|
|
@@ -958,13 +816,6 @@ function register(key) {
|
|
|
958
816
|
}
|
|
959
817
|
};
|
|
960
818
|
}
|
|
961
|
-
function shopifyCanTrack() {
|
|
962
|
-
try {
|
|
963
|
-
return window.Shopify.customerPrivacy?.analyticsProcessingAllowed?.() ?? false;
|
|
964
|
-
} catch (e) {
|
|
965
|
-
}
|
|
966
|
-
return false;
|
|
967
|
-
}
|
|
968
819
|
function messageOnError(field, envVar) {
|
|
969
820
|
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.`;
|
|
970
821
|
}
|
|
@@ -974,18 +825,17 @@ function AnalyticsProvider({
|
|
|
974
825
|
children,
|
|
975
826
|
consent,
|
|
976
827
|
customData = {},
|
|
977
|
-
shop: shopProp = null
|
|
978
|
-
cookieDomain
|
|
828
|
+
shop: shopProp = null
|
|
979
829
|
}) {
|
|
980
830
|
const { shop } = useShopAnalytics(shopProp);
|
|
981
|
-
const [
|
|
982
|
-
|
|
983
|
-
);
|
|
984
|
-
const [consentCollected, setConsentCollected] = useState(false);
|
|
831
|
+
const [consentVersion, setConsentVersion] = useState(0);
|
|
832
|
+
const [privacyReady, setPrivacyReady] = useState(false);
|
|
985
833
|
const [carts, setCarts] = useState({ cart: null, prevCart: null });
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
|
|
834
|
+
const canTrack = customCanTrack ?? hasAnalyticsConsent;
|
|
835
|
+
const onConsentChange = useCallback(() => {
|
|
836
|
+
setPrivacyReady(true);
|
|
837
|
+
setConsentVersion((version2) => version2 + 1);
|
|
838
|
+
}, []);
|
|
989
839
|
if (!!shop) {
|
|
990
840
|
if (/\/68817551382$/.test(shop.shopId)) {
|
|
991
841
|
warnOnce(
|
|
@@ -1031,7 +881,7 @@ function AnalyticsProvider({
|
|
|
1031
881
|
privacyBanner: getPrivacyBanner()
|
|
1032
882
|
};
|
|
1033
883
|
}, [
|
|
1034
|
-
|
|
884
|
+
consentVersion,
|
|
1035
885
|
canTrack,
|
|
1036
886
|
carts,
|
|
1037
887
|
carts.cart?.updatedAt,
|
|
@@ -1049,21 +899,8 @@ function AnalyticsProvider({
|
|
|
1049
899
|
children,
|
|
1050
900
|
!!shop && /* @__PURE__ */ jsx(AnalyticsPageView, {}),
|
|
1051
901
|
!!shop && !!currentCart && /* @__PURE__ */ jsx(CartAnalytics, { cart: currentCart, setCarts }),
|
|
1052
|
-
!!shop && /* @__PURE__ */ jsx(
|
|
1053
|
-
|
|
1054
|
-
{
|
|
1055
|
-
consent,
|
|
1056
|
-
onReady: () => {
|
|
1057
|
-
setAnalyticsLoaded(true);
|
|
1058
|
-
setCanTrack(
|
|
1059
|
-
customCanTrack ? () => customCanTrack : () => shopifyCanTrack
|
|
1060
|
-
);
|
|
1061
|
-
setConsentCollected(true);
|
|
1062
|
-
},
|
|
1063
|
-
domain: cookieDomain
|
|
1064
|
-
}
|
|
1065
|
-
),
|
|
1066
|
-
!!shop && consentCollected && /* @__PURE__ */ jsx(PerfKit, { shop })
|
|
902
|
+
!!shop && /* @__PURE__ */ jsx(ShopifyAnalytics, { consent, onConsentChange }),
|
|
903
|
+
!!shop && privacyReady && /* @__PURE__ */ jsx(PerfKit, { shop })
|
|
1067
904
|
] });
|
|
1068
905
|
}
|
|
1069
906
|
function useAnalytics() {
|
|
@@ -1128,6 +965,17 @@ var Analytics = {
|
|
|
1128
965
|
SearchView: AnalyticsSearchView
|
|
1129
966
|
};
|
|
1130
967
|
|
|
968
|
+
// src/constants.ts
|
|
969
|
+
var STOREFRONT_REQUEST_GROUP_ID_HEADER = "Custom-Storefront-Request-Group-ID";
|
|
970
|
+
var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
|
|
971
|
+
var SDK_VARIANT_HEADER = "X-SDK-Variant";
|
|
972
|
+
var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
|
|
973
|
+
var SDK_VERSION_HEADER = "X-SDK-Version";
|
|
974
|
+
var SHOPIFY_CLIENT_IP_HEADER = "X-Shopify-Client-IP";
|
|
975
|
+
var SHOPIFY_CLIENT_IP_SIG_HEADER = "X-Shopify-Client-IP-Sig";
|
|
976
|
+
var HYDROGEN_SFAPI_PROXY_KEY = "_sfapi_proxy";
|
|
977
|
+
var STOREFRONT_CONSENT_MANAGEMENT_HEADER = "Shopify-Storefront-Consent-Management";
|
|
978
|
+
|
|
1131
979
|
// src/utils/request.ts
|
|
1132
980
|
function getHeader(request, key) {
|
|
1133
981
|
return getHeaderValue(request.headers, key);
|
|
@@ -1833,7 +1681,7 @@ function generateUUID() {
|
|
|
1833
1681
|
}
|
|
1834
1682
|
|
|
1835
1683
|
// src/version.ts
|
|
1836
|
-
var LIB_VERSION = "2026.4.
|
|
1684
|
+
var LIB_VERSION = "2026.4.6";
|
|
1837
1685
|
|
|
1838
1686
|
// src/utils/graphql.ts
|
|
1839
1687
|
function minifyQuery(string) {
|
|
@@ -2094,8 +1942,7 @@ function createStorefrontClient(options) {
|
|
|
2094
1942
|
streamConfig,
|
|
2095
1943
|
onRawHeaders: (headers2) => {
|
|
2096
1944
|
collectedSubrequestHeaders ??= {
|
|
2097
|
-
setCookie: headers2.getSetCookie()
|
|
2098
|
-
serverTiming: headers2.get("server-timing") ?? ""
|
|
1945
|
+
setCookie: headers2.getSetCookie()
|
|
2099
1946
|
};
|
|
2100
1947
|
}
|
|
2101
1948
|
});
|
|
@@ -2228,6 +2075,10 @@ function createStorefrontClient(options) {
|
|
|
2228
2075
|
"origin",
|
|
2229
2076
|
"referer",
|
|
2230
2077
|
"user-agent",
|
|
2078
|
+
// The marker identifies headless consent-management traffic
|
|
2079
|
+
// to the backend, so the proxy forwards it upstream with the
|
|
2080
|
+
// rest of the allowlisted set.
|
|
2081
|
+
STOREFRONT_CONSENT_MANAGEMENT_HEADER,
|
|
2231
2082
|
STOREFRONT_ACCESS_TOKEN_HEADER,
|
|
2232
2083
|
SHOPIFY_UNIQUE_TOKEN_HEADER,
|
|
2233
2084
|
SHOPIFY_VISIT_TOKEN_HEADER
|
|
@@ -2256,7 +2107,9 @@ function createStorefrontClient(options) {
|
|
|
2256
2107
|
headers: forwardedHeaders
|
|
2257
2108
|
}
|
|
2258
2109
|
);
|
|
2259
|
-
|
|
2110
|
+
const proxyResponse = new Response(sfapiResponse.body, sfapiResponse);
|
|
2111
|
+
proxyResponse.headers.delete("server-timing");
|
|
2112
|
+
return proxyResponse;
|
|
2260
2113
|
},
|
|
2261
2114
|
/**
|
|
2262
2115
|
* Checks if the request is targeting the Storefront MCP endpoint.
|
|
@@ -2305,7 +2158,9 @@ function createStorefrontClient(options) {
|
|
|
2305
2158
|
body: request.body,
|
|
2306
2159
|
headers: forwardedHeaders
|
|
2307
2160
|
});
|
|
2308
|
-
|
|
2161
|
+
const proxyResponse = new Response(mcpResponse.body, mcpResponse);
|
|
2162
|
+
proxyResponse.headers.delete("server-timing");
|
|
2163
|
+
return proxyResponse;
|
|
2309
2164
|
} catch (error) {
|
|
2310
2165
|
const JSON_RPC_INTERNAL_ERROR = -32603;
|
|
2311
2166
|
const message = error instanceof Error ? error.message : "Internal proxy error";
|
|
@@ -2328,21 +2183,6 @@ function createStorefrontClient(options) {
|
|
|
2328
2183
|
response.headers.append("Set-Cookie", value);
|
|
2329
2184
|
}
|
|
2330
2185
|
}
|
|
2331
|
-
const serverTiming = extractServerTimingHeader(
|
|
2332
|
-
collectedSubrequestHeaders?.serverTiming
|
|
2333
|
-
);
|
|
2334
|
-
const isDocumentResponse = response.headers.get("content-type")?.startsWith("text/html");
|
|
2335
|
-
const fallbackValues = isDocumentResponse ? { _y: uniqueToken, _s: visitToken } : void 0;
|
|
2336
|
-
appendServerTimingHeader(response, {
|
|
2337
|
-
...fallbackValues,
|
|
2338
|
-
...serverTiming
|
|
2339
|
-
});
|
|
2340
|
-
if (isDocumentResponse && collectedSubrequestHeaders && // _shopify_essential cookie is always set, but we need more than that
|
|
2341
|
-
collectedSubrequestHeaders.setCookie.length > 1 && serverTiming?._y && serverTiming?._s && serverTiming?._cmp) {
|
|
2342
|
-
appendServerTimingHeader(response, {
|
|
2343
|
-
[HYDROGEN_SERVER_TRACKING_KEY]: "1"
|
|
2344
|
-
});
|
|
2345
|
-
}
|
|
2346
2186
|
}
|
|
2347
2187
|
}
|
|
2348
2188
|
};
|
|
@@ -4504,6 +4344,19 @@ function createHydrogenContext(options, additionalContext) {
|
|
|
4504
4344
|
});
|
|
4505
4345
|
return hybridProvider;
|
|
4506
4346
|
}
|
|
4347
|
+
|
|
4348
|
+
// src/utils/server-timing.ts
|
|
4349
|
+
function buildServerTimingHeader(values) {
|
|
4350
|
+
return Object.entries(values).map(([key, value]) => value ? `${key};desc=${value}` : void 0).filter(Boolean).join(", ");
|
|
4351
|
+
}
|
|
4352
|
+
function appendServerTimingHeader(response, values) {
|
|
4353
|
+
const header = typeof values === "string" ? values : buildServerTimingHeader(values);
|
|
4354
|
+
if (header) {
|
|
4355
|
+
response.headers.append("Server-Timing", header);
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
|
|
4359
|
+
// src/createRequestHandler.ts
|
|
4507
4360
|
function createRequestHandler({
|
|
4508
4361
|
build,
|
|
4509
4362
|
mode,
|
|
@@ -6018,7 +5871,7 @@ function renderTitle(template, title) {
|
|
|
6018
5871
|
return template.replace("%s", title ?? "");
|
|
6019
5872
|
}
|
|
6020
5873
|
function inferMimeType(url) {
|
|
6021
|
-
const ext = url && url.split(".").pop();
|
|
5874
|
+
const ext = url && url.split(/[?#]/)[0].split(".").pop()?.toLowerCase();
|
|
6022
5875
|
switch (ext) {
|
|
6023
5876
|
case "svg":
|
|
6024
5877
|
return "image/svg+xml";
|