@shopify/hydrogen 2026.10.0-preview.2 → 2026.10.0-preview.4
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 +34 -3
- package/dist/cli/index.mjs +578 -85
- package/dist/client/client.mjs +2 -2
- package/dist/client/errors.d.mts +19 -19
- package/dist/client/errors.mjs +19 -16
- package/dist/client/errors.mjs.map +1 -1
- package/dist/client/types.d.mts +91 -10
- package/dist/core/analytics/cart-tracker.d.mts +14 -0
- package/dist/core/analytics/cart-tracker.mjs +15 -1
- package/dist/core/analytics/cart-tracker.mjs.map +1 -1
- package/dist/core/analytics/custom-consent.mjs +12 -0
- package/dist/core/analytics/custom-consent.mjs.map +1 -0
- package/dist/core/analytics/events.d.mts +7 -0
- package/dist/core/analytics/events.mjs +7 -0
- package/dist/core/analytics/events.mjs.map +1 -1
- package/dist/core/analytics/index.d.mts +1 -1
- package/dist/core/analytics/types.d.mts +152 -16
- package/dist/core/cart/actions.d.mts +47 -3
- package/dist/core/cart/actions.mjs +29 -0
- package/dist/core/cart/actions.mjs.map +1 -1
- package/dist/core/cart/attach-quantity-input.d.mts +26 -0
- package/dist/core/cart/attach-quantity-input.mjs +26 -0
- package/dist/core/cart/attach-quantity-input.mjs.map +1 -1
- package/dist/core/cart/cart.d.mts +106 -6
- package/dist/core/cart/cart.mjs +57 -6
- package/dist/core/cart/cart.mjs.map +1 -1
- package/dist/core/cart/cookie.d.mts +15 -0
- package/dist/core/cart/cookie.mjs +15 -0
- package/dist/core/cart/cookie.mjs.map +1 -1
- package/dist/core/cart/form.d.mts +61 -0
- package/dist/core/cart/form.mjs +18 -0
- package/dist/core/cart/form.mjs.map +1 -1
- package/dist/core/cart/get-cart.d.mts +43 -2
- package/dist/core/cart/get-cart.mjs +33 -0
- package/dist/core/cart/get-cart.mjs.map +1 -1
- package/dist/core/cart/index.d.mts +1 -1
- package/dist/core/cart/quantity.d.mts +17 -0
- package/dist/core/cart/quantity.mjs +17 -0
- package/dist/core/cart/quantity.mjs.map +1 -1
- package/dist/core/cart/queries.d.mts +402 -369
- package/dist/core/cart/queries.mjs +2 -1
- package/dist/core/cart/queries.mjs.map +1 -1
- package/dist/core/cart/server-handlers.d.mts +69 -0
- package/dist/core/cart/server-handlers.mjs +2 -10
- package/dist/core/cart/server-handlers.mjs.map +1 -1
- package/dist/core/cart/state.d.mts +133 -0
- package/dist/core/cart/state.mjs +8 -0
- package/dist/core/cart/state.mjs.map +1 -1
- package/dist/core/collection/collection.d.mts +18 -2
- package/dist/core/collection/collection.mjs +11 -0
- package/dist/core/collection/collection.mjs.map +1 -1
- package/dist/core/collection/index.d.mts +1 -1
- package/dist/core/collection/reconciler.d.mts +13 -4
- package/dist/core/collection/reconciler.mjs.map +1 -1
- package/dist/core/collection/state.d.mts +7 -2
- package/dist/core/collection/state.mjs.map +1 -1
- package/dist/core/collection/url.d.mts +3 -0
- package/dist/core/collection/url.mjs.map +1 -1
- package/dist/core/constants.d.mts +1 -1
- package/dist/core/constants.mjs +2 -2
- package/dist/core/constants.mjs.map +1 -1
- package/dist/core/headers.mjs +2 -2
- package/dist/core/headers.mjs.map +1 -1
- package/dist/core/index.d.mts +5 -5
- package/dist/core/logging/logging.mjs.map +1 -1
- package/dist/core/money/format.mjs +6 -3
- package/dist/core/money/format.mjs.map +1 -1
- package/dist/core/observable.mjs +0 -3
- package/dist/core/observable.mjs.map +1 -1
- package/dist/core/predictive-search/form.d.mts +1 -0
- package/dist/core/predictive-search/form.mjs +1 -0
- package/dist/core/predictive-search/form.mjs.map +1 -1
- package/dist/core/predictive-search/index.d.mts +1 -1
- package/dist/core/predictive-search/queries.d.mts +40 -4
- package/dist/core/predictive-search/queries.mjs +7 -1
- package/dist/core/predictive-search/queries.mjs.map +1 -1
- package/dist/core/predictive-search/search.d.mts +43 -10
- package/dist/core/predictive-search/search.mjs +34 -0
- package/dist/core/predictive-search/search.mjs.map +1 -1
- package/dist/core/predictive-search/server-handlers.d.mts +31 -5
- package/dist/core/predictive-search/server-handlers.mjs +5 -3
- package/dist/core/predictive-search/server-handlers.mjs.map +1 -1
- package/dist/core/predictive-search/store.d.mts +3 -1
- package/dist/core/predictive-search/store.mjs.map +1 -1
- package/dist/core/predictive-search/url.d.mts +50 -8
- package/dist/core/predictive-search/url.mjs +9 -0
- package/dist/core/predictive-search/url.mjs.map +1 -1
- package/dist/core/product/form.d.mts +21 -2
- package/dist/core/product/form.mjs +13 -0
- package/dist/core/product/form.mjs.map +1 -1
- package/dist/core/product/index.d.mts +1 -1
- package/dist/core/product/options.d.mts +2 -2
- package/dist/core/product/options.mjs +2 -2
- package/dist/core/product/options.mjs.map +1 -1
- package/dist/core/product/product-form.d.mts +33 -1
- package/dist/core/product/product-form.mjs +30 -0
- package/dist/core/product/product-form.mjs.map +1 -1
- package/dist/core/product/state.d.mts +41 -0
- package/dist/core/product/url.d.mts +6 -0
- package/dist/core/product/url.mjs.map +1 -1
- package/dist/core/request-context.d.mts +8 -5
- package/dist/core/request-context.mjs +17 -39
- package/dist/core/request-context.mjs.map +1 -1
- package/dist/core/request-routing/handle-shopify-routes.mjs +4 -1
- package/dist/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/ajax-api.mjs +1 -1
- package/dist/core/request-routing/interceptors/api-proxy.mjs +1 -1
- package/dist/core/request-routing/interceptors/checkout.mjs +22 -6
- package/dist/core/request-routing/interceptors/checkout.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/mcp-proxy.mjs +1 -1
- package/dist/core/request-routing/interceptors/proxy.mjs +36 -10
- package/dist/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/query-param-redirect.mjs +5 -11
- package/dist/core/request-routing/interceptors/query-param-redirect.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/sfapi-proxy.mjs +19 -1
- package/dist/core/request-routing/interceptors/sfapi-proxy.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/ucp-mcp-proxy.mjs +35 -0
- package/dist/core/request-routing/interceptors/ucp-mcp-proxy.mjs.map +1 -0
- package/dist/core/request-routing/interceptors/well-known.mjs +1 -1
- package/dist/core/shopify-scripts/analytics.mjs +2 -2
- package/dist/core/shopify-scripts/analytics.mjs.map +1 -1
- package/dist/core/shopify-scripts/constants.mjs +4 -2
- package/dist/core/shopify-scripts/constants.mjs.map +1 -1
- package/dist/core/shopify-scripts/global.mjs +9 -4
- package/dist/core/shopify-scripts/global.mjs.map +1 -1
- package/dist/core/shopify-scripts/index.d.mts +1 -0
- package/dist/core/shopify-scripts/index.mjs +14 -12
- package/dist/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/core/shopify-scripts/initialize.d.mts +1 -0
- package/dist/core/shopify-scripts/initialize.mjs +10 -3
- package/dist/core/shopify-scripts/initialize.mjs.map +1 -1
- package/dist/core/shopify-scripts/types.d.mts +6 -0
- package/dist/core/url.mjs +21 -2
- package/dist/core/url.mjs.map +1 -1
- package/dist/customer-account/client.d.mts +128 -4
- package/dist/customer-account/client.mjs +27 -1
- package/dist/customer-account/client.mjs.map +1 -1
- package/dist/customer-account/errors.d.mts +46 -0
- package/dist/customer-account/errors.mjs +46 -0
- package/dist/customer-account/errors.mjs.map +1 -1
- package/dist/customer-account/graphql.d.mts +68 -0
- package/dist/customer-account/graphql.mjs +47 -0
- package/dist/customer-account/graphql.mjs.map +1 -1
- package/dist/customer-account/session.d.mts +114 -23
- package/dist/customer-account/session.mjs +27 -12
- package/dist/customer-account/session.mjs.map +1 -1
- package/dist/customer-account/type-resolver.d.mts +23 -0
- package/dist/customer-account-api-types.d.ts +570 -374
- package/dist/customer-account.schema.json +2981 -1527
- package/dist/development/client/client.mjs +2 -2
- package/dist/development/client/errors.d.mts +19 -19
- package/dist/development/client/errors.mjs +20 -20
- package/dist/development/client/errors.mjs.map +1 -1
- package/dist/development/client/types.d.mts +91 -10
- package/dist/development/core/analytics/cart-tracker.d.mts +14 -0
- package/dist/development/core/analytics/cart-tracker.mjs +15 -1
- package/dist/development/core/analytics/cart-tracker.mjs.map +1 -1
- package/dist/development/core/analytics/custom-consent.mjs +12 -0
- package/dist/development/core/analytics/custom-consent.mjs.map +1 -0
- package/dist/development/core/analytics/events.d.mts +7 -0
- package/dist/development/core/analytics/events.mjs +7 -0
- package/dist/development/core/analytics/events.mjs.map +1 -1
- package/dist/development/core/analytics/index.d.mts +1 -1
- package/dist/development/core/analytics/types.d.mts +152 -16
- package/dist/development/core/cart/actions.d.mts +47 -3
- package/dist/development/core/cart/actions.mjs +29 -0
- package/dist/development/core/cart/actions.mjs.map +1 -1
- package/dist/development/core/cart/attach-quantity-input.d.mts +26 -0
- package/dist/development/core/cart/attach-quantity-input.mjs +26 -0
- package/dist/development/core/cart/attach-quantity-input.mjs.map +1 -1
- package/dist/development/core/cart/cart.d.mts +106 -6
- package/dist/development/core/cart/cart.mjs +57 -6
- package/dist/development/core/cart/cart.mjs.map +1 -1
- package/dist/development/core/cart/cookie.d.mts +15 -0
- package/dist/development/core/cart/cookie.mjs +15 -0
- package/dist/development/core/cart/cookie.mjs.map +1 -1
- package/dist/development/core/cart/form.d.mts +61 -0
- package/dist/development/core/cart/form.mjs +18 -0
- package/dist/development/core/cart/form.mjs.map +1 -1
- package/dist/development/core/cart/get-cart.d.mts +43 -2
- package/dist/development/core/cart/get-cart.mjs +33 -0
- package/dist/development/core/cart/get-cart.mjs.map +1 -1
- package/dist/development/core/cart/index.d.mts +1 -1
- package/dist/development/core/cart/quantity.d.mts +17 -0
- package/dist/development/core/cart/quantity.mjs +17 -0
- package/dist/development/core/cart/quantity.mjs.map +1 -1
- package/dist/development/core/cart/queries.d.mts +345 -312
- package/dist/development/core/cart/queries.mjs +2 -1
- package/dist/development/core/cart/queries.mjs.map +1 -1
- package/dist/development/core/cart/server-handlers.d.mts +69 -0
- package/dist/development/core/cart/server-handlers.mjs +2 -10
- package/dist/development/core/cart/server-handlers.mjs.map +1 -1
- package/dist/development/core/cart/state.d.mts +133 -0
- package/dist/development/core/cart/state.mjs +8 -0
- package/dist/development/core/cart/state.mjs.map +1 -1
- package/dist/development/core/collection/collection.d.mts +18 -2
- package/dist/development/core/collection/collection.mjs +11 -0
- package/dist/development/core/collection/collection.mjs.map +1 -1
- package/dist/development/core/collection/index.d.mts +1 -1
- package/dist/development/core/collection/reconciler.d.mts +13 -4
- package/dist/development/core/collection/reconciler.mjs.map +1 -1
- package/dist/development/core/collection/state.d.mts +7 -2
- package/dist/development/core/collection/state.mjs.map +1 -1
- package/dist/development/core/collection/url.d.mts +3 -0
- package/dist/development/core/collection/url.mjs.map +1 -1
- package/dist/development/core/constants.mjs +1 -1
- package/dist/development/core/constants.mjs.map +1 -1
- package/dist/development/core/development.d.mts +5 -5
- package/dist/development/core/headers.mjs +2 -2
- package/dist/development/core/headers.mjs.map +1 -1
- package/dist/development/core/index.d.mts +4 -4
- package/dist/development/core/logging/logging.mjs.map +1 -1
- package/dist/development/core/money/format.mjs +6 -3
- package/dist/development/core/money/format.mjs.map +1 -1
- package/dist/development/core/observable.mjs +0 -3
- package/dist/development/core/observable.mjs.map +1 -1
- package/dist/development/core/predictive-search/form.d.mts +1 -0
- package/dist/development/core/predictive-search/form.mjs +1 -0
- package/dist/development/core/predictive-search/form.mjs.map +1 -1
- package/dist/development/core/predictive-search/index.d.mts +1 -1
- package/dist/development/core/predictive-search/queries.d.mts +38 -2
- package/dist/development/core/predictive-search/queries.mjs +7 -1
- package/dist/development/core/predictive-search/queries.mjs.map +1 -1
- package/dist/development/core/predictive-search/search.d.mts +43 -10
- package/dist/development/core/predictive-search/search.mjs +34 -0
- package/dist/development/core/predictive-search/search.mjs.map +1 -1
- package/dist/development/core/predictive-search/server-handlers.d.mts +31 -5
- package/dist/development/core/predictive-search/server-handlers.mjs +5 -3
- package/dist/development/core/predictive-search/server-handlers.mjs.map +1 -1
- package/dist/development/core/predictive-search/store.d.mts +3 -1
- package/dist/development/core/predictive-search/store.mjs.map +1 -1
- package/dist/development/core/predictive-search/url.d.mts +50 -8
- package/dist/development/core/predictive-search/url.mjs +9 -0
- package/dist/development/core/predictive-search/url.mjs.map +1 -1
- package/dist/development/core/product/form.d.mts +21 -2
- package/dist/development/core/product/form.mjs +13 -0
- package/dist/development/core/product/form.mjs.map +1 -1
- package/dist/development/core/product/index.d.mts +1 -1
- package/dist/development/core/product/options.d.mts +2 -2
- package/dist/development/core/product/options.mjs +2 -2
- package/dist/development/core/product/options.mjs.map +1 -1
- package/dist/development/core/product/product-form.d.mts +33 -1
- package/dist/development/core/product/product-form.mjs +30 -0
- package/dist/development/core/product/product-form.mjs.map +1 -1
- package/dist/development/core/product/state.d.mts +41 -0
- package/dist/development/core/product/url.d.mts +6 -0
- package/dist/development/core/product/url.mjs.map +1 -1
- package/dist/development/core/request-context.d.mts +8 -5
- package/dist/development/core/request-context.mjs +17 -39
- package/dist/development/core/request-context.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-routes.mjs +4 -1
- package/dist/development/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/ajax-api.mjs +1 -1
- package/dist/development/core/request-routing/interceptors/api-proxy.mjs +1 -1
- package/dist/development/core/request-routing/interceptors/checkout.mjs +22 -6
- package/dist/development/core/request-routing/interceptors/checkout.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/mcp-proxy.mjs +1 -1
- package/dist/development/core/request-routing/interceptors/proxy.mjs +36 -10
- package/dist/development/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/query-param-redirect.mjs +5 -11
- package/dist/development/core/request-routing/interceptors/query-param-redirect.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/sfapi-proxy.mjs +19 -1
- package/dist/development/core/request-routing/interceptors/sfapi-proxy.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/ucp-mcp-proxy.mjs +35 -0
- package/dist/development/core/request-routing/interceptors/ucp-mcp-proxy.mjs.map +1 -0
- package/dist/development/core/request-routing/interceptors/well-known.mjs +1 -1
- package/dist/development/core/shopify-scripts/analytics.mjs +2 -2
- package/dist/development/core/shopify-scripts/analytics.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/constants.mjs +4 -2
- package/dist/development/core/shopify-scripts/constants.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/global.mjs +9 -4
- package/dist/development/core/shopify-scripts/global.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/index.d.mts +1 -0
- package/dist/development/core/shopify-scripts/index.mjs +14 -12
- package/dist/development/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/initialize.d.mts +1 -0
- package/dist/development/core/shopify-scripts/initialize.mjs +10 -3
- package/dist/development/core/shopify-scripts/initialize.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/types.d.mts +6 -0
- package/dist/development/core/url.mjs +21 -2
- package/dist/development/core/url.mjs.map +1 -1
- package/dist/development/customer-account/session.d.mts +69 -13
- package/dist/development/customer-account/session.mjs +4 -0
- package/dist/development/customer-account/session.mjs.map +1 -1
- package/dist/development/globals.d.mts +6 -5
- package/dist/development/graphql/generated/graphql-env.d.mts +459 -16
- package/dist/development/graphql/generated/storefront-api-types.d.mts +1 -1
- package/dist/development/graphql/graphql.d.mts +33 -0
- package/dist/development/graphql/graphql.mjs +30 -0
- package/dist/development/graphql/graphql.mjs.map +1 -1
- package/dist/development/graphql/type-resolver.d.mts +14 -0
- package/dist/development/react/cart.d.mts +85 -14
- package/dist/development/react/cart.mjs +123 -3
- package/dist/development/react/cart.mjs.map +1 -1
- package/dist/development/react/collection.d.mts +2 -4
- package/dist/development/react/collection.mjs.map +1 -1
- package/dist/development/react/index.d.mts +8 -6
- package/dist/development/react/index.mjs +2 -2
- package/dist/development/react/predictive-search.d.mts +68 -7
- package/dist/development/react/predictive-search.mjs +54 -12
- package/dist/development/react/predictive-search.mjs.map +1 -1
- package/dist/development/react/product.d.mts +1 -0
- package/dist/development/react/product.mjs +5 -2
- package/dist/development/react/product.mjs.map +1 -1
- package/dist/development/react/shopify-scripts.mjs +1 -0
- package/dist/development/react/shopify-scripts.mjs.map +1 -1
- package/dist/development/vue/cart.d.mts +85 -6
- package/dist/development/vue/cart.mjs +95 -0
- package/dist/development/vue/cart.mjs.map +1 -1
- package/dist/development/vue/collection.d.mts +83 -5
- package/dist/development/vue/collection.mjs +52 -0
- package/dist/development/vue/collection.mjs.map +1 -1
- package/dist/development/vue/index.d.mts +6 -4
- package/dist/development/vue/predictive-search.d.mts +53 -6
- package/dist/development/vue/predictive-search.mjs +33 -0
- package/dist/development/vue/predictive-search.mjs.map +1 -1
- package/dist/development/vue/product.d.mts +6 -2
- package/dist/development/vue/product.mjs.map +1 -1
- package/dist/development/vue/shopify-scripts.d.mts +30 -3
- package/dist/development/vue/shopify-scripts.mjs +6 -0
- package/dist/development/vue/shopify-scripts.mjs.map +1 -1
- package/dist/globals.d.mts +6 -5
- package/dist/graphql/generated/customer-account-graphql-env.d.mts +1717 -1090
- package/dist/graphql/generated/graphql-env.d.mts +459 -16
- package/dist/graphql/generated/storefront-api-types.d.mts +1 -1
- package/dist/graphql/graphql.d.mts +33 -0
- package/dist/graphql/graphql.mjs +30 -0
- package/dist/graphql/graphql.mjs.map +1 -1
- package/dist/graphql/type-resolver.d.mts +14 -0
- package/dist/react/cart.d.mts +85 -14
- package/dist/react/cart.mjs +123 -3
- package/dist/react/cart.mjs.map +1 -1
- package/dist/react/collection.d.mts +2 -4
- package/dist/react/collection.mjs.map +1 -1
- package/dist/react/index.d.mts +8 -6
- package/dist/react/index.mjs +2 -2
- package/dist/react/predictive-search.d.mts +68 -7
- package/dist/react/predictive-search.mjs +54 -12
- package/dist/react/predictive-search.mjs.map +1 -1
- package/dist/react/product.d.mts +1 -0
- package/dist/react/product.mjs +5 -2
- package/dist/react/product.mjs.map +1 -1
- package/dist/react/shopify-scripts.mjs +1 -0
- package/dist/react/shopify-scripts.mjs.map +1 -1
- package/dist/storefront-api-types.d.ts +165 -20
- package/dist/storefront.schema.json +782 -57
- package/dist/vite/customer-account.mjs +191 -0
- package/dist/vite/customer-account.mjs.map +1 -0
- package/dist/vite/local-https.mjs +13 -29
- package/dist/vite/local-https.mjs.map +1 -1
- package/dist/vite/mkcert.mjs +1 -1
- package/dist/vue/cart.d.mts +85 -6
- package/dist/vue/cart.mjs +95 -0
- package/dist/vue/cart.mjs.map +1 -1
- package/dist/vue/collection.d.mts +83 -5
- package/dist/vue/collection.mjs +52 -0
- package/dist/vue/collection.mjs.map +1 -1
- package/dist/vue/index.d.mts +6 -4
- package/dist/vue/predictive-search.d.mts +53 -6
- package/dist/vue/predictive-search.mjs +33 -0
- package/dist/vue/predictive-search.mjs.map +1 -1
- package/dist/vue/product.d.mts +6 -2
- package/dist/vue/product.mjs.map +1 -1
- package/dist/vue/shopify-scripts.d.mts +31 -4
- package/dist/vue/shopify-scripts.mjs +6 -0
- package/dist/vue/shopify-scripts.mjs.map +1 -1
- package/package.json +5 -2
- package/skills/hydrogen-analytics/SKILL.md +6 -5
- package/skills/hydrogen-analytics/references/custom-consent.md +52 -0
- package/skills/hydrogen-analytics/references/react.md +1 -1
- package/skills/hydrogen-cart-metafields/SKILL.md +133 -0
- package/skills/hydrogen-cart-ui/SKILL.md +2 -1
- package/skills/hydrogen-customer-account/SKILL.md +3 -3
- package/skills/hydrogen-local-https/SKILL.md +22 -7
- package/skills/hydrogen-request-handlers/SKILL.md +5 -5
- package/skills/hydrogen-request-handlers/references/nuxt.md +0 -6
- package/skills/hydrogen-setup/references/analytics.md +24 -23
- package/skills/hydrogen-setup/steps/1-inspect-app.md +1 -1
- package/skills/hydrogen-setup/steps/11-verify.md +1 -0
- package/skills/hydrogen-setup/steps/2-scaffold.md +22 -1
- package/skills/hydrogen-smoke-test/SKILL.md +2 -2
- package/skills/hydrogen-storefront-client/SKILL.md +4 -2
- package/skills/hydrogen-variant-form/SKILL.md +10 -4
- package/ts-plugin/package.json +5 -0
- package/dist/core/shopify-scripts/consent.mjs +0 -17
- package/dist/core/shopify-scripts/consent.mjs.map +0 -1
- package/dist/core/shopify-scripts/deprecated-cookies.mjs +0 -97
- package/dist/core/shopify-scripts/deprecated-cookies.mjs.map +0 -1
- package/dist/core/shopify-scripts/utils/cookie-domain.mjs +0 -43
- package/dist/core/shopify-scripts/utils/cookie-domain.mjs.map +0 -1
- package/dist/core/shopify-scripts/utils/tracking-values.mjs +0 -65
- package/dist/core/shopify-scripts/utils/tracking-values.mjs.map +0 -1
- package/dist/core/shopify-scripts/utils/uuid.mjs +0 -17
- package/dist/core/shopify-scripts/utils/uuid.mjs.map +0 -1
- package/dist/development/core/shopify-scripts/consent.mjs +0 -17
- package/dist/development/core/shopify-scripts/consent.mjs.map +0 -1
- package/dist/development/core/shopify-scripts/deprecated-cookies.mjs +0 -97
- package/dist/development/core/shopify-scripts/deprecated-cookies.mjs.map +0 -1
- package/dist/development/core/shopify-scripts/utils/cookie-domain.mjs +0 -43
- package/dist/development/core/shopify-scripts/utils/cookie-domain.mjs.map +0 -1
- package/dist/development/core/shopify-scripts/utils/tracking-values.mjs +0 -65
- package/dist/development/core/shopify-scripts/utils/tracking-values.mjs.map +0 -1
- package/dist/development/core/shopify-scripts/utils/uuid.mjs +0 -17
- package/dist/development/core/shopify-scripts/utils/uuid.mjs.map +0 -1
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
|
|
23
23
|
**Prerequisites:**
|
|
24
24
|
|
|
25
|
-
- A storefront built on `@shopify/hydrogen` with the request interceptors already wired (`handleShopifyRoutes` and `handleShopifyRedirects`). The analytics bus depends on the SFAPI proxy so the browser can observe same-origin Storefront API responses for session cookies. Without the proxy,
|
|
25
|
+
- A storefront built on `@shopify/hydrogen` with the request interceptors already wired (`handleShopifyRoutes` and `handleShopifyRedirects`). The analytics bus depends on the SFAPI proxy so the browser can observe same-origin Storefront API responses for session cookies. Without the proxy, the backend session cookies cannot be established and analytics should be treated as incomplete. If you have not installed the interceptors yet, install them first with the local `hydrogen-request-handlers` skill.
|
|
26
26
|
- Shopify runtime scripts rendered from the root/document head. Use `ShopifyScripts` from your framework binding if it exports one, or `getShopifyScriptTags()` / `renderShopifyScriptTags()` from core in other framework heads. Pass `{country, language, currency?}` as `i18n`; pass `{shopId: env.SHOP_ID, storefrontId: env.PUBLIC_STOREFRONT_ID ?? "0", myshopifyDomain: env.PUBLIC_STORE_DOMAIN}` as `shop`. Resolve both on the server, declare them as consts annotated with the `ShopifyScriptsShop` / `ShopifyScriptsI18n` types from `@shopify/hydrogen` (so wrong or missing fields fail typecheck where they are built), and serialize them into ShopifyScripts. ShopifyScripts creates `window.Shopify.analytics` by default and exposes the permanent domain as `window.Shopify.shop`. Analytics consent config does not accept `country` or `language`.
|
|
27
27
|
- A client-side lifecycle hook in your framework (route-change effect, navigation event, `<script>` tag, etc.) so view events can fire on the right URL transitions.
|
|
28
28
|
|
|
29
|
-
`ShopifyScripts` creates the zero-dependency analytics bus, sets it on `window.Shopify.analytics`, and owns Shopify consent setup
|
|
29
|
+
`ShopifyScripts` creates the zero-dependency analytics bus, sets it on `window.Shopify.analytics`, and owns Shopify consent setup and analytics CDN loading. Framework adapters stay thin: they translate framework lifecycle events into bus calls and wire cart delta tracking with `trackCartAnalytics()`.
|
|
30
30
|
|
|
31
31
|
## What you're installing, and what it does on its own
|
|
32
32
|
|
|
@@ -36,8 +36,7 @@ Your app code
|
|
|
36
36
|
▼
|
|
37
37
|
window.Shopify.analytics
|
|
38
38
|
│
|
|
39
|
-
├── publish /
|
|
40
|
-
├── raw live subscribers
|
|
39
|
+
├── publish / addDestination / getConfig (app-facing)
|
|
41
40
|
└── consent-gated destinations with replay
|
|
42
41
|
│
|
|
43
42
|
└── Customer Privacy gates destination delivery and replay
|
|
@@ -46,8 +45,7 @@ ShopifyScripts
|
|
|
46
45
|
│
|
|
47
46
|
├── loads Shopify Customer Privacy script (consent + region gating)
|
|
48
47
|
├── loads Privacy Banner script in default-banner mode
|
|
49
|
-
|
|
50
|
-
└── writes deprecated _shopify_y / _shopify_s cookies
|
|
48
|
+
└── loads Shopify analytics destination by default
|
|
51
49
|
```
|
|
52
50
|
|
|
53
51
|
The bus is **browser-only effective** and is created by ShopifyScripts in the browser. There is no server-side dispatch.
|
|
@@ -93,7 +91,7 @@ const consent: ConsentConfig = {
|
|
|
93
91
|
};
|
|
94
92
|
|
|
95
93
|
const analytics: NonNullable<ShopifyScriptTagsOptions["analytics"]> = {
|
|
96
|
-
customData: { theme: "v2" }, // optional
|
|
94
|
+
customData: { theme: "v2" }, // optional
|
|
97
95
|
};
|
|
98
96
|
```
|
|
99
97
|
|
|
@@ -115,19 +113,19 @@ Pass the app's resolved `country` and `language` market values. Optional `curren
|
|
|
115
113
|
|
|
116
114
|
### `analytics`
|
|
117
115
|
|
|
118
|
-
The analytics bus is enabled by default. Pass `analytics` only when you need optional bus configuration such as `customData
|
|
116
|
+
The analytics bus is enabled by default. Pass `analytics` only when you need optional bus configuration such as `customData`. Shopify analytics reads currency from `window.Shopify.currency.active`, which is seeded by `i18n.currency` and updated from cart currency when available.
|
|
119
117
|
|
|
120
118
|
### `consent`
|
|
121
119
|
|
|
122
120
|
This is where the location/region nuance lives. Shopify's hosted Customer Privacy API decides per-visitor whether tracking requires consent based on the visitor's geography:
|
|
123
121
|
|
|
124
|
-
- **Visitors in jurisdictions with consent requirements** (EU/EEA/UK GDPR, parts of Canada, California CCPA, etc.) — analytics must wait for consent. Use `mode: "default-banner"` for Shopify's hosted privacy banner, or `mode: "custom-banner"`
|
|
125
|
-
- **Visitors in jurisdictions without consent requirements** — the Customer Privacy SDK
|
|
122
|
+
- **Visitors in jurisdictions with consent requirements** (EU/EEA/UK GDPR, parts of Canada, California CCPA, etc.) — analytics must wait for consent. Use `mode: "default-banner"` for Shopify's hosted privacy banner, or `mode: "custom-banner"` with a `setup` callback that connects the app's consent provider.
|
|
123
|
+
- **Visitors in jurisdictions without consent requirements** — the Customer Privacy SDK may allow tracking without showing Shopify's banner. Custom-banner integrations still wait for the provider's resolved consent; the provider owns its regional policy.
|
|
126
124
|
|
|
127
125
|
`mode` controls how consent is collected:
|
|
128
126
|
|
|
129
127
|
- `"default-banner"` loads Shopify's hosted privacy banner and waits when the Customer Privacy API says banner interaction is required.
|
|
130
|
-
- `"custom-banner"` loads only the Customer Privacy API and
|
|
128
|
+
- `"custom-banner"` loads only the Customer Privacy API and waits for the required asynchronous `setup` callback to resolve before checking consent. Read [Custom consent providers](../../hydrogen-analytics/references/custom-consent.md) when integrating a third-party banner.
|
|
131
129
|
- `"no-banner"` loads only the Customer Privacy API and releases analytics after consent setup. Use this only when consent is already allowed or managed outside this storefront.
|
|
132
130
|
|
|
133
131
|
### Consent Gating
|
|
@@ -138,9 +136,9 @@ Default:
|
|
|
138
136
|
() => window.Shopify?.customerPrivacy?.analyticsProcessingAllowed() ?? false
|
|
139
137
|
```
|
|
140
138
|
|
|
141
|
-
|
|
139
|
+
If the Customer Privacy script is blocked, hasn't loaded, or is unavailable, **destination delivery is blocked**. Destinations receive events only after consent is loaded and analytics processing is allowed.
|
|
142
140
|
|
|
143
|
-
|
|
141
|
+
The bus retains up to 500 events for destination replay, including events published while analytics consent was already allowed. New destinations receive that retained history only when analytics consent allows delivery, and only for supported event names they subscribe to. If the visitor explicitly denies analytics consent, the replay buffer is cleared.
|
|
144
142
|
|
|
145
143
|
Custom event names such as `custom_*` are temporarily unsupported. Publishing or subscribing to an unsupported event name logs a small warning and the event is ignored.
|
|
146
144
|
|
|
@@ -179,7 +177,7 @@ Non-optional constraints:
|
|
|
179
177
|
- **One instance per page.** The singleton means every route, every effect, every script tag shares the same bus. Multiple instances on the same page overwrite `window.Shopify.customerPrivacy.config`; the latest initialized config wins. Multi-store on one page is not supported.
|
|
180
178
|
- **Lazy.** Reading the bus happens on first `getAnalytics()` call. ShopifyScripts owns bus construction.
|
|
181
179
|
|
|
182
|
-
|
|
180
|
+
Register destinations once during browser app initialization, after ShopifyScripts has created the bus. Keep destination registration outside route component mount/unmount cycles; route components publish events through the shared bus:
|
|
183
181
|
|
|
184
182
|
```ts
|
|
185
183
|
const cleanup = analytics.addDestination({
|
|
@@ -205,7 +203,7 @@ const cleanup = analytics.addDestination({
|
|
|
205
203
|
});
|
|
206
204
|
```
|
|
207
205
|
|
|
208
|
-
Destinations are consent-gated and receive
|
|
206
|
+
Destinations are consent-gated and receive retained history once tracking is allowed. The returned cleanup function removes the destination when the app intentionally stops that integration.
|
|
209
207
|
|
|
210
208
|
---
|
|
211
209
|
|
|
@@ -555,7 +553,7 @@ type AnalyticsCart = {
|
|
|
555
553
|
};
|
|
556
554
|
```
|
|
557
555
|
|
|
558
|
-
Manually published `AnalyticsCart` payloads (like `CART_VIEWED`) accept both `lines.nodes` and `lines.edges` (GraphQL connection) shapes at the type level; the bus forwards them unchanged, so subscribers that read lines should flatten them with the exported `flattenConnection()` helper. The cart store consumed by `trackCartAnalytics` is different: it reads `cart.lines.nodes` directly, so the app's cart query must select `lines.nodes`.
|
|
556
|
+
Manually published `AnalyticsCart` payloads (like `CART_VIEWED`) accept both `lines.nodes` and `lines.edges` (GraphQL connection) shapes at the type level; the bus forwards them unchanged, so subscribers that read lines should flatten them with the exported `flattenConnection()` helper. The cart store consumed by `trackCartAnalytics` is different: it reads `cart.lines.nodes` directly, so the app's cart query must select `lines.nodes`.
|
|
559
557
|
|
|
560
558
|
Application code should not manually publish `cart_updated`, `product_added_to_cart`, or `product_removed_from_cart`. Always go through `trackCartAnalytics`.
|
|
561
559
|
|
|
@@ -563,7 +561,7 @@ Application code should not manually publish `cart_updated`, `product_added_to_c
|
|
|
563
561
|
|
|
564
562
|
## Wiring third-party destinations
|
|
565
563
|
|
|
566
|
-
The bus is the right integration point for GA4, Meta Pixel, Klaviyo, etc. Register
|
|
564
|
+
The bus is the right integration point for GA4, Meta Pixel, Klaviyo, etc. Register each integration once with `addDestination()` during browser app initialization, after ShopifyScripts has created the bus. The bus gates destination callbacks with Shopify Customer Privacy and replays retained history when analytics consent allows:
|
|
567
565
|
|
|
568
566
|
```ts
|
|
569
567
|
const analytics = getAnalytics();
|
|
@@ -578,7 +576,11 @@ analytics?.addDestination({
|
|
|
578
576
|
});
|
|
579
577
|
```
|
|
580
578
|
|
|
581
|
-
|
|
579
|
+
Register all event consumers with `addDestination()` and subscribe inside its setup callback. Consent gating happens at the bus level before destination callbacks see the payload.
|
|
580
|
+
|
|
581
|
+
Removing and re-adding a destination, even with the same name, creates a fresh registration and replays retained history again. This can duplicate deliveries on component remounts, including React Strict Mode's development effect replay. Keep destinations registered for the page's lifetime; use the returned cleanup when intentionally removing an integration.
|
|
582
|
+
|
|
583
|
+
Destinations that need Shopify visitor IDs can call `getTrackingValues()` from the callback's second argument: `subscribe(event, (payload, { getTrackingValues }) => { ... })`. It reads current `uniqueToken` and `visitToken` values from the consent API when called, including during replay, and requests fallback generation with the tag `hydrogen:<destination name>`. Unavailable tokens are empty strings, and the getter also returns empty strings whenever analytics tracking is not currently allowed, so a retained getter cannot read or generate tokens after consent is revoked. Call it inside the destination callback; registration itself does not read or generate tokens.
|
|
582
584
|
|
|
583
585
|
---
|
|
584
586
|
|
|
@@ -589,7 +591,7 @@ After wiring, smoke-test each event in the browser dev tools:
|
|
|
589
591
|
1. **Destination log fires** — open the page with the dev console open. You should see `[analytics] page_viewed` (or whichever events you logged) after consent allows tracking. If nothing logs, either `getAnalytics()` is no-op'ing on the server, the bus is unavailable, or `analyticsProcessingAllowed()` is false.
|
|
590
592
|
2. **Monorail request fires** — Network tab, filter for `monorail-edge.shopifysvc.com`. A `produce_batch` POST should land within ~1s of consent being granted (or immediately if the visitor is in a no-consent-required region). If it never fires, either consent has not been granted, the schemas are missing required fields (check console for warnings about missing `id`/`title`/`vendor`/etc.), or `hasUserConsent` is false on the payload.
|
|
591
593
|
3. **Per-route navigation fires page_viewed** — click around. Each navigation should produce a fresh `page_viewed` event. If only the initial page load fires, the route-change hook is wired wrong (e.g. effect dependency missing in React, reactive read missing in Solid).
|
|
592
|
-
4. **Cart events fire** — add an item to the cart. You should see `cart_updated` followed by `product_added_to_cart`.
|
|
594
|
+
4. **Cart events fire** — add an item to the cart. You should see `cart_updated` followed by `product_added_to_cart`.
|
|
593
595
|
5. **Privacy banner renders for EU/UK visitors** — if `mode: "default-banner"`, simulate a GDPR-protected region with browser dev-tools location override or VPN. The banner should render. If it does not, check that `cdn.shopify.com` is not blocked by your CSP.
|
|
594
596
|
|
|
595
597
|
For production, re-verify against the production bundle. Several gotchas only appear once the SSR/CSR boundary stabilizes.
|
|
@@ -598,7 +600,7 @@ For production, re-verify against the production bundle. Several gotchas only ap
|
|
|
598
600
|
|
|
599
601
|
## Common gotchas
|
|
600
602
|
|
|
601
|
-
- **
|
|
603
|
+
- **Destinations receive live events and buffered replay after analytics consent is granted.** If the visitor explicitly denies analytics consent, the buffer is cleared and those pre-denial events are never replayed.
|
|
602
604
|
- **The singleton must be lazy.** Reading the global bus at module top-level can run on the server during SSR and crash on `window` access. Always wrap in a `typeof window === 'undefined'` guard.
|
|
603
605
|
- **Use the right shop shape for each API.** `ShopifyScripts` accepts a numeric Shop ID or Shopify Shop GID plus `storefrontId` and the permanent `myshopifyDomain`; the analytics bus normalizes `shop.shopId` to a Shopify Shop GID before dispatch, while the bootstrap exposes the domain as `window.Shopify.shop`.
|
|
604
606
|
- **Customer Privacy script blocked by CSP.** If your CSP does not allow `cdn.shopify.com`, the consent script never loads, `analyticsProcessingAllowed()` stays `false`, and destination events never deliver. Check Network tab for blocked requests; add `cdn.shopify.com` to `script-src`.
|
|
@@ -607,8 +609,7 @@ For production, re-verify against the production bundle. Several gotchas only ap
|
|
|
607
609
|
- **Astro inline scripts cannot reference component scope.** Astro hoists `<script>` tags at build time. Bridge SSR data through hidden DOM (`data-*` attributes) and read it from the script. Trying to interpolate `{product.id}` directly into a script body silently fails — the script ships as a static string.
|
|
608
610
|
- **Astro page-view fires only on full loads.** Astro is MPA-by-default. If you adopt View Transitions, listen for `astro:after-swap` instead of relying on the inline-script-runs-on-load behavior — otherwise SPA-nav transitions skip `page_viewed`.
|
|
609
611
|
- **Required product fields silently drop the Monorail leg.** Missing `id`/`title`/`vendor`/`variantId`/`variantTitle`/`price` causes the Shopify analytics subscriber to skip Monorail dispatch and log a field-specific error. The bus event still fires for your subscribers — the loss is only in Shopify analytics. Watch the console.
|
|
610
|
-
-
|
|
611
|
-
- **`destroy()` is not called by any of the framework adapter sketches.** During HMR or React Strict Mode double-mount, this means duplicate event subscribers and possibly duplicate Monorail events in dev. For production this is rarely visible (one bus per page lifetime). If duplicate dev events bother you, wire `analytics.destroy()` into your framework's teardown (React effect cleanup, Svelte `onDestroy`, Solid `onCleanup`, or equivalent).
|
|
612
|
+
- **Register destinations once per page lifetime.** Hydrogen owns the shared bus. Removing and re-adding a destination resets its replay position, so component remounts can deliver retained events again.
|
|
612
613
|
- **Lighthouse skip is silent.** Monorail dispatch is skipped for Chrome Lighthouse user-agents. If your synthetic monitoring runs Lighthouse, you will see no Monorail requests in those runs — this is intentional.
|
|
613
614
|
|
|
614
615
|
---
|
|
@@ -621,4 +622,4 @@ For production, re-verify against the production bundle. Several gotchas only ap
|
|
|
621
622
|
- **Don't reimplement Monorail dispatch.** If you need a third-party destination, register it with `addDestination()` and forward from there — do not parallel-publish to Monorail yourself.
|
|
622
623
|
- **Don't put per-route view events in a global subscriber.** A single subscriber that watches `page_viewed` and synthesizes `product_viewed` from URL parsing is brittle and loses payload context. Publish each view event from the route that has the data.
|
|
623
624
|
- **Don't construct multiple buses for "different consent contexts" on the same page.** Customer Privacy config is global; the latest initialized config takes effect. If you need conditional behavior, branch inside subscribers, not at construction.
|
|
624
|
-
- **Don't skip the request-handler prerequisite.** Without the SFAPI proxy, modern same-origin Shopify cookies cannot be set.
|
|
625
|
+
- **Don't skip the request-handler prerequisite.** Without the SFAPI proxy, modern same-origin Shopify cookies cannot be set. Session continuity into checkout depends on these backend cookies. Treat analytics as incomplete until the proxy is live in production.
|
|
@@ -10,7 +10,7 @@ If it does not contain `@shopify/hydrogen@preview` (or a more specific hash) in
|
|
|
10
10
|
|
|
11
11
|
### Continue when
|
|
12
12
|
|
|
13
|
-
- [ ] Current
|
|
13
|
+
- [ ] Current directory has `package.json`
|
|
14
14
|
- [ ] Current project includes `@shopify/hydrogen@preview` or similar in its dependencies
|
|
15
15
|
|
|
16
16
|
## Styling
|
|
@@ -20,6 +20,7 @@ If Playwright is present, run it headless.
|
|
|
20
20
|
|
|
21
21
|
- [ ] Every applicable script was executed in this session, after the last code change, and passed
|
|
22
22
|
- [ ] `hydrogen gql check` ran as part of static checks (not skipped)
|
|
23
|
+
- [ ] `dev` still starts with `hydrogen skills check --mode=warn` and prints no skills warning
|
|
23
24
|
|
|
24
25
|
## Run Runtime Smoke Tests
|
|
25
26
|
|
|
@@ -10,7 +10,7 @@ Preserve the app's existing route shape when present. When there is no establish
|
|
|
10
10
|
- `/products/{handle}` for product detail. Use plural `products`, not `/product`.
|
|
11
11
|
- `/cart` for the full cart page.
|
|
12
12
|
|
|
13
|
-
Hydrogen-owned handlers are not page routes: `/api/cart`, `/api/{api-version}/graphql.json`, the generic `/__shopify/*` API proxy, `/checkout`, cart permalinks like `/cart/{variantId}:{quantity}`, AJAX cart URLs like `/cart.js` and `/cart/add.js`, `/api/mcp`, `/graphiql` in development, Liquid-style `?variant=<numeric id>` product redirects, `/admin` redirects, and Storefront URL redirects belong in the `hydrogen-request-handlers` wiring.
|
|
13
|
+
Hydrogen-owned handlers are not page routes: `/api/cart`, `/api/{api-version}/graphql.json`, the generic `/__shopify/*` API proxy, `/checkout`, cart permalinks like `/cart/{variantId}:{quantity}`, UCP buy permalinks like `/buy/{itemId}:{quantity}`, AJAX cart URLs like `/cart.js` and `/cart/add.js`, `/api/mcp`, `/graphiql` in development, Liquid-style `?variant=<numeric id>` product redirects, `/admin` redirects, and Storefront URL redirects belong in the `hydrogen-request-handlers` wiring.
|
|
14
14
|
|
|
15
15
|
Invoke the `hydrogen-routing` skill and create the shared route template manifest for Shopify resources such as products, collections, pages, blogs, or articles.
|
|
16
16
|
|
|
@@ -18,6 +18,25 @@ Invoke the `hydrogen-routing` skill and create the shared route template manifes
|
|
|
18
18
|
|
|
19
19
|
- [ ] Route templates are set up
|
|
20
20
|
|
|
21
|
+
## Keep Skills In Sync
|
|
22
|
+
|
|
23
|
+
The packaged skills describe the installed Hydrogen version. Chain `hydrogen skills check --mode=warn` in front of the app's `dev` script so a later `@shopify/hydrogen` bump prints the resync command the next time the dev server starts, without blocking it:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev": "hydrogen skills check --mode=warn && <framework dev command>"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Keep the framework's own dev command exactly as it was; only prefix it. Apply the same prefix to any sibling dev script such as `dev:https`. Do not add the plain `hydrogen skills check` (error mode) to `dev`; that belongs in CI, where a stale skill copy should fail the build.
|
|
34
|
+
|
|
35
|
+
### Continue when
|
|
36
|
+
|
|
37
|
+
- [ ] `dev` runs `hydrogen skills check --mode=warn` before the framework dev command
|
|
38
|
+
- [ ] Running the `dev` script prints no skills warning (skills were synced by `hydrogen setup`)
|
|
39
|
+
|
|
21
40
|
## Use Standard Environment Names
|
|
22
41
|
|
|
23
42
|
Use these canonical environment variable names throughout the app (kept in sync with `hydrogen-storefront-client`):
|
|
@@ -35,6 +54,8 @@ If the framework requires a prefix to expose client-side variables, preserve the
|
|
|
35
54
|
|
|
36
55
|
List `PUBLIC_STOREFRONT_API_TOKEN` in the app's env example file as a commented-out entry (`# PUBLIC_STOREFRONT_API_TOKEN=`). The Storefront client accepts `undefined` as tokenless access, which is all mock.shop supports, so the scaffold works before the user has tokens and upgrades in place when they add one. Do not write an uncommented empty assignment (`PUBLIC_STOREFRONT_API_TOKEN=`): env loaders parse that as an empty string, and the client rejects empty tokens. Recommend filling it in (or switching to a private client) once the app targets a real store.
|
|
37
56
|
|
|
57
|
+
When the user has no store yet, point `PUBLIC_STORE_DOMAIN` at a mock.shop store. `mock.shop` is the default apparel catalog; https://mock.shop/llms.txt lists every other fictional store with its host (for example `pets.mock.shop`) and what it sells. Pick the store closest to what the user is building, and see the `hydrogen-storefront-client` skill for the details.
|
|
58
|
+
|
|
38
59
|
### Continue when
|
|
39
60
|
|
|
40
61
|
- [ ] Env vars follow the canonical names (plus any required framework prefix)
|
|
@@ -68,7 +68,7 @@ Collection and search:
|
|
|
68
68
|
Analytics:
|
|
69
69
|
- [ ] Page view fires on initial load and client navigations
|
|
70
70
|
- [ ] Product/collection/search/cart view events fire once per route data change
|
|
71
|
-
- [ ] Cart tracking is wired once via trackCartAnalytics(cartStore) (React/Vue: useCartAnalytics()), delta events fire on confirmed cart data changes
|
|
71
|
+
- [ ] Cart tracking is wired once via trackCartAnalytics(cartStore) (React/Vue: useCartAnalytics()), and delta events fire on confirmed cart data changes
|
|
72
72
|
- [ ] No browser module reads private env variables
|
|
73
73
|
- [ ] Production does not bypass Customer Privacy consent gating (no forced-always-true consent checks)
|
|
74
74
|
|
|
@@ -190,7 +190,7 @@ Expected: a redirect whose `location` header points at Shopify's hosted login, n
|
|
|
190
190
|
|
|
191
191
|
- Page view fires on initial load and client navigations.
|
|
192
192
|
- Product/collection/search/cart view events fire once per route data change.
|
|
193
|
-
- Cart tracking is wired once per cart store lifecycle via `trackCartAnalytics(cartStore)` (React/Vue: `useCartAnalytics()`), cart delta events fire on confirmed cart data changes
|
|
193
|
+
- Cart tracking is wired once per cart store lifecycle via `trackCartAnalytics(cartStore)` (React/Vue: `useCartAnalytics()`), and cart delta events fire on confirmed cart data changes.
|
|
194
194
|
- No browser module reads private env variables.
|
|
195
195
|
- Analytics destinations only receive events after Shopify Customer Privacy allows analytics processing.
|
|
196
196
|
|
|
@@ -43,6 +43,10 @@ Environment variables are still a **server-side input boundary**. Do not read `p
|
|
|
43
43
|
|
|
44
44
|
Default to a public client. `publicStorefrontToken` accepts `undefined`, which means tokenless access — there is no rate limit for tokenless clients, and it is all mock.shop supports. Always recommend a token-backed client (public or private) once the app targets a real store: token-based Storefront API access is required for product tags, metaobjects, metafields, menus, and customers. Public and private tokens can query the same token-required fields; the difference is whether the token is safe to expose and whether the request has trusted buyer context.
|
|
45
45
|
|
|
46
|
+
### mock.shop is a catalog of stores
|
|
47
|
+
|
|
48
|
+
mock.shop is not one store. The default store at `mock.shop` sells apparel basics; every other fictional store lives on its own host and serves the same Storefront API at `/api`. When the user has no store yet, read the directory at https://mock.shop/llms.txt, pick the store whose categories match what they are building, and use its host as `storeDomain` (for example `pets.mock.shop`). Each store describes its own catalog at `https://<store>.mock.shop/llms.txt`. Every mock.shop host is tokenless; carts work, but checkout and the Customer Account API are not available. Moving to a real store changes only `storeDomain` and the token.
|
|
49
|
+
|
|
46
50
|
### Public client (default)
|
|
47
51
|
|
|
48
52
|
Wire `publicStorefrontToken` from the canonical env variable without asserting it exists. When `PUBLIC_STOREFRONT_API_TOKEN` is unset the client runs tokenless, so the same scaffold works against mock.shop before the user has tokens and upgrades in place when they add one. In env example files list the variable commented out (`# PUBLIC_STOREFRONT_API_TOKEN=`) rather than as an empty assignment: env loaders parse `PUBLIC_STOREFRONT_API_TOKEN=` as an empty string, and the client rejects empty tokens.
|
|
@@ -280,8 +284,6 @@ Every successful result includes `headers` (the raw `Headers` object from the re
|
|
|
280
284
|
|
|
281
285
|
A 200 response with GraphQL `errors` does NOT throw — partial success is valid in GraphQL. Non-200 responses, timeouts, network failures, and JSON parse errors all throw `StorefrontApiError` (or the `StorefrontTimeoutError` subclass).
|
|
282
286
|
|
|
283
|
-
`StorefrontApiError` carries GraphQL error context (`locations`, `path`, `extensions`) when available. `extensions.code` enables programmatic branching (e.g. retry on `"THROTTLED"`). `toJSON()` strips dev-only fields (`queryText`, `variables`, `stack`) — safe for error reporters.
|
|
284
|
-
|
|
285
287
|
---
|
|
286
288
|
|
|
287
289
|
## Testing
|
|
@@ -86,6 +86,10 @@ Accepts `{ value: number }` for a controlled input or `{ defaultValue: number }`
|
|
|
86
86
|
|
|
87
87
|
Requires `{ optionName, value }`. Returns `{ name, value, onChange, onClick }` — form identity plus activation handlers. Derive caller-owned UI props from the matching option value state, such as `value.exists`, `value.available`, and `value.selected`.
|
|
88
88
|
|
|
89
|
+
### `register("attributeValue", opts)`
|
|
90
|
+
|
|
91
|
+
Requires `{ key: string; value: string }` (controlled) or `{ key: string; defaultValue: string }` (uncontrolled). Returns `{ name: "attributes.<key>", value }` — a hidden input that attaches a line-item attribute to the add-to-cart submission. Use for per-line metadata like engraving text, gift messages, or custom options. Multiple `attributeValue` fields can be registered for different keys. The `key` must be non-empty (throws `TypeError` otherwise). Attributes with keys starting with `_` are conventionally internal/private and should not be set from the storefront UI.
|
|
92
|
+
|
|
89
93
|
### `register("addToCart", opts)`
|
|
90
94
|
|
|
91
95
|
Returns `{ name: "add-to-cart", type: "submit" }` for the add-to-cart submit button.
|
|
@@ -174,8 +178,8 @@ Pass `style: "variant"` with a resolved `variant` to emit a shareable `?variant=
|
|
|
174
178
|
### Add-to-cart
|
|
175
179
|
|
|
176
180
|
- **ALWAYS use `canAddToCart(product, options)` to determine if the add-to-cart button should be enabled.** This checks three conditions: a variant is selected, it is available for sale, and the product does not require a selling plan. Checking only `selectedVariant !== null` misses the selling-plan and availability constraints.
|
|
177
|
-
- **The add-to-cart form is separate from the variant selector.** Variant selection uses buttons and links — not form submissions. The add-to-cart form contains `merchandiseId` (the selected variant ID) and `
|
|
178
|
-
- **Use `register` to bind form fields.** `register("merchandiseId", {})` returns the hidden input props with the current variant ID. `register("quantity", { value: 1 })` returns the quantity input props. `register("addToCart", {})` returns stable add-to-cart submit button props. These stay synchronized with store state automatically.
|
|
181
|
+
- **The add-to-cart form is separate from the variant selector.** Variant selection uses buttons and links — not form submissions. The add-to-cart form contains `merchandiseId` (the selected variant ID), `quantity`, and optionally `attributeValue` fields for line-item attributes. Do not put variant selection controls inside the cart form.
|
|
182
|
+
- **Use `register` to bind form fields.** `register("merchandiseId", {})` returns the hidden input props with the current variant ID. `register("quantity", { value: 1 })` returns the quantity input props. `register("attributeValue", { key: "Engraving", value })` returns a hidden input for a line-item attribute. `register("addToCart", {})` returns stable add-to-cart submit button props. These stay synchronized with store state automatically.
|
|
179
183
|
- **Use the local `hydrogen-shop-pay` skill** when adding accelerated checkout near the add-to-cart form.
|
|
180
184
|
- **Show contextual CTA text.** When `canAddToCart` is `true`: "Add to cart". When no variant is selected (`selectedVariant === null`): "Select options" unless a navigation or submission is actually pending. When a variant is selected but unavailable: "Unavailable" or "Sold out".
|
|
181
185
|
- **Surface cart errors from `errors` state.** After form submission, user errors, warnings, and network errors relevant to the current product form are available on `state.errors`. Display these to the buyer.
|
|
@@ -246,14 +250,16 @@ Pass `style: "variant"` with a resolved `variant` to emit a shareable `?variant=
|
|
|
246
250
|
28. **Caller-owned option attributes** — `register("optionValue", { optionName: "Color", value: "Red" })` returns only `{ name, value, onChange, onClick }`. Derive `disabled`, `aria-pressed`, and visual state from the matching `options` value.
|
|
247
251
|
29. **Quantity registration** — `register("quantity", { value: 1 })` returns `{ name: "quantity", value: "1" }`. `register("quantity", { defaultValue: 1 })` returns `{ name: "quantity", defaultValue: "1" }`.
|
|
248
252
|
30. **Add-to-cart registration** — `register("addToCart", {})` returns `{ name: "add-to-cart", type: "submit" }`.
|
|
253
|
+
31. **Attribute value registration** — `register("attributeValue", { key: "Engraving", value: "Hello" })` returns `{ name: "attributes.Engraving", value: "Hello" }`. `register("attributeValue", { key: "Engraving", defaultValue: "" })` returns `{ name: "attributes.Engraving", defaultValue: "" }`. An empty `key` throws a `TypeError`.
|
|
254
|
+
32. **Attribute in add-to-cart** — When `register("attributeValue", { key: "Engraving", value: "Hello" })` is included as a hidden input in the add-to-cart form, the submitted cart line carries the attribute. The attribute appears on the matched line item in the cart drawer (with `_`-prefixed internal attributes filtered out).
|
|
249
255
|
|
|
250
256
|
### Unresolved selection
|
|
251
257
|
|
|
252
|
-
|
|
258
|
+
33. **Transient unresolved** — In URL-routing apps, when a complete selection returns `unresolved` because the exact variant is absent from the local cache, the app navigates to the new URL and re-fetches product data. The subsequent hydration resolves the variant. Incomplete selections should remain in selection UI until the buyer chooses the remaining options.
|
|
253
259
|
|
|
254
260
|
### Reset
|
|
255
261
|
|
|
256
|
-
|
|
262
|
+
34. **Reset to initial state** — Calling `reset()` restores the store to the product and selected options it was created with. All user selections are discarded.
|
|
257
263
|
|
|
258
264
|
---
|
|
259
265
|
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { asInlineScript } from "./utils/inline-script.mjs";
|
|
2
|
-
import { SHOPIFY_CONSENT_SCRIPT_ID, VISITOR_CONSENT_COLLECTED_EVENT } from "./constants.mjs";
|
|
3
|
-
|
|
4
|
-
//#region src/core/shopify-scripts/consent.ts
|
|
5
|
-
const initializeShopifyConsentTracking = (scriptData) => decodeURIComponent("(function()%7Bfunction%20e(e)%7Breturn%20typeof%20e%3D%3D%60object%60%26%26!!e%7Dfunction%20t(e%2Ct)%7Blet%20n%3D%5B%60%5Bhydrogen%3Aerror%3Aconsent%5D%20%24%7Be%7D%60%5D%3Bt!%3D%3Dvoid%200%26%26n.push(t)%2Cconsole.error(...n)%7Dfunction%20n(n)%7Blet%20r%3Dwindow%2Ci%3Dn.mode%3D%3D%3D%60default-banner%60%2Ca%3D!1%2Co%3D!1%2Cs%3D%60initial%60%2Cc%3D()%3D%3Er.Shopify%3F.customerPrivacy%2Cl%3D()%3D%3E%7Blet%20e%3Dc()%3Be%26%26(e.consentStatus%3D%60loaded%60)%7D%2Cu%3D()%3D%3E%7Bo%3D!0%2Ca%26%26l()%7D%2Cd%3D()%3D%3E%7Blet%20e%3Dc()%3F.setTrackingConsent%3Bif(typeof%20e!%3D%60function%60)return%20t(%60unable%20to%20request%20initial%20consent%60)%2Cu()%3Be(%7BheadlessStorefront%3A!0%7D%2Ce%3D%3E%7Bif(e%3F.error)%7Bt(%60unable%20to%20request%20initial%20consent%60%2Ce.error)%3Breturn%7Du()%7D)%7D%2Cf%3D()%3D%3E%7Blet%20e%3Dc()%3Breturn%20typeof%20e%3F.setTrackingConsent%3D%3D%60function%60%7C%7Ctypeof%20e%3F.shouldShowBanner%3D%3D%60function%60%7C%7Ctypeof%20window.privacyBanner%3D%3D%60object%60%7D%2Cp%3D()%3D%3E%7Ba%7C%7C(a%3D!0%2Ci%3Fo%26%26u()%3Ad())%7D%3Bdocument.addEventListener(n.eventName%2Ct%3D%3E%7Blet%20n%3D%60detail%60in%20t%3Ft.detail%3Avoid%200%3Be(n)%26%26(n.source%3F%3F%3Ds)%2Cs%3D%60interaction%60%7D%2C%7Bcapture%3A!0%7D)%2Cdocument.addEventListener(n.eventName%2Cu%2C%7Bcapture%3A!0%7D)%3Blet%20m%3Ddocument.getElementById(n.scriptId)%3Bf()%3Fp()%3Am%3F.addEventListener(%60load%60%2Cp%2C%7Bonce%3A!0%7D)%7Dn(__SHOPIFY_SCRIPT_DATA__)%7D)()%3B").replace("__SHOPIFY_SCRIPT_DATA__", () => (JSON.stringify(scriptData) ?? "undefined").replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029"));
|
|
6
|
-
function getShopifyConsentTrackingScript(consent = {}) {
|
|
7
|
-
const config = {
|
|
8
|
-
eventName: VISITOR_CONSENT_COLLECTED_EVENT,
|
|
9
|
-
mode: consent.mode ?? "no-banner",
|
|
10
|
-
scriptId: SHOPIFY_CONSENT_SCRIPT_ID
|
|
11
|
-
};
|
|
12
|
-
return asInlineScript(initializeShopifyConsentTracking)(config);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
//#endregion
|
|
16
|
-
export { getShopifyConsentTrackingScript };
|
|
17
|
-
//# sourceMappingURL=consent.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"consent.mjs","names":[],"sources":["../../../src/core/shopify-scripts/consent.ts"],"sourcesContent":["import type { ConsentConfig } from \"../analytics/types\";\nimport initializeShopifyConsentTracking from \"./consent-script\" with { type: \"script\" };\nimport type { ShopifyConsentTrackingConfig } from \"./consent-script\";\nimport { SHOPIFY_CONSENT_SCRIPT_ID, VISITOR_CONSENT_COLLECTED_EVENT } from \"./constants\";\nimport { asInlineScript } from \"./utils/inline-script\";\n\nexport function getShopifyConsentTrackingScript(consent: ConsentConfig = {}): string {\n const config: ShopifyConsentTrackingConfig = {\n eventName: VISITOR_CONSENT_COLLECTED_EVENT,\n mode: consent.mode ?? \"no-banner\",\n scriptId: SHOPIFY_CONSENT_SCRIPT_ID,\n };\n\n return asInlineScript(initializeShopifyConsentTracking)(config);\n}\n"],"mappings":";;;;AACA,MAAM,oCAAkC,eAAgB,mBAAqB,q8CAAU,CAAA,QAAA,kCAAA,KAAA,UAAA,WAAA,IAAA,aAAA,QAAA,MAAA,UAAA,CAAA,QAAA,WAAA,UAAA,CAAA,QAAA,WAAA,UAAA,CAAA;AAKvF,SAAgB,gCAAgC,UAAyB,EAAE,EAAU;CACnF,MAAM,SAAuC;EAC3C,WAAW;EACX,MAAM,QAAQ,QAAQ;EACtB,UAAU;EACX;AAED,QAAO,eAAe,iCAAiC,CAAC,OAAO"}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { VISITOR_CONSENT_COLLECTED_EVENT } from "./constants.mjs";
|
|
2
|
-
import { findWritableCookieDomain } from "./utils/cookie-domain.mjs";
|
|
3
|
-
import { getTrackingValues } from "./utils/tracking-values.mjs";
|
|
4
|
-
import { buildUUID } from "./utils/uuid.mjs";
|
|
5
|
-
|
|
6
|
-
//#region src/core/shopify-scripts/deprecated-cookies.ts
|
|
7
|
-
/**
|
|
8
|
-
* @deprecated This module manages the legacy `_shopify_y` and `_shopify_s`
|
|
9
|
-
* JavaScript-visible cookies. Modern Shopify storefronts use http-only cookies
|
|
10
|
-
* set by the Storefront API via the SFAPI proxy. This module exists only for
|
|
11
|
-
* backward compatibility with downstream systems that may still read these
|
|
12
|
-
* JS cookies directly.
|
|
13
|
-
*
|
|
14
|
-
* To remove: delete this file and remove the ShopifyScripts startup call.
|
|
15
|
-
*/
|
|
16
|
-
const SHOPIFY_Y = "_shopify_y";
|
|
17
|
-
const SHOPIFY_S = "_shopify_s";
|
|
18
|
-
const LONG_TERM_EXPIRY_IN_SECONDS = 3600 * 24 * 360;
|
|
19
|
-
const SHORT_TERM_EXPIRY_IN_SECONDS = 1800;
|
|
20
|
-
/**
|
|
21
|
-
* Computes the cookie domain as the broadest wildcard domain the browser will
|
|
22
|
-
* accept for the consent domain. Returns empty string for localhost or when
|
|
23
|
-
* probing fails (cookies use current domain).
|
|
24
|
-
*
|
|
25
|
-
* @deprecated Part of legacy cookie management.
|
|
26
|
-
*/
|
|
27
|
-
function computeCookieDomain(consentDomain) {
|
|
28
|
-
if (typeof window === "undefined") return "";
|
|
29
|
-
return findWritableCookieDomain(consentDomain || window.location.host);
|
|
30
|
-
}
|
|
31
|
-
function setCookie(name, value, maxAgeInSeconds, domain) {
|
|
32
|
-
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
33
|
-
parts.push(`max-age=${maxAgeInSeconds}`);
|
|
34
|
-
parts.push("path=/");
|
|
35
|
-
parts.push("SameSite=Lax");
|
|
36
|
-
if (domain) parts.push(`domain=${domain}`);
|
|
37
|
-
document.cookie = parts.join("; ");
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* @deprecated Sets or removes the legacy `_shopify_y` and `_shopify_s` cookies
|
|
41
|
-
* based on the current consent state and tracking values.
|
|
42
|
-
*/
|
|
43
|
-
function updateCookies(hasConsent, domain) {
|
|
44
|
-
if (hasConsent) {
|
|
45
|
-
const trackingValues = getTrackingValues();
|
|
46
|
-
if ((trackingValues.uniqueToken || trackingValues.visitToken || "").startsWith("00000000-")) return;
|
|
47
|
-
setCookie(SHOPIFY_Y, trackingValues.uniqueToken || buildUUID(), LONG_TERM_EXPIRY_IN_SECONDS, domain);
|
|
48
|
-
setCookie(SHOPIFY_S, trackingValues.visitToken || buildUUID(), SHORT_TERM_EXPIRY_IN_SECONDS, domain);
|
|
49
|
-
} else {
|
|
50
|
-
setCookie(SHOPIFY_Y, "", 0, domain);
|
|
51
|
-
setCookie(SHOPIFY_S, "", 0, domain);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
function hasAnalyticsConsent() {
|
|
55
|
-
try {
|
|
56
|
-
return (window.Shopify?.customerPrivacy)?.analyticsProcessingAllowed?.() ?? false;
|
|
57
|
-
} catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function isConsentLoaded() {
|
|
62
|
-
return window.Shopify?.customerPrivacy?.consentStatus === "loaded";
|
|
63
|
-
}
|
|
64
|
-
function getConsentDomain() {
|
|
65
|
-
return window.Shopify?.customerPrivacy?.config?.consentDomain;
|
|
66
|
-
}
|
|
67
|
-
let didInitializeDeprecatedCookies = false;
|
|
68
|
-
const noOpCleanup = () => {};
|
|
69
|
-
/**
|
|
70
|
-
* @deprecated Starts page-lifetime legacy cookie management from ShopifyScripts.
|
|
71
|
-
*/
|
|
72
|
-
function initializeDeprecatedCookies() {
|
|
73
|
-
if (typeof window === "undefined" || typeof document === "undefined") return noOpCleanup;
|
|
74
|
-
if (didInitializeDeprecatedCookies) return noOpCleanup;
|
|
75
|
-
didInitializeDeprecatedCookies = true;
|
|
76
|
-
const domain = computeCookieDomain(getConsentDomain());
|
|
77
|
-
const syncConsent = () => {
|
|
78
|
-
updateCookies(hasAnalyticsConsent(), domain);
|
|
79
|
-
};
|
|
80
|
-
document.addEventListener(VISITOR_CONSENT_COLLECTED_EVENT, syncConsent);
|
|
81
|
-
if (isConsentLoaded()) syncConsent();
|
|
82
|
-
const syncPageView = () => {
|
|
83
|
-
if (hasAnalyticsConsent()) updateCookies(true, domain);
|
|
84
|
-
};
|
|
85
|
-
const subscribeToAnalytics = () => {
|
|
86
|
-
const subscribe = window.Shopify?.analytics?.subscribe;
|
|
87
|
-
if (typeof subscribe !== "function") return false;
|
|
88
|
-
subscribe("page_viewed", syncPageView);
|
|
89
|
-
return true;
|
|
90
|
-
};
|
|
91
|
-
if (!subscribeToAnalytics() && document.readyState === "loading") document.addEventListener("DOMContentLoaded", subscribeToAnalytics, { once: true });
|
|
92
|
-
return noOpCleanup;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
//#endregion
|
|
96
|
-
export { initializeDeprecatedCookies };
|
|
97
|
-
//# sourceMappingURL=deprecated-cookies.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"deprecated-cookies.mjs","names":[],"sources":["../../../src/core/shopify-scripts/deprecated-cookies.ts"],"sourcesContent":["/**\n * @deprecated This module manages the legacy `_shopify_y` and `_shopify_s`\n * JavaScript-visible cookies. Modern Shopify storefronts use http-only cookies\n * set by the Storefront API via the SFAPI proxy. This module exists only for\n * backward compatibility with downstream systems that may still read these\n * JS cookies directly.\n *\n * To remove: delete this file and remove the ShopifyScripts startup call.\n */\n\nimport { VISITOR_CONSENT_COLLECTED_EVENT } from \"./constants\";\nimport { findWritableCookieDomain } from \"./utils/cookie-domain\";\nimport { getTrackingValues } from \"./utils/tracking-values\";\nimport { buildUUID } from \"./utils/uuid\";\n\nconst SHOPIFY_Y = \"_shopify_y\";\nconst SHOPIFY_S = \"_shopify_s\";\n\nconst LONG_TERM_EXPIRY_IN_SECONDS = 60 * 60 * 24 * 360;\nconst SHORT_TERM_EXPIRY_IN_SECONDS = 60 * 30;\n\n/**\n * Computes the cookie domain as the broadest wildcard domain the browser will\n * accept for the consent domain. Returns empty string for localhost or when\n * probing fails (cookies use current domain).\n *\n * @deprecated Part of legacy cookie management.\n */\nexport function computeCookieDomain(consentDomain?: string): string {\n if (typeof window === \"undefined\") return \"\";\n\n return findWritableCookieDomain(consentDomain || window.location.host);\n}\n\nfunction setCookie(name: string, value: string, maxAgeInSeconds: number, domain: string): void {\n const parts = [`${name}=${encodeURIComponent(value)}`];\n parts.push(`max-age=${maxAgeInSeconds}`);\n parts.push(\"path=/\");\n parts.push(\"SameSite=Lax\");\n if (domain) parts.push(`domain=${domain}`);\n document.cookie = parts.join(\"; \");\n}\n\n/**\n * @deprecated Sets or removes the legacy `_shopify_y` and `_shopify_s` cookies\n * based on the current consent state and tracking values.\n */\nfunction updateCookies(hasConsent: boolean, domain: string): void {\n if (hasConsent) {\n const trackingValues = getTrackingValues();\n const tokenValue = trackingValues.uniqueToken || trackingValues.visitToken || \"\";\n if (tokenValue.startsWith(\"00000000-\")) return;\n\n setCookie(\n SHOPIFY_Y,\n trackingValues.uniqueToken || buildUUID(),\n LONG_TERM_EXPIRY_IN_SECONDS,\n domain,\n );\n setCookie(\n SHOPIFY_S,\n trackingValues.visitToken || buildUUID(),\n SHORT_TERM_EXPIRY_IN_SECONDS,\n domain,\n );\n } else {\n setCookie(SHOPIFY_Y, \"\", 0, domain);\n setCookie(SHOPIFY_S, \"\", 0, domain);\n }\n}\n\nfunction hasAnalyticsConsent(): boolean {\n try {\n const privacy = window.Shopify?.customerPrivacy;\n return privacy?.analyticsProcessingAllowed?.() ?? false;\n } catch {\n return false;\n }\n}\n\nfunction isConsentLoaded(): boolean {\n return window.Shopify?.customerPrivacy?.consentStatus === \"loaded\";\n}\n\nfunction getConsentDomain(): string | undefined {\n return window.Shopify?.customerPrivacy?.config?.consentDomain;\n}\n\nlet didInitializeDeprecatedCookies = false;\nconst noOpCleanup = () => {};\n\n/**\n * @deprecated Starts page-lifetime legacy cookie management from ShopifyScripts.\n */\nexport function initializeDeprecatedCookies(): () => void {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return noOpCleanup;\n if (didInitializeDeprecatedCookies) return noOpCleanup;\n\n didInitializeDeprecatedCookies = true;\n const domain = computeCookieDomain(getConsentDomain());\n\n const syncConsent = () => {\n updateCookies(hasAnalyticsConsent(), domain);\n };\n\n document.addEventListener(VISITOR_CONSENT_COLLECTED_EVENT, syncConsent);\n if (isConsentLoaded()) syncConsent();\n\n const syncPageView = () => {\n if (hasAnalyticsConsent()) {\n updateCookies(true, domain);\n }\n };\n\n const subscribeToAnalytics = () => {\n const subscribe = window.Shopify?.analytics?.subscribe;\n if (typeof subscribe !== \"function\") return false;\n\n subscribe(\"page_viewed\", syncPageView);\n return true;\n };\n\n if (!subscribeToAnalytics() && document.readyState === \"loading\") {\n // The bus is inline today, but if it moves to a module/defer script it will\n // still be available before DOMContentLoaded.\n document.addEventListener(\"DOMContentLoaded\", subscribeToAnalytics, { once: true });\n }\n\n return noOpCleanup;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAM,YAAY;AAClB,MAAM,YAAY;AAElB,MAAM,8BAA8B,OAAU,KAAK;AACnD,MAAM,+BAA+B;;;;;;;;AASrC,SAAgB,oBAAoB,eAAgC;AAClE,KAAI,OAAO,WAAW,YAAa,QAAO;AAE1C,QAAO,yBAAyB,iBAAiB,OAAO,SAAS,KAAK;;AAGxE,SAAS,UAAU,MAAc,OAAe,iBAAyB,QAAsB;CAC7F,MAAM,QAAQ,CAAC,GAAG,KAAK,GAAG,mBAAmB,MAAM,GAAG;AACtD,OAAM,KAAK,WAAW,kBAAkB;AACxC,OAAM,KAAK,SAAS;AACpB,OAAM,KAAK,eAAe;AAC1B,KAAI,OAAQ,OAAM,KAAK,UAAU,SAAS;AAC1C,UAAS,SAAS,MAAM,KAAK,KAAK;;;;;;AAOpC,SAAS,cAAc,YAAqB,QAAsB;AAChE,KAAI,YAAY;EACd,MAAM,iBAAiB,mBAAmB;AAE1C,OADmB,eAAe,eAAe,eAAe,cAAc,IAC/D,WAAW,YAAY,CAAE;AAExC,YACE,WACA,eAAe,eAAe,WAAW,EACzC,6BACA,OACD;AACD,YACE,WACA,eAAe,cAAc,WAAW,EACxC,8BACA,OACD;QACI;AACL,YAAU,WAAW,IAAI,GAAG,OAAO;AACnC,YAAU,WAAW,IAAI,GAAG,OAAO;;;AAIvC,SAAS,sBAA+B;AACtC,KAAI;AAEF,UADgB,OAAO,SAAS,kBAChB,8BAA8B,IAAI;SAC5C;AACN,SAAO;;;AAIX,SAAS,kBAA2B;AAClC,QAAO,OAAO,SAAS,iBAAiB,kBAAkB;;AAG5D,SAAS,mBAAuC;AAC9C,QAAO,OAAO,SAAS,iBAAiB,QAAQ;;AAGlD,IAAI,iCAAiC;AACrC,MAAM,oBAAoB;;;;AAK1B,SAAgB,8BAA0C;AACxD,KAAI,OAAO,WAAW,eAAe,OAAO,aAAa,YAAa,QAAO;AAC7E,KAAI,+BAAgC,QAAO;AAE3C,kCAAiC;CACjC,MAAM,SAAS,oBAAoB,kBAAkB,CAAC;CAEtD,MAAM,oBAAoB;AACxB,gBAAc,qBAAqB,EAAE,OAAO;;AAG9C,UAAS,iBAAiB,iCAAiC,YAAY;AACvE,KAAI,iBAAiB,CAAE,cAAa;CAEpC,MAAM,qBAAqB;AACzB,MAAI,qBAAqB,CACvB,eAAc,MAAM,OAAO;;CAI/B,MAAM,6BAA6B;EACjC,MAAM,YAAY,OAAO,SAAS,WAAW;AAC7C,MAAI,OAAO,cAAc,WAAY,QAAO;AAE5C,YAAU,eAAe,aAAa;AACtC,SAAO;;AAGT,KAAI,CAAC,sBAAsB,IAAI,SAAS,eAAe,UAGrD,UAAS,iBAAiB,oBAAoB,sBAAsB,EAAE,MAAM,MAAM,CAAC;AAGrF,QAAO"}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
//#region src/core/shopify-scripts/utils/cookie-domain.ts
|
|
2
|
-
function normalizeHost(host) {
|
|
3
|
-
return host.replace(/^https?:\/\//, "").split("/")[0]?.split(":")[0]?.replace(/^\./, "").toLowerCase() ?? "";
|
|
4
|
-
}
|
|
5
|
-
function isIpAddress(host) {
|
|
6
|
-
return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) || host.includes(":");
|
|
7
|
-
}
|
|
8
|
-
function canUseDomainCookie(domain) {
|
|
9
|
-
if (typeof document === "undefined") return false;
|
|
10
|
-
const name = `hydrogen_domain_probe_${Math.random().toString(36).slice(2)}`;
|
|
11
|
-
const nameValuePair = `${name}=1`;
|
|
12
|
-
const cookie = `${nameValuePair}; domain=${domain}; path=/; SameSite=Lax`;
|
|
13
|
-
const cleanup = `${name}=; domain=${domain}; path=/; max-age=0; SameSite=Lax`;
|
|
14
|
-
try {
|
|
15
|
-
document.cookie = cookie;
|
|
16
|
-
const found = document.cookie.split(";").some((part) => part.trim() === nameValuePair);
|
|
17
|
-
document.cookie = cleanup;
|
|
18
|
-
return found;
|
|
19
|
-
} catch {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Finds the broadest wildcard cookie domain the current document can set for a
|
|
25
|
-
* host. Browsers reject public suffixes like `.com` and `.co.uk`, so probing is
|
|
26
|
-
* the only reliable way to find the earliest usable wildcard without shipping a
|
|
27
|
-
* public suffix list.
|
|
28
|
-
*/
|
|
29
|
-
function findWritableCookieDomain(host, probe = canUseDomainCookie) {
|
|
30
|
-
const normalizedHost = normalizeHost(host);
|
|
31
|
-
if (!normalizedHost || normalizedHost === "localhost" || isIpAddress(normalizedHost)) return "";
|
|
32
|
-
const parts = normalizedHost.split(".").filter(Boolean);
|
|
33
|
-
if (parts.length < 2) return "";
|
|
34
|
-
for (let index = parts.length - 2; index >= 0; index--) {
|
|
35
|
-
const domain = `.${parts.slice(index).join(".")}`;
|
|
36
|
-
if (probe(domain)) return domain;
|
|
37
|
-
}
|
|
38
|
-
return "";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
//#endregion
|
|
42
|
-
export { findWritableCookieDomain };
|
|
43
|
-
//# sourceMappingURL=cookie-domain.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cookie-domain.mjs","names":[],"sources":["../../../../src/core/shopify-scripts/utils/cookie-domain.ts"],"sourcesContent":["function normalizeHost(host: string): string {\n const withoutProtocol = host.replace(/^https?:\\/\\//, \"\");\n return withoutProtocol.split(\"/\")[0]?.split(\":\")[0]?.replace(/^\\./, \"\").toLowerCase() ?? \"\";\n}\n\nfunction isIpAddress(host: string): boolean {\n return /^\\d{1,3}(?:\\.\\d{1,3}){3}$/.test(host) || host.includes(\":\");\n}\n\nfunction canUseDomainCookie(domain: string): boolean {\n if (typeof document === \"undefined\") return false;\n\n const name = `hydrogen_domain_probe_${Math.random().toString(36).slice(2)}`;\n const value = \"1\";\n const nameValuePair = `${name}=${value}`;\n const cookie = `${nameValuePair}; domain=${domain}; path=/; SameSite=Lax`;\n const cleanup = `${name}=; domain=${domain}; path=/; max-age=0; SameSite=Lax`;\n\n try {\n document.cookie = cookie;\n const found = document.cookie.split(\";\").some((part) => part.trim() === nameValuePair);\n document.cookie = cleanup;\n return found;\n } catch {\n return false;\n }\n}\n\n/**\n * Finds the broadest wildcard cookie domain the current document can set for a\n * host. Browsers reject public suffixes like `.com` and `.co.uk`, so probing is\n * the only reliable way to find the earliest usable wildcard without shipping a\n * public suffix list.\n */\nexport function findWritableCookieDomain(\n host: string,\n probe: (domain: string) => boolean = canUseDomainCookie,\n): string {\n const normalizedHost = normalizeHost(host);\n if (!normalizedHost || normalizedHost === \"localhost\" || isIpAddress(normalizedHost)) return \"\";\n\n const parts = normalizedHost.split(\".\").filter(Boolean);\n if (parts.length < 2) return \"\";\n\n for (let index = parts.length - 2; index >= 0; index--) {\n const domain = `.${parts.slice(index).join(\".\")}`;\n if (probe(domain)) return domain;\n }\n\n return \"\";\n}\n"],"mappings":";AAAA,SAAS,cAAc,MAAsB;AAE3C,QADwB,KAAK,QAAQ,gBAAgB,GAC/B,CAAC,MAAM,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,IAAI,QAAQ,OAAO,GAAG,CAAC,aAAa,IAAI;;AAG3F,SAAS,YAAY,MAAuB;AAC1C,QAAO,4BAA4B,KAAK,KAAK,IAAI,KAAK,SAAS,IAAI;;AAGrE,SAAS,mBAAmB,QAAyB;AACnD,KAAI,OAAO,aAAa,YAAa,QAAO;CAE5C,MAAM,OAAO,yBAAyB,KAAK,QAAQ,CAAC,SAAS,GAAG,CAAC,MAAM,EAAE;CAEzE,MAAM,gBAAgB,GAAG,KAAK;CAC9B,MAAM,SAAS,GAAG,cAAc,WAAW,OAAO;CAClD,MAAM,UAAU,GAAG,KAAK,YAAY,OAAO;AAE3C,KAAI;AACF,WAAS,SAAS;EAClB,MAAM,QAAQ,SAAS,OAAO,MAAM,IAAI,CAAC,MAAM,SAAS,KAAK,MAAM,KAAK,cAAc;AACtF,WAAS,SAAS;AAClB,SAAO;SACD;AACN,SAAO;;;;;;;;;AAUX,SAAgB,yBACd,MACA,QAAqC,oBAC7B;CACR,MAAM,iBAAiB,cAAc,KAAK;AAC1C,KAAI,CAAC,kBAAkB,mBAAmB,eAAe,YAAY,eAAe,CAAE,QAAO;CAE7F,MAAM,QAAQ,eAAe,MAAM,IAAI,CAAC,OAAO,QAAQ;AACvD,KAAI,MAAM,SAAS,EAAG,QAAO;AAE7B,MAAK,IAAI,QAAQ,MAAM,SAAS,GAAG,SAAS,GAAG,SAAS;EACtD,MAAM,SAAS,IAAI,MAAM,MAAM,MAAM,CAAC,KAAK,IAAI;AAC/C,MAAI,MAAM,OAAO,CAAE,QAAO;;AAG5B,QAAO"}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
//#region src/core/shopify-scripts/utils/tracking-values.ts
|
|
2
|
-
const cachedTrackingValues = { current: null };
|
|
3
|
-
function getTrackingValues() {
|
|
4
|
-
let trackingValues;
|
|
5
|
-
if (typeof window !== "undefined" && typeof window.performance !== "undefined") try {
|
|
6
|
-
const resourceRE = /^https?:\/\/([^/]+)(\/api\/(?:unstable|2\d{3}-\d{2})\/graphql\.json(?=$|\?))?/;
|
|
7
|
-
const entries = performance.getEntriesByType("resource");
|
|
8
|
-
let matchedValues;
|
|
9
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
10
|
-
const entry = entries[i];
|
|
11
|
-
if (entry.initiatorType !== "fetch") continue;
|
|
12
|
-
const currentHost = window.location.host;
|
|
13
|
-
const match = entry.name.match(resourceRE);
|
|
14
|
-
if (!match) continue;
|
|
15
|
-
const [, matchedHost, sfapiPath] = match;
|
|
16
|
-
if (matchedHost === currentHost || sfapiPath && matchedHost?.endsWith(`.${currentHost}`)) {
|
|
17
|
-
const values = extractFromPerformanceEntry(entry);
|
|
18
|
-
if (values) {
|
|
19
|
-
matchedValues = values;
|
|
20
|
-
break;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
if (matchedValues) trackingValues = matchedValues;
|
|
25
|
-
if (trackingValues) cachedTrackingValues.current = trackingValues;
|
|
26
|
-
else if (cachedTrackingValues.current) trackingValues = cachedTrackingValues.current;
|
|
27
|
-
if (!trackingValues) {
|
|
28
|
-
const navigationEntries = performance.getEntriesByType("navigation")[0];
|
|
29
|
-
trackingValues = extractFromPerformanceEntry(navigationEntries, false);
|
|
30
|
-
}
|
|
31
|
-
} catch {}
|
|
32
|
-
if (!trackingValues) {
|
|
33
|
-
const cookie = typeof document !== "undefined" ? document.cookie : "";
|
|
34
|
-
trackingValues = {
|
|
35
|
-
uniqueToken: cookie.match(/\b_shopify_y=([^;]+)/)?.[1] || "",
|
|
36
|
-
visitToken: cookie.match(/\b_shopify_s=([^;]+)/)?.[1] || "",
|
|
37
|
-
consent: cookie.match(/\b_tracking_consent=([^;]+)/)?.[1] || ""
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
return trackingValues;
|
|
41
|
-
}
|
|
42
|
-
function extractFromPerformanceEntry(entry, isConsentRequired = true) {
|
|
43
|
-
let uniqueToken = "";
|
|
44
|
-
let visitToken = "";
|
|
45
|
-
let consent = "";
|
|
46
|
-
const serverTiming = entry.serverTiming;
|
|
47
|
-
const minimumEntries = isConsentRequired ? 3 : 2;
|
|
48
|
-
if (serverTiming && serverTiming.length >= minimumEntries) for (let i = serverTiming.length - 1; i >= 0; i--) {
|
|
49
|
-
const { name, description } = serverTiming[i];
|
|
50
|
-
if (!name || !description) continue;
|
|
51
|
-
if (name === "_y") uniqueToken = description;
|
|
52
|
-
else if (name === "_s") visitToken = description;
|
|
53
|
-
else if (name === "_cmp") consent = description;
|
|
54
|
-
if (uniqueToken && visitToken && consent) break;
|
|
55
|
-
}
|
|
56
|
-
return uniqueToken && visitToken && (isConsentRequired ? consent : true) ? {
|
|
57
|
-
uniqueToken,
|
|
58
|
-
visitToken,
|
|
59
|
-
consent
|
|
60
|
-
} : void 0;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
//#endregion
|
|
64
|
-
export { getTrackingValues };
|
|
65
|
-
//# sourceMappingURL=tracking-values.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tracking-values.mjs","names":[],"sources":["../../../../src/core/shopify-scripts/utils/tracking-values.ts"],"sourcesContent":["/**\n * Inlined from @shopify/hydrogen-react/tracking-utils.\n * Retrieves user session tracking values from Server-Timing headers\n * and the Performance API.\n */\n\ntype TrackingValues = {\n uniqueToken: string;\n visitToken: string;\n consent: string;\n};\n\nconst cachedTrackingValues: { current: null | TrackingValues } = { current: null };\n\nexport function getTrackingValues(): TrackingValues {\n // Tracking values are returned in Server-Timing headers from SFAPI responses.\n // Prefer recent fetch/resource entries, then cached values, then the initial\n // navigation entry, and finally the deprecated JS-visible cookies.\n let trackingValues: TrackingValues | undefined;\n\n if (typeof window !== \"undefined\" && typeof window.performance !== \"undefined\") {\n try {\n // Extract host and optionally match the SFAPI GraphQL pathname.\n const resourceRE =\n /^https?:\\/\\/([^/]+)(\\/api\\/(?:unstable|2\\d{3}-\\d{2})\\/graphql\\.json(?=$|\\?))?/;\n\n // Search backwards so the latest matching fetch with complete tracking\n // values wins. Same-origin fetches cover Hydrogen proxy/server responses;\n // subdomain SFAPI requests cover direct Storefront API calls.\n const entries = performance.getEntriesByType(\"resource\") as PerformanceResourceTiming[];\n\n let matchedValues: TrackingValues | undefined;\n\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i];\n if (entry.initiatorType !== \"fetch\") continue;\n\n const currentHost = window.location.host;\n const match = entry.name.match(resourceRE);\n if (!match) continue;\n\n const [, matchedHost, sfapiPath] = match;\n const isMatch =\n matchedHost === currentHost || (sfapiPath && matchedHost?.endsWith(`.${currentHost}`));\n\n if (isMatch) {\n const values = extractFromPerformanceEntry(entry);\n if (values) {\n matchedValues = values;\n break;\n }\n }\n }\n\n if (matchedValues) {\n trackingValues = matchedValues;\n }\n\n // Resource entries have a limited buffer and can be removed over time.\n // A cached resource entry is newer than the navigation entry.\n if (trackingValues) {\n cachedTrackingValues.current = trackingValues;\n } else if (cachedTrackingValues.current) {\n trackingValues = cachedTrackingValues.current;\n }\n\n if (!trackingValues) {\n // The document navigation entry can omit _cmp when the server generated\n // tracking tokens, so only _y and _s are required for this fallback.\n const navigationEntries = performance.getEntriesByType(\n \"navigation\",\n )[0] as PerformanceNavigationTiming;\n trackingValues = extractFromPerformanceEntry(navigationEntries, false);\n }\n } catch {}\n }\n\n if (!trackingValues) {\n const cookie = typeof document !== \"undefined\" ? document.cookie : \"\";\n\n trackingValues = {\n uniqueToken: cookie.match(/\\b_shopify_y=([^;]+)/)?.[1] || \"\",\n visitToken: cookie.match(/\\b_shopify_s=([^;]+)/)?.[1] || \"\",\n consent: cookie.match(/\\b_tracking_consent=([^;]+)/)?.[1] || \"\",\n };\n }\n\n return trackingValues;\n}\n\nfunction extractFromPerformanceEntry(\n entry: PerformanceNavigationTiming | PerformanceResourceTiming,\n isConsentRequired = true,\n): TrackingValues | undefined {\n let uniqueToken = \"\";\n let visitToken = \"\";\n let consent = \"\";\n\n const serverTiming = entry.serverTiming;\n const minimumEntries = isConsentRequired ? 3 : 2;\n if (serverTiming && serverTiming.length >= minimumEntries) {\n // Iterate backwards since Shopify tracking metrics are usually appended\n // after framework/server metrics.\n for (let i = serverTiming.length - 1; i >= 0; i--) {\n const { name, description } = serverTiming[i];\n if (!name || !description) continue;\n\n if (name === \"_y\") uniqueToken = description;\n else if (name === \"_s\") visitToken = description;\n else if (name === \"_cmp\") {\n // Consent value used by consent-tracking-api and privacy-banner scripts.\n consent = description;\n }\n\n if (uniqueToken && visitToken && consent) break;\n }\n }\n\n return uniqueToken && visitToken && (isConsentRequired ? consent : true)\n ? { uniqueToken, visitToken, consent }\n : undefined;\n}\n"],"mappings":";AAYA,MAAM,uBAA2D,EAAE,SAAS,MAAM;AAElF,SAAgB,oBAAoC;CAIlD,IAAI;AAEJ,KAAI,OAAO,WAAW,eAAe,OAAO,OAAO,gBAAgB,YACjE,KAAI;EAEF,MAAM,aACJ;EAKF,MAAM,UAAU,YAAY,iBAAiB,WAAW;EAExD,IAAI;AAEJ,OAAK,IAAI,IAAI,QAAQ,SAAS,GAAG,KAAK,GAAG,KAAK;GAC5C,MAAM,QAAQ,QAAQ;AACtB,OAAI,MAAM,kBAAkB,QAAS;GAErC,MAAM,cAAc,OAAO,SAAS;GACpC,MAAM,QAAQ,MAAM,KAAK,MAAM,WAAW;AAC1C,OAAI,CAAC,MAAO;GAEZ,MAAM,GAAG,aAAa,aAAa;AAInC,OAFE,gBAAgB,eAAgB,aAAa,aAAa,SAAS,IAAI,cAAc,EAE1E;IACX,MAAM,SAAS,4BAA4B,MAAM;AACjD,QAAI,QAAQ;AACV,qBAAgB;AAChB;;;;AAKN,MAAI,cACF,kBAAiB;AAKnB,MAAI,eACF,sBAAqB,UAAU;WACtB,qBAAqB,QAC9B,kBAAiB,qBAAqB;AAGxC,MAAI,CAAC,gBAAgB;GAGnB,MAAM,oBAAoB,YAAY,iBACpC,aACD,CAAC;AACF,oBAAiB,4BAA4B,mBAAmB,MAAM;;SAElE;AAGV,KAAI,CAAC,gBAAgB;EACnB,MAAM,SAAS,OAAO,aAAa,cAAc,SAAS,SAAS;AAEnE,mBAAiB;GACf,aAAa,OAAO,MAAM,uBAAuB,GAAG,MAAM;GAC1D,YAAY,OAAO,MAAM,uBAAuB,GAAG,MAAM;GACzD,SAAS,OAAO,MAAM,8BAA8B,GAAG,MAAM;GAC9D;;AAGH,QAAO;;AAGT,SAAS,4BACP,OACA,oBAAoB,MACQ;CAC5B,IAAI,cAAc;CAClB,IAAI,aAAa;CACjB,IAAI,UAAU;CAEd,MAAM,eAAe,MAAM;CAC3B,MAAM,iBAAiB,oBAAoB,IAAI;AAC/C,KAAI,gBAAgB,aAAa,UAAU,eAGzC,MAAK,IAAI,IAAI,aAAa,SAAS,GAAG,KAAK,GAAG,KAAK;EACjD,MAAM,EAAE,MAAM,gBAAgB,aAAa;AAC3C,MAAI,CAAC,QAAQ,CAAC,YAAa;AAE3B,MAAI,SAAS,KAAM,eAAc;WACxB,SAAS,KAAM,cAAa;WAC5B,SAAS,OAEhB,WAAU;AAGZ,MAAI,eAAe,cAAc,QAAS;;AAI9C,QAAO,eAAe,eAAe,oBAAoB,UAAU,QAC/D;EAAE;EAAa;EAAY;EAAS,GACpC"}
|