UNPKG

@shopify/hydrogen

Version:
1,656 lines (1,637 loc) • 183 kB
'use strict'; var react = require('react'); var react$1 = require('@remix-run/react'); var jsxRuntime = require('react/jsx-runtime'); var hydrogenReact = require('@shopify/hydrogen-react'); var cookie = require('worktop/cookie'); var cspBuilder = require('content-security-policy-builder'); require('url'); require('path'); require('fs/promises'); var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null; function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var cspBuilder__default = /*#__PURE__*/_interopDefault(cspBuilder); var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/seo/log-seo-tags.ts var log_seo_tags_exports = {}; __export(log_seo_tags_exports, { default: () => Logger, logSeoTags: () => logSeoTags }); function Logger({ headTags }) { logSeoTags(headTags); return null; } function logSeoTags(headTags) { console.log(" "); console.log("%cSEO Meta Tags", `${titleStyle}`); console.log(" "); headTags.forEach((tag) => { if (tag.tag === "script") { console.log(`%c\u2022 JSON LD `, headingStyle); if (tag.children) { try { console.table(JSON.parse(tag.children), ["name", "content"]); } catch { console.log(tag.children); } } } else { console.log(`%c\u2022 ${tag.tag} `, headingStyle); if (tag.children) { if (typeof tag.children === "string") { console.log(`\u21B3 ${tag.children}`); } else { try { Object.entries(JSON.parse(tag.children)).map( ([key, val]) => console.log(`\u21B3 ${val}`) ); } catch { console.log(tag.children); } } } if (tag.props.property === "og:image:url") { const urlKey = tag.props.content; fetchImage(urlKey).then((image) => { const imageStyle = `font-size: 400px; padding: 10px; background: white url(${image}) no-repeat center; background-size: contain;`; console.log(`%c\u2022 Share image preview`, headingStyle); console.log("%c ", imageStyle); console.log(`\u21B3 ${urlKey}`); }).catch((err) => { console.error(err); }); } Object.entries(tag.props).map(([key, val]) => { console.log(`\u21B3 ${key} \u2192 ${val}`); }); } console.log(" "); }); } async function fetchImage(url) { const result = await fetch(url); const data = await result.blob(); const buff = await data.arrayBuffer(); const base64String = arrayBufferToBase64(buff); return `data:image/png;base64,${base64String}`; } function arrayBufferToBase64(buffer) { let binary = ""; const bytes = new Uint8Array(buffer); const len = bytes.byteLength; for (let index = 0; index < len; index++) { binary += String.fromCharCode(bytes[index]); } return btoa(binary); } var headingStyle, titleStyle; var init_log_seo_tags = __esm({ "src/seo/log-seo-tags.ts"() { headingStyle = "text-transform: uppercase;"; titleStyle = "text-transform: uppercase; font-weight: bold; text-transform: uppercase;font-weight: bold"; } }); function AnalyticsView(props) { const { type, data = {}, customData } = props; const location = react$1.useLocation(); const { publish: publish2, cart, prevCart, shop, customData: analyticProviderCustomData } = useAnalytics(); const url = location.pathname + location.search; let viewPayload2 = { ...data, customData: { ...analyticProviderCustomData, ...customData }, cart, prevCart, shop }; react.useEffect(() => { if (!shop?.shopId) return; viewPayload2 = { ...viewPayload2, url: window.location.href }; publish2(type, viewPayload2); }, [publish2, url, shop?.shopId]); return null; } function AnalyticsPageView(props) { return /* @__PURE__ */ jsxRuntime.jsx(AnalyticsView, { ...props, type: "page_viewed" }); } function AnalyticsProductView(props) { return /* @__PURE__ */ jsxRuntime.jsx(AnalyticsView, { ...props, type: "product_viewed" }); } function AnalyticsCollectionView(props) { return /* @__PURE__ */ jsxRuntime.jsx(AnalyticsView, { ...props, type: "collection_viewed" }); } function AnalyticsCartView(props) { return /* @__PURE__ */ jsxRuntime.jsx(AnalyticsView, { ...props, type: "cart_viewed" }); } function AnalyticsSearchView(props) { return /* @__PURE__ */ jsxRuntime.jsx(AnalyticsView, { ...props, type: "search_viewed" }); } function AnalyticsCustomView(props) { return /* @__PURE__ */ jsxRuntime.jsx(AnalyticsView, { ...props }); } // src/analytics-manager/events.ts var AnalyticsEvent = { // Views PAGE_VIEWED: "page_viewed", PRODUCT_VIEWED: "product_viewed", COLLECTION_VIEWED: "collection_viewed", CART_VIEWED: "cart_viewed", SEARCH_VIEWED: "search_viewed", // Cart CART_UPDATED: "cart_updated", PRODUCT_ADD_TO_CART: "product_added_to_cart", PRODUCT_REMOVED_FROM_CART: "product_removed_from_cart", // Custom CUSTOM_EVENT: `custom_` }; var CONSENT_API = "https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.1/consent-tracking-api.js"; var CONSENT_API_WITH_BANNER = "https://cdn.shopify.com/shopifycloud/privacy-banner/storefront-banner.js"; function logMissingConfig(fieldName) { console.error( `[h2:error:useCustomerPrivacy] Unable to setup Customer Privacy API: Missing consent.${fieldName} configuration.` ); } function useCustomerPrivacy(props) { const { withPrivacyBanner = false, onVisitorConsentCollected, onReady, ...consentConfig } = props; hydrogenReact.useLoadScript(withPrivacyBanner ? CONSENT_API_WITH_BANNER : CONSENT_API, { attributes: { id: "customer-privacy-api" } }); const { observing, setLoaded } = useApisLoaded({ withPrivacyBanner, onLoaded: onReady }); const config = react.useMemo(() => { const { checkoutDomain, storefrontAccessToken } = consentConfig; if (!checkoutDomain) logMissingConfig("checkoutDomain"); if (!storefrontAccessToken) logMissingConfig("storefrontAccessToken"); if (storefrontAccessToken.startsWith("shpat_") || storefrontAccessToken.length !== 32) { console.error( `[h2:error:useCustomerPrivacy] It looks like you passed a private access token, make sure to use the public token` ); } const config2 = { checkoutRootDomain: checkoutDomain, storefrontAccessToken, storefrontRootDomain: parseStoreDomain(checkoutDomain), country: consentConfig.country, locale: consentConfig.locale }; return config2; }, [consentConfig, parseStoreDomain, logMissingConfig]); react.useEffect(() => { const consentCollectedHandler = (event) => { if (onVisitorConsentCollected) { onVisitorConsentCollected(event.detail); } }; document.addEventListener( "visitorConsentCollected", consentCollectedHandler ); return () => { document.removeEventListener( "visitorConsentCollected", consentCollectedHandler ); }; }, [onVisitorConsentCollected]); react.useEffect(() => { if (!withPrivacyBanner || observing.current.privacyBanner) return; observing.current.privacyBanner = true; let customPrivacyBanner = window.privacyBanner || void 0; const privacyBannerWatcher = { configurable: true, get() { return customPrivacyBanner; }, set(value) { if (typeof value === "object" && value !== null && "showPreferences" in value && "loadBanner" in value) { const privacyBanner = value; privacyBanner.loadBanner(config); customPrivacyBanner = overridePrivacyBannerMethods({ privacyBanner, config }); setLoaded.privacyBanner(); emitCustomerPrivacyApiLoaded(); } } }; Object.defineProperty(window, "privacyBanner", privacyBannerWatcher); }, [ withPrivacyBanner, config, overridePrivacyBannerMethods, setLoaded.privacyBanner ]); react.useEffect(() => { if (observing.current.customerPrivacy) return; observing.current.customerPrivacy = true; let customCustomerPrivacy = null; let customShopify = window.Shopify || void 0; Object.defineProperty(window, "Shopify", { configurable: true, get() { return customShopify; }, set(value) { if (typeof value === "object" && value !== null && Object.keys(value).length === 0) { customShopify = value; Object.defineProperty(window.Shopify, "customerPrivacy", { configurable: true, get() { return customCustomerPrivacy; }, set(value2) { if (typeof value2 === "object" && value2 !== null && "setTrackingConsent" in value2) { const customerPrivacy = value2; customCustomerPrivacy = { ...customerPrivacy, setTrackingConsent: overrideCustomerPrivacySetTrackingConsent( { customerPrivacy, config } ) }; customShopify = { ...customShopify, customerPrivacy: customCustomerPrivacy }; setLoaded.customerPrivacy(); emitCustomerPrivacyApiLoaded(); } } }); } } }); }, [ config, overrideCustomerPrivacySetTrackingConsent, setLoaded.customerPrivacy ]); const result = { customerPrivacy: getCustomerPrivacy() }; if (withPrivacyBanner) { result.privacyBanner = getPrivacyBanner(); } return result; } var hasEmitted = false; function emitCustomerPrivacyApiLoaded() { if (hasEmitted) return; hasEmitted = true; const event = new CustomEvent("shopifyCustomerPrivacyApiLoaded"); document.dispatchEvent(event); } function useApisLoaded({ withPrivacyBanner, onLoaded }) { const observing = react.useRef({ customerPrivacy: false, privacyBanner: false }); const [apisLoaded, setApisLoaded] = react.useState( withPrivacyBanner ? [false, false] : [false] ); const loaded = apisLoaded.every(Boolean); const setLoaded = { customerPrivacy: () => { if (withPrivacyBanner) { setApisLoaded((prev) => [true, prev[1]]); } else { setApisLoaded(() => [true]); } }, privacyBanner: () => { if (!withPrivacyBanner) { return; } setApisLoaded((prev) => [prev[0], true]); } }; react.useEffect(() => { if (loaded && onLoaded) { onLoaded(); } }, [loaded, onLoaded]); return { observing, setLoaded }; } function parseStoreDomain(checkoutDomain) { if (typeof window === "undefined") return; const host = window.document.location.host; const checkoutDomainParts = checkoutDomain.split(".").reverse(); const currentDomainParts = host.split(".").reverse(); const sameDomainParts = []; checkoutDomainParts.forEach((part, index) => { if (part === currentDomainParts[index]) { sameDomainParts.push(part); } }); return sameDomainParts.reverse().join("."); } function overrideCustomerPrivacySetTrackingConsent({ customerPrivacy, config }) { const original = customerPrivacy.setTrackingConsent; const { locale, country, ...rest } = config; function updatedSetTrackingConsent(consent, callback) { original( { ...rest, headlessStorefront: true, ...consent }, callback ); } return updatedSetTrackingConsent; } function overridePrivacyBannerMethods({ privacyBanner, config }) { const originalLoadBanner = privacyBanner.loadBanner; const originalShowPreferences = privacyBanner.showPreferences; function loadBanner(userConfig) { if (typeof userConfig === "object") { originalLoadBanner({ ...config, ...userConfig }); return; } originalLoadBanner(config); } function showPreferences(userConfig) { if (typeof userConfig === "object") { originalShowPreferences({ ...config, ...userConfig }); return; } originalShowPreferences(config); } return { loadBanner, showPreferences }; } function getCustomerPrivacy() { try { return window.Shopify && window.Shopify.customerPrivacy ? window.Shopify?.customerPrivacy : null; } catch (e) { return null; } } function getPrivacyBanner() { try { return window && window?.privacyBanner ? window.privacyBanner : null; } catch (e) { return null; } } // package.json var version = "2025.1.3"; // src/analytics-manager/ShopifyAnalytics.tsx function getCustomerPrivacyRequired() { const customerPrivacy = getCustomerPrivacy(); if (!customerPrivacy) { throw new Error( "Shopify Customer Privacy API not available. Must be used within a useEffect. Make sure to load the Shopify Customer Privacy API with useCustomerPrivacy() or <AnalyticsProvider>." ); } return customerPrivacy; } function ShopifyAnalytics({ consent, onReady, domain }) { const { subscribe: subscribe2, register: register2, canTrack } = useAnalytics(); const [shopifyReady, setShopifyReady] = react.useState(false); const [privacyReady, setPrivacyReady] = react.useState(false); const init = react.useRef(false); const { checkoutDomain, storefrontAccessToken, language } = consent; const { ready: shopifyAnalyticsReady } = register2("Internal_Shopify_Analytics"); useCustomerPrivacy({ ...consent, locale: language, checkoutDomain: !checkoutDomain ? "mock.shop" : checkoutDomain, storefrontAccessToken: !storefrontAccessToken ? "abcdefghijklmnopqrstuvwxyz123456" : storefrontAccessToken, onVisitorConsentCollected: () => setPrivacyReady(true), onReady: () => setPrivacyReady(true) }); hydrogenReact.useShopifyCookies({ hasUserConsent: privacyReady ? canTrack() : true, // must be initialized with true domain, checkoutDomain }); react.useEffect(() => { if (init.current) return; init.current = true; subscribe2(AnalyticsEvent.PAGE_VIEWED, pageViewHandler); subscribe2(AnalyticsEvent.PRODUCT_VIEWED, productViewHandler); subscribe2(AnalyticsEvent.COLLECTION_VIEWED, collectionViewHandler); subscribe2(AnalyticsEvent.SEARCH_VIEWED, searchViewHandler); subscribe2(AnalyticsEvent.PRODUCT_ADD_TO_CART, productAddedToCartHandler); setShopifyReady(true); }, [subscribe2]); react.useEffect(() => { if (shopifyReady && privacyReady) { shopifyAnalyticsReady(); onReady(); } }, [shopifyReady, privacyReady, onReady]); return null; } function logMissingConfig2(fieldName) { console.error( `[h2:error:ShopifyAnalytics] Unable to send Shopify analytics: Missing shop.${fieldName} configuration.` ); } function prepareBasePageViewPayload(payload) { const customerPrivacy = getCustomerPrivacyRequired(); const hasUserConsent = customerPrivacy.analyticsProcessingAllowed(); if (!payload?.shop?.shopId) { logMissingConfig2("shopId"); return; } if (!payload?.shop?.acceptedLanguage) { logMissingConfig2("acceptedLanguage"); return; } if (!payload?.shop?.currency) { logMissingConfig2("currency"); return; } if (!payload?.shop?.hydrogenSubchannelId) { logMissingConfig2("hydrogenSubchannelId"); return; } const eventPayload = { shopifySalesChannel: "hydrogen", assetVersionId: version, ...payload.shop, hasUserConsent, ...hydrogenReact.getClientBrowserParameters(), ccpaEnforced: !customerPrivacy.saleOfDataAllowed(), gdprEnforced: !(customerPrivacy.marketingAllowed() && customerPrivacy.analyticsProcessingAllowed()), analyticsAllowed: customerPrivacy.analyticsProcessingAllowed(), marketingAllowed: customerPrivacy.marketingAllowed(), saleOfDataAllowed: customerPrivacy.saleOfDataAllowed() }; return eventPayload; } function prepareBaseCartPayload(payload, cart) { if (cart === null) return; const pageViewPayload = prepareBasePageViewPayload(payload); if (!pageViewPayload) return; const eventPayload = { ...pageViewPayload, cartId: cart.id }; return eventPayload; } var viewPayload = {}; function pageViewHandler(payload) { const eventPayload = prepareBasePageViewPayload(payload); if (!eventPayload) return; hydrogenReact.sendShopifyAnalytics({ eventName: hydrogenReact.AnalyticsEventName.PAGE_VIEW_2, payload: { ...eventPayload, ...viewPayload } }); viewPayload = {}; } function productViewHandler(payload) { let eventPayload = prepareBasePageViewPayload(payload); if (eventPayload && validateProducts({ type: "product", products: payload.products })) { const formattedProducts = formatProduct(payload.products); viewPayload = { pageType: hydrogenReact.AnalyticsPageType.product, resourceId: formattedProducts[0].productGid }; eventPayload = { ...eventPayload, ...viewPayload, products: formatProduct(payload.products) }; hydrogenReact.sendShopifyAnalytics({ eventName: hydrogenReact.AnalyticsEventName.PRODUCT_VIEW, payload: eventPayload }); } } function collectionViewHandler(payload) { let eventPayload = prepareBasePageViewPayload(payload); if (!eventPayload) return; viewPayload = { pageType: hydrogenReact.AnalyticsPageType.collection, resourceId: payload.collection.id }; eventPayload = { ...eventPayload, ...viewPayload, collectionHandle: payload.collection.handle, collectionId: payload.collection.id }; hydrogenReact.sendShopifyAnalytics({ eventName: hydrogenReact.AnalyticsEventName.COLLECTION_VIEW, payload: eventPayload }); } function searchViewHandler(payload) { let eventPayload = prepareBasePageViewPayload(payload); if (!eventPayload) return; viewPayload = { pageType: hydrogenReact.AnalyticsPageType.search }; eventPayload = { ...eventPayload, ...viewPayload, searchString: payload.searchTerm }; hydrogenReact.sendShopifyAnalytics({ eventName: hydrogenReact.AnalyticsEventName.SEARCH_VIEW, payload: eventPayload }); } function productAddedToCartHandler(payload) { const { cart, currentLine } = payload; const eventPayload = prepareBaseCartPayload(payload, cart); if (!eventPayload || !currentLine?.id) return; sendCartAnalytics({ matchedLine: currentLine, eventPayload }); } function sendCartAnalytics({ matchedLine, eventPayload }) { const product = { id: matchedLine.merchandise.product.id, variantId: matchedLine.merchandise.id, title: matchedLine.merchandise.product.title, variantTitle: matchedLine.merchandise.title, vendor: matchedLine.merchandise.product.vendor, price: matchedLine.merchandise.price.amount, quantity: matchedLine.quantity, productType: matchedLine.merchandise.product.productType, sku: matchedLine.merchandise.sku }; if (validateProducts({ type: "cart", products: [product] })) { hydrogenReact.sendShopifyAnalytics({ eventName: hydrogenReact.AnalyticsEventName.ADD_TO_CART, payload: { ...eventPayload, products: formatProduct([product]) } }); } } function missingErrorMessage(type, fieldName, isVariantField, viewKeyName) { if (type === "cart") { const name = `${isVariantField ? "merchandise" : "merchandise.product"}.${fieldName}`; console.error( `[h2:error:ShopifyAnalytics] Can't set up cart analytics events because the \`cart.lines[].${name}\` value is missing from your GraphQL cart query. In your project, search for where \`fragment CartLine on CartLine\` is defined and make sure \`${name}\` is part of your cart query. Check the Hydrogen Skeleton template for reference: https://github.com/Shopify/hydrogen/blob/main/templates/skeleton/app/lib/fragments.ts#L25-L56.` ); } else { const name = `${viewKeyName || fieldName}`; console.error( `[h2:error:ShopifyAnalytics] Can't set up product view analytics events because the \`${name}\` is missing from your \`<Analytics.ProductView>\`. Make sure \`${name}\` is part of your products data prop. Check the Hydrogen Skeleton template for reference: https://github.com/Shopify/hydrogen/blob/main/templates/skeleton/app/routes/products.%24handle.tsx#L159-L165.` ); } } function validateProducts({ type, products }) { if (!products || products.length === 0) { missingErrorMessage(type, "", false, "data.products"); return false; } products.forEach((product) => { if (!product.id) { missingErrorMessage(type, "id", false); return false; } if (!product.title) { missingErrorMessage(type, "title", false); return false; } if (!product.price) { missingErrorMessage(type, "price.amount", true, "price"); return false; } if (!product.vendor) { missingErrorMessage(type, "vendor", false); return false; } if (!product.variantId) { missingErrorMessage(type, "id", true, "variantId"); return false; } if (!product.variantTitle) { missingErrorMessage(type, "title", true, "variantTitle"); return false; } }); return true; } function formatProduct(products) { return products.map((product) => { const formattedProduct = { productGid: product.id, variantGid: product.variantId, name: product.title, variantName: product.variantTitle, brand: product.vendor, price: product.price, quantity: product.quantity || 1, category: product.productType }; if (product.sku) formattedProduct.sku = product.sku; if (product.productType) formattedProduct.category = product.productType; return formattedProduct; }); } function logMissingField(fieldName) { console.error( `[h2:error:CartAnalytics] Can't set up cart analytics events because the \`cart.${fieldName}\` value is missing from your GraphQL cart query. In your project, search for where \`fragment CartApiQuery on Cart\` is defined and make sure \`${fieldName}\` is part of your cart query. Check the Hydrogen Skeleton template for reference: https://github.com/Shopify/hydrogen/blob/main/templates/skeleton/app/lib/fragments.ts#L59.` ); } function CartAnalytics({ cart: currentCart, setCarts }) { const { publish: publish2, shop, customData, canTrack, cart, prevCart } = useAnalytics(); const lastEventId = react.useRef(null); react.useEffect(() => { if (!currentCart) return; Promise.resolve(currentCart).then((updatedCart) => { if (updatedCart && updatedCart.lines) { if (!updatedCart.id) { logMissingField("id"); return; } if (!updatedCart.updatedAt) { logMissingField("updatedAt"); return; } } setCarts(({ cart: cart2, prevCart: prevCart2 }) => { return updatedCart?.updatedAt !== cart2?.updatedAt ? { cart: updatedCart, prevCart: cart2 } : { cart: cart2, prevCart: prevCart2 }; }); }); return () => { }; }, [setCarts, currentCart]); react.useEffect(() => { if (!cart || !cart?.updatedAt) return; if (cart?.updatedAt === prevCart?.updatedAt) return; let cartLastUpdatedAt; try { cartLastUpdatedAt = JSON.parse( localStorage.getItem("cartLastUpdatedAt") || "" ); } catch (e) { cartLastUpdatedAt = null; } if (cart.id === cartLastUpdatedAt?.id && cart.updatedAt === cartLastUpdatedAt?.updatedAt) return; const payload = { eventTimestamp: Date.now(), cart, prevCart, shop, customData }; if (cart.updatedAt === lastEventId.current) return; lastEventId.current = cart.updatedAt; publish2("cart_updated", payload); localStorage.setItem( "cartLastUpdatedAt", JSON.stringify({ id: cart.id, updatedAt: cart.updatedAt }) ); const previousCartLines = prevCart?.lines ? hydrogenReact.flattenConnection(prevCart?.lines) : []; const currentCartLines = cart.lines ? hydrogenReact.flattenConnection(cart.lines) : []; previousCartLines?.forEach((prevLine) => { const matchedLineId = currentCartLines.filter( (line) => prevLine.id === line.id ); if (matchedLineId?.length === 1) { const matchedLine = matchedLineId[0]; if (prevLine.quantity < matchedLine.quantity) { publish2("product_added_to_cart", { ...payload, prevLine, currentLine: matchedLine }); } else if (prevLine.quantity > matchedLine.quantity) { publish2("product_removed_from_cart", { ...payload, prevLine, currentLine: matchedLine }); } } else { publish2("product_removed_from_cart", { ...payload, prevLine }); } }); currentCartLines?.forEach((line) => { const matchedLineId = previousCartLines.filter( (previousLine) => line.id === previousLine.id ); if (!matchedLineId || matchedLineId.length === 0) { publish2("product_added_to_cart", { ...payload, currentLine: line }); } }); }, [cart, prevCart, publish2, shop, customData, canTrack]); return null; } var PERF_KIT_URL = "https://cdn.shopify.com/shopifycloud/perf-kit/shopify-perf-kit-1.0.1.min.js"; function PerfKit({ shop }) { const loadedEvent = react.useRef(false); const { subscribe: subscribe2, register: register2 } = useAnalytics(); const { ready } = register2("Internal_Shopify_Perf_Kit"); const scriptStatus = hydrogenReact.useLoadScript(PERF_KIT_URL, { attributes: { id: "perfkit", "data-application": "hydrogen", "data-shop-id": hydrogenReact.parseGid(shop.shopId).id.toString(), "data-storefront-id": shop.hydrogenSubchannelId, "data-monorail-region": "global", "data-spa-mode": "true", "data-resource-timing-sampling-rate": "100" } }); react.useEffect(() => { if (scriptStatus !== "done" || loadedEvent.current) return; loadedEvent.current = true; subscribe2(AnalyticsEvent.PAGE_VIEWED, () => { window.PerfKit?.navigate(); }); subscribe2(AnalyticsEvent.PRODUCT_VIEWED, () => { window.PerfKit?.setPageType("product"); }); subscribe2(AnalyticsEvent.COLLECTION_VIEWED, () => { window.PerfKit?.setPageType("collection"); }); subscribe2(AnalyticsEvent.SEARCH_VIEWED, () => { window.PerfKit?.setPageType("search"); }); subscribe2(AnalyticsEvent.CART_VIEWED, () => { window.PerfKit?.setPageType("cart"); }); ready(); }, [subscribe2, ready, scriptStatus]); return null; } // src/utils/warning.ts var warnings = /* @__PURE__ */ new Set(); var warnOnce = (string) => { if (!warnings.has(string)) { console.warn(string); warnings.add(string); } }; var errors = /* @__PURE__ */ new Set(); var errorOnce = (string) => { if (!errors.has(string)) { console.error(new Error(string)); errors.add(string); } }; var defaultAnalyticsContext = { canTrack: () => false, cart: null, customData: {}, prevCart: null, publish: () => { }, shop: null, subscribe: () => { }, register: () => ({ ready: () => { } }), customerPrivacy: null, privacyBanner: null }; var AnalyticsContext = react.createContext( defaultAnalyticsContext ); var subscribers = /* @__PURE__ */ new Map(); var registers = {}; function areRegistersReady() { return Object.values(registers).every(Boolean); } function subscribe(event, callback) { if (!subscribers.has(event)) { subscribers.set(event, /* @__PURE__ */ new Map()); } subscribers.get(event)?.set(callback.toString(), callback); } var waitForReadyQueue = /* @__PURE__ */ new Map(); function publish(event, payload) { if (!areRegistersReady()) { waitForReadyQueue.set(event, payload); return; } publishEvent(event, payload); } function publishEvent(event, payload) { (subscribers.get(event) ?? /* @__PURE__ */ new Map()).forEach((callback, subscriber) => { try { callback(payload); } catch (error) { if (typeof error === "object" && error instanceof Error) { console.error( "Analytics publish error", error.message, subscriber, error.stack ); } else { console.error("Analytics publish error", error, subscriber); } } }); } function register(key) { if (!registers.hasOwnProperty(key)) { registers[key] = false; } return { ready: () => { registers[key] = true; if (areRegistersReady() && waitForReadyQueue.size > 0) { waitForReadyQueue.forEach((queuePayload, queueEvent) => { publishEvent(queueEvent, queuePayload); }); waitForReadyQueue.clear(); } } }; } function shopifyCanTrack() { try { return window.Shopify.customerPrivacy.analyticsProcessingAllowed(); } catch (e) { } return false; } function messageOnError(field, envVar) { 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.`; } function AnalyticsProvider({ canTrack: customCanTrack, cart: currentCart, children, consent, customData = {}, shop: shopProp = null, cookieDomain }) { const listenerSet = react.useRef(false); const { shop } = useShopAnalytics(shopProp); const [analyticsLoaded, setAnalyticsLoaded] = react.useState( customCanTrack ? true : false ); const [carts, setCarts] = react.useState({ cart: null, prevCart: null }); const [canTrack, setCanTrack] = react.useState( customCanTrack ? () => customCanTrack : () => shopifyCanTrack ); if (!!shop) { if (/\/68817551382$/.test(shop.shopId)) { warnOnce( "[h2:error:Analytics.Provider] - Mock shop is used. Analytics will not work properly." ); } else { if (!consent.checkoutDomain) { const errorMsg = messageOnError( "consent.checkoutDomain", "PUBLIC_CHECKOUT_DOMAIN" ); errorOnce(errorMsg); } if (!consent.storefrontAccessToken) { const errorMsg = messageOnError( "consent.storefrontAccessToken", "PUBLIC_STOREFRONT_API_TOKEN" ); errorOnce(errorMsg); } if (!consent?.country) { consent.country = "US"; } if (!consent?.language) { consent.language = "EN"; } if (consent.withPrivacyBanner === void 0) { consent.withPrivacyBanner = false; } } } const value = react.useMemo(() => { return { canTrack, ...carts, customData, publish: canTrack() ? publish : () => { }, shop, subscribe, register, customerPrivacy: getCustomerPrivacy(), privacyBanner: getPrivacyBanner() }; }, [ analyticsLoaded, canTrack, carts, carts.cart?.updatedAt, carts.prevCart, publish, subscribe, customData, shop, register, JSON.stringify(registers), getCustomerPrivacy, getPrivacyBanner ]); return /* @__PURE__ */ jsxRuntime.jsxs(AnalyticsContext.Provider, { value, children: [ children, !!shop && /* @__PURE__ */ jsxRuntime.jsx(AnalyticsPageView, {}), !!shop && !!currentCart && /* @__PURE__ */ jsxRuntime.jsx(CartAnalytics, { cart: currentCart, setCarts }), !!shop && consent.checkoutDomain && /* @__PURE__ */ jsxRuntime.jsx( ShopifyAnalytics, { consent, onReady: () => { listenerSet.current = true; setAnalyticsLoaded(true); setCanTrack( customCanTrack ? () => customCanTrack : () => shopifyCanTrack ); }, domain: cookieDomain } ), !!shop && /* @__PURE__ */ jsxRuntime.jsx(PerfKit, { shop }) ] }); } function useAnalytics() { const analyticsContext = react.useContext(AnalyticsContext); if (!analyticsContext) { throw new Error( `[h2:error:useAnalytics] 'useAnalytics()' must be a descendent of <AnalyticsProvider/>` ); } return analyticsContext; } function useShopAnalytics(shopProp) { const [shop, setShop] = react.useState(null); react.useEffect(() => { Promise.resolve(shopProp).then(setShop); return () => { }; }, [setShop, shopProp]); return { shop }; } async function getShopAnalytics({ storefront, publicStorefrontId = "0" }) { return storefront.query(SHOP_QUERY, { cache: storefront.CacheLong() }).then(({ shop, localization }) => { return { shopId: shop.id, acceptedLanguage: localization.language.isoCode, currency: localization.country.currency.isoCode, hydrogenSubchannelId: publicStorefrontId }; }); } var SHOP_QUERY = `#graphql query ShopData( $country: CountryCode $language: LanguageCode ) @inContext(country: $country, language: $language) { shop { id } localization { country { currency { isoCode } } language { isoCode } } } `; var Analytics = { CartView: AnalyticsCartView, CollectionView: AnalyticsCollectionView, CustomView: AnalyticsCustomView, ProductView: AnalyticsProductView, Provider: AnalyticsProvider, SearchView: AnalyticsSearchView }; // src/utils/request.ts function getHeader(request, key) { return getHeaderValue(request.headers, key); } function getHeaderValue(headers, key) { const value = headers?.get?.(key) ?? headers?.[key]; return typeof value === "string" ? value : null; } function getDebugHeaders(request) { return { requestId: request ? getHeader(request, "request-id") : void 0, purpose: request ? getHeader(request, "purpose") : void 0 }; } // src/utils/callsites.ts function withSyncStack(promise, options = {}) { const syncError = new Error(); const getSyncStack = (message, name = "Error") => { const syncStack = (syncError.stack ?? "").split("\n").slice(3 + (options.stackOffset ?? 0)).join("\n").replace(/ at loader(\d+) \(/, (all, m1) => all.replace(m1, "")); return `${name}: ${message} ` + syncStack; }; return promise.then((result) => { if (result?.errors && Array.isArray(result.errors)) { const logErrors = typeof options.logErrors === "function" ? options.logErrors : () => options.logErrors ?? false; result.errors.forEach((error) => { if (error) { error.stack = getSyncStack(error.message, error.name); if (logErrors(error)) console.error(error); } }); } return result; }).catch((error) => { if (error) error.stack = getSyncStack(error.message, error.name); throw error; }); } var getCallerStackLine = (stackOffset = 0) => { let stackInfo = void 0; const original = Error.prepareStackTrace; Error.prepareStackTrace = (_, callsites) => { const cs = callsites[2 + stackOffset]; stackInfo = cs && { file: cs.getFileName() ?? void 0, func: cs.getFunctionName() ?? void 0, line: cs.getLineNumber() ?? void 0, column: cs.getColumnNumber() ?? void 0 }; return ""; }; const err = { stack: "" }; Error.captureStackTrace(err); err.stack; Error.prepareStackTrace = original; return stackInfo; } ; // src/cache/strategies.ts var PUBLIC = "public"; var PRIVATE = "private"; var NO_STORE = "no-store"; var optionMapping = { maxAge: "max-age", staleWhileRevalidate: "stale-while-revalidate", sMaxAge: "s-maxage", staleIfError: "stale-if-error" }; function generateCacheControlHeader(cacheOptions) { const cacheControl = []; Object.keys(cacheOptions).forEach((key) => { if (key === "mode") { cacheControl.push(cacheOptions[key]); } else if (optionMapping[key]) { cacheControl.push( `${optionMapping[key]}=${cacheOptions[key]}` ); } }); return cacheControl.join(", "); } function CacheNone() { return { mode: NO_STORE }; } function guardExpirableModeType(overrideOptions) { if (overrideOptions?.mode && overrideOptions?.mode !== PUBLIC && overrideOptions?.mode !== PRIVATE) { throw Error("'mode' must be either 'public' or 'private'"); } } function CacheShort(overrideOptions) { guardExpirableModeType(overrideOptions); return { mode: PUBLIC, maxAge: 1, staleWhileRevalidate: 9, ...overrideOptions }; } function CacheLong(overrideOptions) { guardExpirableModeType(overrideOptions); return { mode: PUBLIC, maxAge: 3600, // 1 hour staleWhileRevalidate: 82800, // 23 Hours ...overrideOptions }; } function CacheDefault(overrideOptions) { guardExpirableModeType(overrideOptions); return { mode: PUBLIC, maxAge: 1, staleWhileRevalidate: 86399, // 1 second less than 24 hours ...overrideOptions }; } function CacheCustom(overrideOptions) { return overrideOptions; } // src/utils/parse-json.ts function parseJSON(json) { if (String(json).includes("__proto__")) return JSON.parse(json, noproto); return JSON.parse(json); } function noproto(k, v) { if (k !== "__proto__") return v; } function getCacheControlSetting(userCacheOptions, options) { if (userCacheOptions && options) { return { ...userCacheOptions, ...options }; } else { return userCacheOptions || CacheDefault(); } } function generateDefaultCacheControlHeader(userCacheOptions) { return generateCacheControlHeader(getCacheControlSetting(userCacheOptions)); } async function getItem(cache, request) { if (!cache) return; const response = await cache.match(request); if (!response) { return; } return response; } async function setItem(cache, request, response, userCacheOptions) { if (!cache) return; const cacheControl = getCacheControlSetting(userCacheOptions); const paddedCacheControlString = generateDefaultCacheControlHeader( getCacheControlSetting(cacheControl, { maxAge: (cacheControl.maxAge || 0) + (cacheControl.staleWhileRevalidate || 0) }) ); const cacheControlString = generateDefaultCacheControlHeader( getCacheControlSetting(cacheControl) ); response.headers.set("cache-control", paddedCacheControlString); response.headers.set("real-cache-control", cacheControlString); response.headers.set("cache-put-date", String(Date.now())); await cache.put(request, response); } async function deleteItem(cache, request) { if (!cache) return; await cache.delete(request); } function calculateAge(response, responseDate) { const cacheControl = response.headers.get("real-cache-control"); let responseMaxAge = 0; if (cacheControl) { const maxAgeMatch = cacheControl.match(/max-age=(\d*)/); if (maxAgeMatch && maxAgeMatch.length > 1) { responseMaxAge = parseFloat(maxAgeMatch[1]); } } const ageInMs = Date.now() - Number(responseDate); return [ageInMs / 1e3, responseMaxAge]; } function isStale(request, response) { const responseDate = response.headers.get("cache-put-date"); if (!responseDate) { return false; } const [age, responseMaxAge] = calculateAge(response, responseDate); const result = age > responseMaxAge; return result; } var CacheAPI = { get: getItem, set: setItem, delete: deleteItem, generateDefaultCacheControlHeader, isStale }; // src/cache/sub-request.ts function getKeyUrl(key) { return `https://shopify.dev/?${key}`; } function getCacheOption(userCacheOptions) { return userCacheOptions || CacheDefault(); } async function getItemFromCache(cache, key) { if (!cache) return; const url = getKeyUrl(key); const request = new Request(url); const response = await CacheAPI.get(cache, request); if (!response) { return; } const text = await response.text(); try { return [parseJSON(text), response]; } catch { return [text, response]; } } async function setItemInCache(cache, key, value, userCacheOptions) { if (!cache) return; const url = getKeyUrl(key); const request = new Request(url); const response = new Response(JSON.stringify(value)); await CacheAPI.set( cache, request, response, getCacheOption(userCacheOptions) ); } function isStale2(key, response) { return CacheAPI.isStale(new Request(getKeyUrl(key)), response); } // src/utils/hash.ts function hashKey(queryKey) { const rawKeys = Array.isArray(queryKey) ? queryKey : [queryKey]; let hash = ""; for (const key of rawKeys) { if (key != null) { if (typeof key === "object") { hash += JSON.stringify(key); } else { hash += key.toString(); } } } return encodeURIComponent(hash); } // src/cache/run-with-cache.ts var swrLock = /* @__PURE__ */ new Set(); async function runWithCache(cacheKey, actionFn, { strategy = CacheShort(), cacheInstance, shouldCacheResult = () => true, waitUntil, debugInfo }) { const startTime = Date.now(); const key = hashKey([ // '__HYDROGEN_CACHE_ID__', // TODO purgeQueryCacheOnBuild ...typeof cacheKey === "string" ? [cacheKey] : cacheKey ]); let cachedDebugInfo; let userDebugInfo; const addDebugData = (info) => { userDebugInfo = { displayName: info.displayName, url: info.response?.url, responseInit: { status: info.response?.status || 0, statusText: info.response?.statusText || "", headers: Array.from(info.response?.headers.entries() || []) } }; }; const mergeDebugInfo = () => ({ ...cachedDebugInfo, ...debugInfo, url: userDebugInfo?.url || debugInfo?.url || cachedDebugInfo?.url || getKeyUrl(key), displayName: debugInfo?.displayName || userDebugInfo?.displayName || cachedDebugInfo?.displayName }); const logSubRequestEvent2 = ({ result: result2, cacheStatus, overrideStartTime }) => { globalThis.__H2O_LOG_EVENT?.({ ...mergeDebugInfo(), eventType: "subrequest", startTime: overrideStartTime || startTime, endTime: Date.now(), cacheStatus, responsePayload: result2 && result2[0] || result2, responseInit: result2 && result2[1] || userDebugInfo?.responseInit, cache: { status: cacheStatus, strategy: generateCacheControlHeader(strategy || {}), key }, waitUntil }); } ; if (!cacheInstance || !strategy || strategy.mode === NO_STORE) { const result2 = await actionFn({ addDebugData }); logSubRequestEvent2?.({ result: result2 }); return result2; } const storeInCache = (value) => setItemInCache( cacheInstance, key, { value, debugInfo: mergeDebugInfo() }, strategy ); const cachedItem = await getItemFromCache(cacheInstance, key); if (cachedItem && typeof cachedItem[0] !== "string") { const [{ value: cachedResult, debugInfo: debugInfo2 }, cacheInfo] = cachedItem; cachedDebugInfo = debugInfo2; const cacheStatus = isStale2(key, cacheInfo) ? "STALE" : "HIT"; if (!swrLock.has(key) && cacheStatus === "STALE") { swrLock.add(key); const revalidatingPromise = Promise.resolve().then(async () => { const revalidateStartTime = Date.now(); try { const result2 = await actionFn({ addDebugData }); if (shouldCacheResult(result2)) { await storeInCache(result2); logSubRequestEvent2?.({ result: result2, cacheStatus: "PUT", overrideStartTime: revalidateStartTime }); } } catch (error) { if (error.message) { error.message = "SWR in sub-request failed: " + error.message; } console.error(error); } finally { swrLock.delete(key); } }); waitUntil?.(revalidatingPromise); } logSubRequestEvent2?.({ result: cachedResult, cacheStatus }); return cachedResult; } const result = await actionFn({ addDebugData }); logSubRequestEvent2?.({ result, cacheStatus: "MISS" }); if (shouldCacheResult(result)) { const cacheStoringPromise = Promise.resolve().then(async () => { const putStartTime = Date.now(); await storeInCache(result); logSubRequestEvent2?.({ result, cacheStatus: "PUT", overrideStartTime: putStartTime }); }); waitUntil?.(cacheStoringPromise); } return result; } // src/cache/server-fetch.ts function toSerializableResponse(body, response) { return [ body, { status: response.status, statusText: response.statusText, headers: Array.from(response.headers.entries()) } ]; } function fromSerializableResponse([body, init]) { return [body, new Response(body, init)]; } async function fetchWithServerCache(url, requestInit, { cacheInstance, cache: cacheOptions, cacheKey = [url, requestInit], shouldCacheResponse, waitUntil, debugInfo }) { if (!cacheOptions && (!requestInit.method || requestInit.method === "GET")) { cacheOptions = CacheShort(); } return runWithCache( cacheKey, async () => { const response = await fetch(url, requestInit); if (!response.ok) { return response; } let data = await response.text().catch(() => ""); try { if (data) data = parseJSON(data); } catch { } return toSerializableResponse(data, response); }, { cacheInstance, waitUntil, strategy: cacheOptions ?? null, debugInfo, shouldCacheResult: (payload) => { return "ok" in payload ? false : shouldCacheResponse(...fromSerializableResponse(payload)); } } ).then((payload) => { return "ok" in payload ? [null, payload] : fromSerializableResponse(payload); }); } // src/cache/create-with-cache.ts function createWithCache(cacheOptions) { const { cache, waitUntil, request } = cacheOptions; return { run: ({ cacheKey, cacheStrategy, shouldCacheResult }, fn) => { return runWithCache(cacheKey, fn, { shouldCacheResult, strategy: cacheStrategy, cacheInstance: cache, waitUntil, debugInfo: { ...getDebugHeaders(request), stackInfo: getCallerStackLine?.() } }); }, fetch: (url, requestInit, options) => { return fetchWithServerCache(url, requestInit ?? {}, { waitUntil, cacheKey: [url, requestInit], cacheInstance: cache, debugInfo: { url, ...getDebugHeaders(request), stackInfo: getCallerStackLine?.(), displayName: options?.displayName }, cache: options.cacheStrategy, ...options }).then(([data, response]) => ({ data, response })); } }; } // src/cache/in-memory.ts var InMemoryCache = class { #store; constructor() { this.#store = /* @__PURE__ */ new Map(); } add(request) { throw new Error("Method not implemented. Use `put` instead."); } addAll(requests) { throw new Error("Method not implemented. Use `put` instead."); } matchAll(request, options) { throw new Error("Method not implemented. Use `match` instead."); } async put(request, response) { if (request.method !== "GET") { throw new TypeError("Cannot cache response to non-GET request."); } if (response.status === 206) { throw new TypeError( "Cannot cache response to a range request (206 Partial Content)." ); } if (response.headers.get("vary")?.includes("*")) { throw new TypeError("Cannot cache response with 'Vary: *' header."); } this.#store.set(request.url, { body: new Uint8Array(await response.arrayBuffer()), status: response.status, headers: [...response.headers], timestamp: Date.now() }); } async match(request) { if (request.method !== "GET") return; const match = this.#store.get(request.url); if (!match) { return; } const { body, timestamp, ...metadata } = match; const headers = new Headers(metadata.headers); const cacheControl = headers.get("cache-control") || headers.get("real-cache-control") || ""; const maxAge = parseInt( cacheControl.match(/max-age=(\d+)/)?.[1] || "0", 10 ); const swr = parseInt( cacheControl.match(/stale-while-revalidate=(\d+)/)?.[1] || "0", 10 ); const age = (Date.now() - timestamp) / 1e3; const isMiss = age > maxAge + swr; if (isMiss) { this.#store.delete(request.url); return; } const isStale3 = age > maxAge; headers.set("cache", isStale3 ? "STALE" : "HIT"); headers.set("date", new Date(timestamp).toUTCString()); return new Response(body, { status: metadata.status ?? 200, headers }); } async delete(request) { if (this.#store.has(request.url)) { this.#store.delete(request.url); return true; } return false; } keys(request) { const cacheKeys = []; for (const url of this.#store.keys()) { if (!request || request.url === url) { cacheKeys.push(new Request(url)); } } return Promise.resolve(cacheKeys); } }; var INPUT_NAME = "cartFormInput"; function CartForm({ children, action, inputs, route, fetcherKey }) { const fetcher = react$1.useFetcher({ key: fetcherKey }); return /* @__PURE__ */ jsxRuntime.jsxs(fetcher.Form, { action: route || "", method: "post", children: [ (action || inputs) && /* @__PURE__ */ jsxRuntime.jsx( "input", { type: "