@shopify/hydrogen 2026.10.0-preview.2 → 2026.10.0-preview.3
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 +32 -1
- package/dist/cli/index.mjs +464 -73
- package/dist/core/cart/actions.mjs +2 -0
- package/dist/core/cart/actions.mjs.map +1 -1
- package/dist/core/cart/cart.mjs +16 -6
- package/dist/core/cart/cart.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/request-context.d.mts +6 -0
- package/dist/core/request-context.mjs +6 -2
- package/dist/core/request-context.mjs.map +1 -1
- package/dist/core/request-routing/handle-shopify-routes.mjs +2 -0
- package/dist/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/checkout.mjs +4 -1
- package/dist/core/request-routing/interceptors/checkout.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/proxy.mjs +19 -8
- package/dist/core/request-routing/interceptors/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/shopify-scripts/analytics.mjs +1 -1
- package/dist/core/shopify-scripts/analytics.mjs.map +1 -1
- package/dist/core/shopify-scripts/constants.mjs +3 -1
- package/dist/core/shopify-scripts/constants.mjs.map +1 -1
- package/dist/core/shopify-scripts/deprecated-cookies.mjs +2 -1
- package/dist/core/shopify-scripts/deprecated-cookies.mjs.map +1 -1
- package/dist/core/shopify-scripts/global.mjs +5 -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/types.d.mts +5 -0
- package/dist/core/url.mjs +2 -1
- package/dist/core/url.mjs.map +1 -1
- package/dist/customer-account/client.mjs +1 -1
- package/dist/customer-account/session.mjs +1 -1
- package/dist/development/core/cart/actions.mjs +2 -0
- package/dist/development/core/cart/actions.mjs.map +1 -1
- package/dist/development/core/cart/cart.mjs +16 -6
- package/dist/development/core/cart/cart.mjs.map +1 -1
- package/dist/development/core/cart/queries.d.mts +57 -57
- package/dist/development/core/predictive-search/queries.d.mts +2 -2
- 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/request-context.d.mts +6 -0
- package/dist/development/core/request-context.mjs +6 -2
- package/dist/development/core/request-context.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-routes.mjs +2 -0
- package/dist/development/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/checkout.mjs +4 -1
- package/dist/development/core/request-routing/interceptors/checkout.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/proxy.mjs +19 -8
- package/dist/development/core/request-routing/interceptors/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/shopify-scripts/analytics.mjs +1 -1
- package/dist/development/core/shopify-scripts/analytics.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/constants.mjs +3 -1
- package/dist/development/core/shopify-scripts/constants.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/deprecated-cookies.mjs +2 -1
- package/dist/development/core/shopify-scripts/deprecated-cookies.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/global.mjs +5 -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/types.d.mts +5 -0
- package/dist/development/core/url.mjs +2 -1
- package/dist/development/core/url.mjs.map +1 -1
- package/dist/development/globals.d.mts +4 -1
- package/dist/development/react/index.d.mts +2 -2
- package/dist/development/vue/shopify-scripts.d.mts +9 -0
- package/dist/development/vue/shopify-scripts.mjs +5 -0
- package/dist/development/vue/shopify-scripts.mjs.map +1 -1
- package/dist/globals.d.mts +4 -1
- package/dist/react/index.d.mts +2 -2
- package/dist/vue/shopify-scripts.d.mts +10 -1
- package/dist/vue/shopify-scripts.mjs +5 -0
- package/dist/vue/shopify-scripts.mjs.map +1 -1
- package/package.json +1 -1
- package/skills/hydrogen-cart-metafields/SKILL.md +133 -0
- package/skills/hydrogen-cart-ui/SKILL.md +2 -1
- package/skills/hydrogen-setup/steps/11-verify.md +1 -0
- package/skills/hydrogen-setup/steps/2-scaffold.md +21 -0
- package/skills/hydrogen-storefront-client/SKILL.md +4 -0
- package/skills/hydrogen-variant-form/SKILL.md +10 -4
- package/dist/core/shopify-scripts/consent.mjs +0 -17
- package/dist/core/shopify-scripts/consent.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
|
@@ -73,10 +73,11 @@ function getShopifyGlobalBootstrapScript({ i18n, shop }) {
|
|
|
73
73
|
apiProxyPrefix: SHOPIFY_API_PROXY_PREFIX
|
|
74
74
|
},
|
|
75
75
|
shop: normalizeMyshopifyDomain(shop.myshopifyDomain),
|
|
76
|
-
customerPrivacy: {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
customerPrivacy: { config: {
|
|
77
|
+
isHeadless: true,
|
|
78
|
+
asyncConsent: true,
|
|
79
|
+
asyncVisitorState: true
|
|
80
|
+
} }
|
|
80
81
|
};
|
|
81
82
|
return asInlineScript(initializeShopifyGlobal)(config);
|
|
82
83
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"global.mjs","names":[],"sources":["../../../src/core/shopify-scripts/global.ts"],"sourcesContent":["import type { ShopifyGlobal } from \"../../globals\";\nimport type { I18nConfig } from \"../request-context\";\nimport {\n matchStandardRouteUrl,\n resolveStandardRouteUrl,\n type ShopifyRouteTemplates,\n} from \"../standard-routes/index\";\nimport { SHOPIFY_API_PROXY_PREFIX, isHydrogenServerHandoffPath } from \"../url\";\nimport initializeShopifyGlobal from \"./global-script\" with { type: \"script\" };\nimport type { ShopifyScriptsI18n } from \"./types\";\nimport type { ShopifyScriptsShop } from \"./types\";\nimport { asInlineScript } from \"./utils/inline-script\";\n\nexport type ShopifyGlobalConfig = {\n country: I18nConfig[\"country\"];\n currency?: NonNullable<ShopifyGlobal[\"currency\"]>;\n locale: Lowercase<I18nConfig[\"language\"]>;\n customerPrivacy: {\n config: NonNullable<ShopifyGlobal[\"customerPrivacy\"][\"config\"]>;\n
|
|
1
|
+
{"version":3,"file":"global.mjs","names":[],"sources":["../../../src/core/shopify-scripts/global.ts"],"sourcesContent":["import type { ShopifyGlobal } from \"../../globals\";\nimport type { I18nConfig } from \"../request-context\";\nimport {\n matchStandardRouteUrl,\n resolveStandardRouteUrl,\n type ShopifyRouteTemplates,\n} from \"../standard-routes/index\";\nimport { SHOPIFY_API_PROXY_PREFIX, isHydrogenServerHandoffPath } from \"../url\";\nimport initializeShopifyGlobal from \"./global-script\" with { type: \"script\" };\nimport type { ShopifyScriptsI18n } from \"./types\";\nimport type { ShopifyScriptsShop } from \"./types\";\nimport { asInlineScript } from \"./utils/inline-script\";\n\nexport type ShopifyGlobalConfig = {\n country: I18nConfig[\"country\"];\n currency?: NonNullable<ShopifyGlobal[\"currency\"]>;\n locale: Lowercase<I18nConfig[\"language\"]>;\n customerPrivacy: {\n config: NonNullable<ShopifyGlobal[\"customerPrivacy\"][\"config\"]>;\n };\n routes: {\n root: string;\n apiProxyPrefix: string;\n };\n shop: string;\n};\n\nexport type IncompleteCustomerPrivacy = Partial<\n Omit<ShopifyGlobal[\"customerPrivacy\"], \"config\">\n> & {\n config?: Partial<NonNullable<ShopifyGlobal[\"customerPrivacy\"][\"config\"]>>;\n};\n\nexport type IncompleteShopifyGlobal = Partial<Omit<ShopifyGlobal, \"customerPrivacy\" | \"routes\">> & {\n customerPrivacy?: IncompleteCustomerPrivacy;\n routes?: Partial<ShopifyGlobal[\"routes\"]> & Record<string, unknown>;\n [key: string]: unknown;\n};\n\nconst DEFAULT_COUNTRY: ShopifyGlobalConfig[\"country\"] = \"US\";\nconst DEFAULT_LOCALE: ShopifyGlobalConfig[\"locale\"] = \"en\";\nconst DEFAULT_ROUTES_ROOT: ShopifyGlobalConfig[\"routes\"][\"root\"] = \"/\";\n\n/**\n * Returns the browser's `window.Shopify` object, creating it when needed.\n *\n * This is SSR-safe and returns `undefined` outside the browser.\n */\nexport function getShopifyGlobal(): IncompleteShopifyGlobal | undefined {\n if (typeof window === \"undefined\") return undefined;\n\n const shopifyWindow: { Shopify?: IncompleteShopifyGlobal } = window;\n return (shopifyWindow.Shopify ??= {});\n}\n\n/**\n * Configures Shopify runtime routing hooks used by storefront scripts.\n *\n * This is SSR-safe and no-ops outside the browser.\n */\nexport function configureShopifyRouting({\n navigate,\n routes,\n}: {\n navigate?: ShopifyGlobal[\"routes\"][\"navigate\"];\n routes?: ShopifyRouteTemplates;\n}) {\n const shopify = getShopifyGlobal();\n if (!shopify) return;\n\n const getRouteOptions = (url: string) => ({\n baseUrl: window.location.href,\n pathPrefix: shopify.routes?.root,\n routeTemplates: routes ?? {},\n url,\n });\n\n shopify.routes ??= {};\n shopify.routes.match = (url) => matchStandardRouteUrl(getRouteOptions(url));\n shopify.routes.resolve = (url) => resolveStandardRouteUrl(getRouteOptions(url));\n\n const navigateTo = navigate ?? documentNavigate;\n shopify.routes.navigate = (url) => {\n if (shouldHandoff(url)) {\n return documentNavigate(url);\n }\n\n return navigateTo(shopify.routes?.resolve?.(url) ?? url);\n };\n // Deprecated, kept for temporary backwards compat with WebMCP\n shopify.navigate = shopify.routes.navigate;\n}\n\nfunction documentNavigate(url: string): void {\n window.location.assign(url);\n}\n\nfunction shouldHandoff(url: string): boolean {\n try {\n const parsedUrl = new URL(url, window.location.href);\n\n return (\n parsedUrl.origin === window.location.origin && isHydrogenServerHandoffPath(parsedUrl.pathname)\n );\n } catch {\n return false;\n }\n}\n\nfunction getShopifyRoutesRoot(pathPrefix: I18nConfig[\"pathPrefix\"]): string {\n const normalizedPathPrefix = pathPrefix?.trim().replace(/^\\/+/, \"\").replace(/\\/+$/, \"\") ?? \"\";\n\n return normalizedPathPrefix ? `/${normalizedPathPrefix}/` : DEFAULT_ROUTES_ROOT;\n}\n\nfunction normalizeMyshopifyDomain(domain: string): string {\n return domain\n .trim()\n .replace(/^https?:\\/\\//i, \"\")\n .replace(/\\/+$/, \"\");\n}\n\n/**\n * Builds the inline bootstrap script that initializes `window.Shopify` for storefront scripts.\n */\nexport function getShopifyGlobalBootstrapScript({\n i18n,\n shop,\n}: {\n i18n?: ShopifyScriptsI18n;\n shop: ShopifyScriptsShop;\n}): string {\n const config: ShopifyGlobalConfig = {\n country: i18n?.country ?? DEFAULT_COUNTRY,\n ...(i18n?.currency !== undefined ? { currency: { active: i18n.currency.toUpperCase() } } : {}),\n // oxlint-disable-next-line typescript/consistent-type-assertions\n locale: (i18n?.language ?? DEFAULT_LOCALE).toLowerCase() as Lowercase<\n ShopifyGlobalConfig[\"locale\"]\n >,\n routes: {\n root: getShopifyRoutesRoot(i18n?.pathPrefix),\n apiProxyPrefix: SHOPIFY_API_PROXY_PREFIX,\n },\n shop: normalizeMyshopifyDomain(shop.myshopifyDomain),\n customerPrivacy: {\n config: {\n isHeadless: true,\n asyncConsent: true,\n asyncVisitorState: true,\n },\n },\n };\n\n return asInlineScript(initializeShopifyGlobal)(config);\n}\n"],"mappings":";;;;;;AAQA,MAAM,2BAAyB,eAAe,mBAAsB,sUAAS,CAAA,QAAA,kCAAA,KAAA,UAAA,WAAA,IAAA,aAAA,QAAA,MAAA,UAAA,CAAA,QAAA,WAAA,UAAA,CAAA,QAAA,WAAA,UAAA,CAAA;AA+B7E,MAAM,kBAAkD;AACxD,MAAM,iBAAgD;AACtD,MAAM,sBAA6D;;;;;;AAOnE,SAAgB,mBAAwD;AACtE,KAAI,OAAO,WAAW,YAAa,QAAO;CAE1C,MAAM,gBAAuD;AAC7D,QAAQ,cAAc,YAAY,EAAE;;;;;;;AAQtC,SAAgB,wBAAwB,EACtC,UACA,UAIC;CACD,MAAM,UAAU,kBAAkB;AAClC,KAAI,CAAC,QAAS;CAEd,MAAM,mBAAmB,SAAiB;EACxC,SAAS,OAAO,SAAS;EACzB,YAAY,QAAQ,QAAQ;EAC5B,gBAAgB,UAAU,EAAE;EAC5B;EACD;AAED,SAAQ,WAAW,EAAE;AACrB,SAAQ,OAAO,SAAS,QAAQ,sBAAsB,gBAAgB,IAAI,CAAC;AAC3E,SAAQ,OAAO,WAAW,QAAQ,wBAAwB,gBAAgB,IAAI,CAAC;CAE/E,MAAM,aAAa,YAAY;AAC/B,SAAQ,OAAO,YAAY,QAAQ;AACjC,MAAI,cAAc,IAAI,CACpB,QAAO,iBAAiB,IAAI;AAG9B,SAAO,WAAW,QAAQ,QAAQ,UAAU,IAAI,IAAI,IAAI;;AAG1D,SAAQ,WAAW,QAAQ,OAAO;;AAGpC,SAAS,iBAAiB,KAAmB;AAC3C,QAAO,SAAS,OAAO,IAAI;;AAG7B,SAAS,cAAc,KAAsB;AAC3C,KAAI;EACF,MAAM,YAAY,IAAI,IAAI,KAAK,OAAO,SAAS,KAAK;AAEpD,SACE,UAAU,WAAW,OAAO,SAAS,UAAU,4BAA4B,UAAU,SAAQ;SAEzF;AACN,SAAO;;;AAIX,SAAS,qBAAqB,YAA8C;CAC1E,MAAM,uBAAuB,YAAY,MAAM,CAAC,QAAQ,QAAQ,GAAG,CAAC,QAAQ,QAAQ,GAAG,IAAI;AAE3F,QAAO,uBAAuB,IAAI,qBAAqB,KAAK;;AAG9D,SAAS,yBAAyB,QAAwB;AACxD,QAAO,OACJ,MAAK,CACL,QAAQ,iBAAiB,GAAE,CAC3B,QAAQ,QAAQ,GAAG;;;;;AAMxB,SAAgB,gCAAgC,EAC9C,MACA,QAIS;CACT,MAAM,SAA8B;EAClC,SAAS,MAAM,WAAW;EAC1B,GAAI,MAAM,aAAa,SAAY,EAAE,UAAU,EAAE,QAAQ,KAAK,SAAS,aAAa,EAAE,EAAE,GAAG,EAAE;EAE7F,SAAS,MAAM,YAAY,gBAAgB,aAAa;EAGxD,QAAQ;GACN,MAAM,qBAAqB,MAAM,WAAW;GAC5C,gBAAgB;GACjB;EACD,MAAM,yBAAyB,KAAK,gBAAgB;EACpD,iBAAiB,EACf,QAAQ;GACN,YAAY;GACZ,cAAc;GACd,mBAAmB;GACpB,EACF;EACF;AAED,QAAO,eAAe,wBAAwB,CAAC,OAAO"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { getShopifyAnalyticsBusScript, getShopifyAnalyticsConfig } from "./analytics.mjs";
|
|
2
|
-
import { SHOPIFY_CDN_ORIGIN, SHOPIFY_CONSENT_API_SCRIPT, SHOPIFY_CONSENT_SCRIPT_ID, SHOPIFY_INBOX_SCRIPT, SHOPIFY_PERF_KIT_SCRIPT, SHOPIFY_PRIVACY_BANNER_SCRIPT, SHOPIFY_SHOP_APP_ORIGIN, SHOPIFY_STOREFRONT_ANALYTICS_SCRIPT, SHOPIFY_STOREFRONT_STANDARD_ACTIONS_SCRIPT, SHOPIFY_STOREFRONT_STANDARD_EVENTS_SCRIPT, SHOPIFY_STOREFRONT_WEBMCP_SCRIPT, VISITOR_CONSENT_COLLECTED_EVENT } from "./constants.mjs";
|
|
3
|
-
import { getShopifyConsentTrackingScript } from "./consent.mjs";
|
|
2
|
+
import { CONSENT_TRACKING_API_LOADED_EVENT, SHOPIFY_ACCOUNT_SCRIPT, SHOPIFY_CDN_ORIGIN, SHOPIFY_CONSENT_API_SCRIPT, SHOPIFY_CONSENT_SCRIPT_ID, SHOPIFY_INBOX_SCRIPT, SHOPIFY_PERF_KIT_SCRIPT, SHOPIFY_PRIVACY_BANNER_SCRIPT, SHOPIFY_SHOP_APP_ORIGIN, SHOPIFY_STOREFRONT_ANALYTICS_SCRIPT, SHOPIFY_STOREFRONT_STANDARD_ACTIONS_SCRIPT, SHOPIFY_STOREFRONT_STANDARD_EVENTS_SCRIPT, SHOPIFY_STOREFRONT_WEBMCP_SCRIPT, VISITOR_CONSENT_COLLECTED_EVENT } from "./constants.mjs";
|
|
4
3
|
import { getShopifyGlobal, getShopifyGlobalBootstrapScript } from "./global.mjs";
|
|
5
4
|
import { getPerfKitScript } from "./perfkit.mjs";
|
|
6
5
|
import { renderShopifyScriptTag } from "./render.mjs";
|
|
@@ -14,7 +13,7 @@ import { initializeShopifyScripts } from "./initialize.mjs";
|
|
|
14
13
|
* component. Frameworks without a binding can render these descriptors during SSR and call
|
|
15
14
|
* `initializeShopifyScripts()` during browser hydration.
|
|
16
15
|
*/
|
|
17
|
-
function getShopifyScriptTags({ analytics, consent, debug, i18n, nonce, shop, shopifyAnalytics = true, inbox = false }) {
|
|
16
|
+
function getShopifyScriptTags({ account = false, analytics, consent, debug, i18n, nonce, shop, shopifyAnalytics = true, inbox = false }) {
|
|
18
17
|
const nonceAttributes = nonce !== void 0 ? { nonce } : void 0;
|
|
19
18
|
const analyticsConfig = getShopifyAnalyticsConfig({
|
|
20
19
|
analytics,
|
|
@@ -77,31 +76,34 @@ function getShopifyScriptTags({ analytics, consent, debug, i18n, nonce, shop, sh
|
|
|
77
76
|
src: SHOPIFY_INBOX_SCRIPT
|
|
78
77
|
}
|
|
79
78
|
});
|
|
80
|
-
scripts.push({
|
|
79
|
+
if (account) scripts.push({
|
|
81
80
|
tagName: "script",
|
|
82
81
|
attributes: {
|
|
83
|
-
id:
|
|
82
|
+
id: "shopify-account",
|
|
83
|
+
type: "module",
|
|
84
84
|
async: true,
|
|
85
85
|
crossorigin: "anonymous",
|
|
86
86
|
...nonceAttributes,
|
|
87
|
-
src:
|
|
87
|
+
src: SHOPIFY_ACCOUNT_SCRIPT
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
90
|
scripts.push({
|
|
91
91
|
tagName: "script",
|
|
92
92
|
attributes: {
|
|
93
|
-
id: "shopify-
|
|
93
|
+
id: "shopify-analytics-bus",
|
|
94
94
|
...nonceAttributes
|
|
95
95
|
},
|
|
96
|
-
innerHTML:
|
|
96
|
+
innerHTML: getShopifyAnalyticsBusScript(analyticsConfig)
|
|
97
97
|
});
|
|
98
98
|
scripts.push({
|
|
99
99
|
tagName: "script",
|
|
100
100
|
attributes: {
|
|
101
|
-
id:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
id: SHOPIFY_CONSENT_SCRIPT_ID,
|
|
102
|
+
async: true,
|
|
103
|
+
crossorigin: "anonymous",
|
|
104
|
+
...nonceAttributes,
|
|
105
|
+
src: consent?.mode === "default-banner" ? SHOPIFY_PRIVACY_BANNER_SCRIPT : SHOPIFY_CONSENT_API_SCRIPT
|
|
106
|
+
}
|
|
105
107
|
});
|
|
106
108
|
if (shopifyAnalytics) scripts.push({
|
|
107
109
|
tagName: "script",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/core/shopify-scripts/index.ts"],"sourcesContent":["import { getShopifyAnalyticsBusScript, getShopifyAnalyticsConfig } from \"./analytics\";\nimport {
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/core/shopify-scripts/index.ts"],"sourcesContent":["import { getShopifyAnalyticsBusScript, getShopifyAnalyticsConfig } from \"./analytics\";\nimport {\n SHOPIFY_ACCOUNT_SCRIPT,\n SHOPIFY_CONSENT_API_SCRIPT,\n SHOPIFY_CDN_ORIGIN,\n SHOPIFY_CONSENT_SCRIPT_ID,\n SHOPIFY_PRIVACY_BANNER_SCRIPT,\n SHOPIFY_SHOP_APP_ORIGIN,\n SHOPIFY_INBOX_SCRIPT,\n SHOPIFY_STOREFRONT_ANALYTICS_SCRIPT,\n SHOPIFY_STOREFRONT_STANDARD_ACTIONS_SCRIPT,\n SHOPIFY_STOREFRONT_STANDARD_EVENTS_INSPECTOR_SCRIPT,\n SHOPIFY_STOREFRONT_STANDARD_EVENTS_SCRIPT,\n} from \"./constants\";\nimport { getShopifyGlobalBootstrapScript } from \"./global\";\nimport { getPerfKitScript } from \"./perfkit\";\nimport { renderShopifyScriptTag } from \"./render\";\nimport type {\n ShopifyLinkDescriptor,\n ShopifyScriptDescriptor,\n ShopifyScriptTagDescriptors,\n ShopifyScriptTagsOptions,\n} from \"./types\";\n\nexport {\n CONSENT_TRACKING_API_LOADED_EVENT,\n SHOPIFY_ACCOUNT_SCRIPT,\n SHOPIFY_CDN_ORIGIN,\n SHOPIFY_CONSENT_API_SCRIPT,\n SHOPIFY_PERF_KIT_SCRIPT,\n SHOPIFY_PRIVACY_BANNER_SCRIPT,\n SHOPIFY_SHOP_APP_ORIGIN,\n SHOPIFY_INBOX_SCRIPT,\n SHOPIFY_STOREFRONT_ANALYTICS_SCRIPT,\n SHOPIFY_STOREFRONT_STANDARD_ACTIONS_SCRIPT,\n SHOPIFY_STOREFRONT_STANDARD_EVENTS_SCRIPT,\n SHOPIFY_STOREFRONT_WEBMCP_SCRIPT,\n VISITOR_CONSENT_COLLECTED_EVENT,\n} from \"./constants\";\nexport { getShopifyGlobal, getShopifyGlobalBootstrapScript } from \"./global\";\nexport { initializeShopifyScripts } from \"./initialize\";\nexport { renderShopifyScriptTag } from \"./render\";\nexport type {\n ShopifyScriptsAnalyticsConfig,\n ShopifyRoutesOptions,\n ShopifyScriptTagDescriptor,\n ShopifyScriptTagDescriptors,\n ShopifyScriptTagsOptions,\n ShopifyScriptsOptions,\n ShopifyScriptsI18n,\n ShopifyScriptsShop,\n} from \"./types\";\n\n/**\n * Returns grouped Shopify storefront script/link descriptors for SSR frameworks and bindings.\n *\n * Framework bindings wrap this with `initializeShopifyScripts()` to form their `ShopifyScripts`\n * component. Frameworks without a binding can render these descriptors during SSR and call\n * `initializeShopifyScripts()` during browser hydration.\n */\n// oxlint-disable-next-line complexity -- ordered assembly of optional Shopify script tags; each flag adds one branch and splitting would obscure the required load order\nexport function getShopifyScriptTags({\n account = false,\n analytics,\n consent,\n debug,\n i18n,\n nonce,\n shop,\n shopifyAnalytics = true,\n inbox = false,\n}: ShopifyScriptTagsOptions): ShopifyScriptTagDescriptors {\n const nonceAttributes = nonce !== undefined ? { nonce } : undefined;\n const analyticsConfig = getShopifyAnalyticsConfig({ analytics, consent, shop });\n\n const links: ShopifyLinkDescriptor[] = [\n {\n tagName: \"link\",\n attributes: {\n rel: \"preconnect\",\n href: SHOPIFY_CDN_ORIGIN,\n },\n },\n {\n tagName: \"link\",\n attributes: {\n rel: \"preconnect\",\n href: SHOPIFY_SHOP_APP_ORIGIN,\n },\n },\n {\n tagName: \"link\",\n attributes: {\n rel: \"prefetch\",\n as: \"script\",\n href: SHOPIFY_STOREFRONT_STANDARD_EVENTS_SCRIPT,\n crossorigin: \"anonymous\",\n },\n },\n ];\n const scripts: ShopifyScriptDescriptor[] = [\n {\n tagName: \"script\",\n attributes: { id: \"shopify-global-bootstrap\", ...nonceAttributes },\n innerHTML: getShopifyGlobalBootstrapScript({ i18n, shop }),\n },\n {\n tagName: \"script\",\n attributes: {\n id: \"shopify-standard-actions\",\n type: \"module\",\n crossorigin: \"anonymous\",\n ...nonceAttributes,\n src: SHOPIFY_STOREFRONT_STANDARD_ACTIONS_SCRIPT,\n },\n },\n ];\n\n if (__DEV__ && debug?.standardEventsInspector) {\n scripts.push({\n tagName: \"script\",\n attributes: {\n id: \"shopify-standard-events-inspector\",\n defer: true,\n crossorigin: \"anonymous\",\n ...nonceAttributes,\n src: SHOPIFY_STOREFRONT_STANDARD_EVENTS_INSPECTOR_SCRIPT,\n },\n });\n }\n\n if (inbox) {\n scripts.push({\n tagName: \"script\",\n attributes: {\n id: \"shopify-inbox\",\n type: \"module\",\n async: true,\n crossorigin: \"anonymous\",\n ...nonceAttributes,\n src: SHOPIFY_INBOX_SCRIPT,\n },\n });\n }\n\n if (account) {\n scripts.push({\n tagName: \"script\",\n attributes: {\n id: \"shopify-account\",\n type: \"module\",\n async: true,\n crossorigin: \"anonymous\",\n ...nonceAttributes,\n src: SHOPIFY_ACCOUNT_SCRIPT,\n },\n });\n }\n\n // Install readiness listeners before loading the async consent library so a\n // cached script cannot finish initialization before Hydrogen starts listening.\n scripts.push({\n tagName: \"script\",\n attributes: { id: \"shopify-analytics-bus\", ...nonceAttributes },\n innerHTML: getShopifyAnalyticsBusScript(analyticsConfig),\n });\n\n scripts.push({\n tagName: \"script\",\n attributes: {\n id: SHOPIFY_CONSENT_SCRIPT_ID,\n async: true,\n crossorigin: \"anonymous\",\n ...nonceAttributes,\n src:\n consent?.mode === \"default-banner\"\n ? SHOPIFY_PRIVACY_BANNER_SCRIPT\n : SHOPIFY_CONSENT_API_SCRIPT,\n },\n });\n\n if (shopifyAnalytics) {\n scripts.push({\n tagName: \"script\",\n attributes: {\n id: \"shopify-storefront-analytics\",\n async: true,\n crossorigin: \"anonymous\",\n ...nonceAttributes,\n src: SHOPIFY_STOREFRONT_ANALYTICS_SCRIPT,\n },\n });\n }\n\n const perfKitScript = getPerfKitScript(shop, nonceAttributes);\n if (perfKitScript) {\n scripts.push(perfKitScript);\n }\n\n return {\n links,\n scripts,\n get tags() {\n return [...links, ...scripts];\n },\n };\n}\n\n/**\n * Renders all Shopify storefront script/link descriptors to HTML strings.\n */\nexport function renderShopifyScriptTags(options: ShopifyScriptTagsOptions): string[] {\n return getShopifyScriptTags(options).tags.map(renderShopifyScriptTag);\n}\n"],"mappings":";;;;;;;;;;;;;;;AA6DA,SAAgB,qBAAqB,EACnC,UAAU,OACV,WACA,SACA,OACA,MACA,OACA,MACA,mBAAmB,MACnB,QAAQ,SACgD;CACxD,MAAM,kBAAkB,UAAU,SAAY,EAAE,OAAO,GAAG;CAC1D,MAAM,kBAAkB,0BAA0B;EAAE;EAAW;EAAS;EAAM,CAAC;CAE/E,MAAM,QAAiC;EACrC;GACE,SAAS;GACT,YAAY;IACV,KAAK;IACL,MAAM;IACP;GACF;EACD;GACE,SAAS;GACT,YAAY;IACV,KAAK;IACL,MAAM;IACP;GACF;EACD;GACE,SAAS;GACT,YAAY;IACV,KAAK;IACL,IAAI;IACJ,MAAM;IACN,aAAa;IACd;GACF;EACF;CACD,MAAM,UAAqC,CACzC;EACE,SAAS;EACT,YAAY;GAAE,IAAI;GAA4B,GAAG;GAAiB;EAClE,WAAW,gCAAgC;GAAE;GAAM;GAAM,CAAC;EAC3D,EACD;EACE,SAAS;EACT,YAAY;GACV,IAAI;GACJ,MAAM;GACN,aAAa;GACb,GAAG;GACH,KAAK;GACN;EACF,CACF;AAeD,KAAI,MACF,SAAQ,KAAK;EACX,SAAS;EACT,YAAY;GACV,IAAI;GACJ,MAAM;GACN,OAAO;GACP,aAAa;GACb,GAAG;GACH,KAAK;GACN;EACF,CAAC;AAGJ,KAAI,QACF,SAAQ,KAAK;EACX,SAAS;EACT,YAAY;GACV,IAAI;GACJ,MAAM;GACN,OAAO;GACP,aAAa;GACb,GAAG;GACH,KAAK;GACN;EACF,CAAC;AAKJ,SAAQ,KAAK;EACX,SAAS;EACT,YAAY;GAAE,IAAI;GAAyB,GAAG;GAAiB;EAC/D,WAAW,6BAA6B,gBAAgB;EACzD,CAAC;AAEF,SAAQ,KAAK;EACX,SAAS;EACT,YAAY;GACV,IAAI;GACJ,OAAO;GACP,aAAa;GACb,GAAG;GACH,KACE,SAAS,SAAS,mBACd,gCACA;GACP;EACF,CAAC;AAEF,KAAI,iBACF,SAAQ,KAAK;EACX,SAAS;EACT,YAAY;GACV,IAAI;GACJ,OAAO;GACP,aAAa;GACb,GAAG;GACH,KAAK;GACN;EACF,CAAC;CAGJ,MAAM,gBAAgB,iBAAiB,MAAM,gBAAgB;AAC7D,KAAI,cACF,SAAQ,KAAK,cAAc;AAG7B,QAAO;EACL;EACA;EACA,IAAI,OAAO;AACT,UAAO,CAAC,GAAG,OAAO,GAAG,QAAQ;;EAEhC;;;;;AAMH,SAAgB,wBAAwB,SAA6C;AACnF,QAAO,qBAAqB,QAAQ,CAAC,KAAK,IAAI,uBAAuB"}
|
|
@@ -52,6 +52,11 @@ type ShopifyScriptsShop = {
|
|
|
52
52
|
myshopifyDomain: string;
|
|
53
53
|
};
|
|
54
54
|
type ShopifyScriptTagsOptions = {
|
|
55
|
+
/**
|
|
56
|
+
* Loads the customer account component. Render `<shopify-account>` where you want the account UI to appear.
|
|
57
|
+
* @see {@link https://shopify.dev/docs/api/storefront-web-components/components/shopify-account | shopify-account}
|
|
58
|
+
*/
|
|
59
|
+
account?: boolean;
|
|
55
60
|
analytics?: ShopifyScriptsAnalyticsConfig;
|
|
56
61
|
consent?: ConsentConfig;
|
|
57
62
|
debug?: {
|
package/dist/core/url.mjs
CHANGED
|
@@ -3,6 +3,7 @@ const SFAPI_RE = /^\/api\/(unstable|2\d{3}-\d{2})\/graphql\.json$/;
|
|
|
3
3
|
const SHOPIFY_API_PROXY_PREFIX = "/__shopify";
|
|
4
4
|
const SHOPIFY_API_PROXY_RE = /^\/__shopify(?:\/|$)/;
|
|
5
5
|
const MCP_RE = /^\/api\/mcp$/;
|
|
6
|
+
const UCP_MCP_RE = /^\/api\/ucp\/mcp$/;
|
|
6
7
|
const CHECKOUT_RE = /^\/checkout$/;
|
|
7
8
|
const CART_PERMALINK_RE = /^\/cart\/\d+:\d+(?:,\d+:\d+)*$/;
|
|
8
9
|
const CUSTOMER_ACCOUNT_PATHS = {
|
|
@@ -38,5 +39,5 @@ function normalizeStoreDomain(domain) {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
//#endregion
|
|
41
|
-
export { AJAX_CART_RE, CHECKOUT_RE, CUSTOMER_ACCOUNT_PATHS, MCP_RE, SFAPI_RE, SHOPIFY_API_PROXY_PREFIX, SHOPIFY_API_PROXY_RE, WELL_KNOWN_RE, isHydrogenServerHandoffPath, normalizeStoreDomain };
|
|
42
|
+
export { AJAX_CART_RE, CHECKOUT_RE, CUSTOMER_ACCOUNT_PATHS, MCP_RE, SFAPI_RE, SHOPIFY_API_PROXY_PREFIX, SHOPIFY_API_PROXY_RE, UCP_MCP_RE, WELL_KNOWN_RE, isHydrogenServerHandoffPath, normalizeStoreDomain };
|
|
42
43
|
//# sourceMappingURL=url.mjs.map
|
package/dist/core/url.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"url.mjs","names":[],"sources":["../../src/core/url.ts"],"sourcesContent":["export const SFAPI_RE = /^\\/api\\/(unstable|2\\d{3}-\\d{2})\\/graphql\\.json$/;\nexport const SHOPIFY_API_PROXY_PREFIX = \"/__shopify\";\nexport const SHOPIFY_API_PROXY_RE = /^\\/__shopify(?:\\/|$)/;\nexport const MCP_RE = /^\\/api\\/mcp$/;\nexport const CHECKOUT_RE = /^\\/checkout$/;\nexport const CART_PERMALINK_RE = /^\\/cart\\/\\d+:\\d+(?:,\\d+:\\d+)*$/;\nexport const CUSTOMER_ACCOUNT_PATHS = {\n authorize: \"/account/authorize\",\n login: \"/account/login\",\n logout: \"/account/logout\",\n refresh: \"/account/refresh\",\n} as const;\n\nconst CUSTOMER_ACCOUNT_HANDOFF_PATHS: ReadonlySet<string> = new Set(\n Object.values(CUSTOMER_ACCOUNT_PATHS),\n);\n\n/**\n * Returns whether the pathname conventionally represents a Hydrogen document-level server handoff.\n *\n * Keep this aligned with the routes intercepted before framework routing. API and protocol\n * interceptors are intentionally excluded because they are not browser navigation destinations.\n */\nexport function isHydrogenServerHandoffPath(pathname: string): boolean {\n return (\n CHECKOUT_RE.test(pathname) ||\n CART_PERMALINK_RE.test(pathname) ||\n CUSTOMER_ACCOUNT_HANDOFF_PATHS.has(pathname)\n );\n}\n\n/**\n * Allowlisted `.well-known` resources proxied to the Online Store origin:\n * - `apple-developer-merchantid-domain-association` — Apple Pay domain verification.\n * - `shopify/fec/produce` — Frontend Event Collector ingress. On the Online\n * Store the myshopify.com edge forwards this first-party path to the\n * collector; headless storefronts have no such edge, so Hydrogen proxies it\n * to the same origin (used by WebMCP's Event Refinery client).\n */\nexport const WELL_KNOWN_RE =\n /^\\/\\.well-known\\/(?:apple-developer-merchantid-domain-association|shopify\\/fec\\/produce)$/;\nexport const AJAX_CART_RE =\n /^(?:\\/[a-z]{2}(?:-[a-z]{2})?)?\\/cart(?:\\.(?:js|json)|\\/(?:add|update|change|clear)(?:\\.(?:js|json))?)$/i;\n\nexport function normalizeStoreDomain(domain: string): string {\n if (!domain) {\n throw new Error(\"Storefront `storeDomain` is required.\");\n }\n\n if (domain.startsWith(\"http://\") || domain.startsWith(\"https://\")) {\n return domain.replace(/\\/+$/, \"\");\n }\n return `https://${domain}`.replace(/\\/+$/, \"\");\n}\n"],"mappings":";AAAA,MAAa,WAAW;AACxB,MAAa,2BAA2B;AACxC,MAAa,uBAAuB;AACpC,MAAa,SAAS;AACtB,MAAa,cAAc;AAC3B,MAAa,oBAAoB;AACjC,MAAa,yBAAyB;CACpC,WAAW;CACX,OAAO;CACP,QAAQ;CACR,SAAS;CACV;AAED,MAAM,iCAAsD,IAAI,IAC9D,OAAO,OAAO,uBAAuB,CACtC;;;;;;;AAQD,SAAgB,4BAA4B,UAA2B;AACrE,QACE,YAAY,KAAK,SAAS,IAC1B,kBAAkB,KAAK,SAAS,IAChC,+BAA+B,IAAI,SAAS;;;;;;;;;;AAYhD,MAAa,gBACX;AACF,MAAa,eACX;AAEF,SAAgB,qBAAqB,QAAwB;AAC3D,KAAI,CAAC,OACH,OAAM,IAAI,MAAM,wCAAwC;AAG1D,KAAI,OAAO,WAAW,UAAU,IAAI,OAAO,WAAW,WAAW,CAC/D,QAAO,OAAO,QAAQ,QAAQ,GAAG;AAEnC,QAAO,WAAW,SAAS,QAAQ,QAAQ,GAAG"}
|
|
1
|
+
{"version":3,"file":"url.mjs","names":[],"sources":["../../src/core/url.ts"],"sourcesContent":["export const SFAPI_RE = /^\\/api\\/(unstable|2\\d{3}-\\d{2})\\/graphql\\.json$/;\nexport const SHOPIFY_API_PROXY_PREFIX = \"/__shopify\";\nexport const SHOPIFY_API_PROXY_RE = /^\\/__shopify(?:\\/|$)/;\nexport const MCP_RE = /^\\/api\\/mcp$/;\nexport const UCP_MCP_RE = /^\\/api\\/ucp\\/mcp$/;\nexport const CHECKOUT_RE = /^\\/checkout$/;\nexport const CART_PERMALINK_RE = /^\\/cart\\/\\d+:\\d+(?:,\\d+:\\d+)*$/;\nexport const CUSTOMER_ACCOUNT_PATHS = {\n authorize: \"/account/authorize\",\n login: \"/account/login\",\n logout: \"/account/logout\",\n refresh: \"/account/refresh\",\n} as const;\n\nconst CUSTOMER_ACCOUNT_HANDOFF_PATHS: ReadonlySet<string> = new Set(\n Object.values(CUSTOMER_ACCOUNT_PATHS),\n);\n\n/**\n * Returns whether the pathname conventionally represents a Hydrogen document-level server handoff.\n *\n * Keep this aligned with the routes intercepted before framework routing. API and protocol\n * interceptors are intentionally excluded because they are not browser navigation destinations.\n */\nexport function isHydrogenServerHandoffPath(pathname: string): boolean {\n return (\n CHECKOUT_RE.test(pathname) ||\n CART_PERMALINK_RE.test(pathname) ||\n CUSTOMER_ACCOUNT_HANDOFF_PATHS.has(pathname)\n );\n}\n\n/**\n * Allowlisted `.well-known` resources proxied to the Online Store origin:\n * - `apple-developer-merchantid-domain-association` — Apple Pay domain verification.\n * - `shopify/fec/produce` — Frontend Event Collector ingress. On the Online\n * Store the myshopify.com edge forwards this first-party path to the\n * collector; headless storefronts have no such edge, so Hydrogen proxies it\n * to the same origin (used by WebMCP's Event Refinery client).\n */\nexport const WELL_KNOWN_RE =\n /^\\/\\.well-known\\/(?:apple-developer-merchantid-domain-association|shopify\\/fec\\/produce)$/;\nexport const AJAX_CART_RE =\n /^(?:\\/[a-z]{2}(?:-[a-z]{2})?)?\\/cart(?:\\.(?:js|json)|\\/(?:add|update|change|clear)(?:\\.(?:js|json))?)$/i;\n\nexport function normalizeStoreDomain(domain: string): string {\n if (!domain) {\n throw new Error(\"Storefront `storeDomain` is required.\");\n }\n\n if (domain.startsWith(\"http://\") || domain.startsWith(\"https://\")) {\n return domain.replace(/\\/+$/, \"\");\n }\n return `https://${domain}`.replace(/\\/+$/, \"\");\n}\n"],"mappings":";AAAA,MAAa,WAAW;AACxB,MAAa,2BAA2B;AACxC,MAAa,uBAAuB;AACpC,MAAa,SAAS;AACtB,MAAa,aAAa;AAC1B,MAAa,cAAc;AAC3B,MAAa,oBAAoB;AACjC,MAAa,yBAAyB;CACpC,WAAW;CACX,OAAO;CACP,QAAQ;CACR,SAAS;CACV;AAED,MAAM,iCAAsD,IAAI,IAC9D,OAAO,OAAO,uBAAuB,CACtC;;;;;;;AAQD,SAAgB,4BAA4B,UAA2B;AACrE,QACE,YAAY,KAAK,SAAS,IAC1B,kBAAkB,KAAK,SAAS,IAChC,+BAA+B,IAAI,SAAS;;;;;;;;;;AAYhD,MAAa,gBACX;AACF,MAAa,eACX;AAEF,SAAgB,qBAAqB,QAAwB;AAC3D,KAAI,CAAC,OACH,OAAM,IAAI,MAAM,wCAAwC;AAG1D,KAAI,OAAO,WAAW,UAAU,IAAI,OAAO,WAAW,WAAW,CAC/D,QAAO,OAAO,QAAQ,QAAQ,GAAG;AAEnC,QAAO,WAAW,SAAS,QAAQ,QAAQ,GAAG"}
|
|
@@ -15,7 +15,7 @@ const LOCAL_ORIGIN_HOSTS = new Set([
|
|
|
15
15
|
"::1",
|
|
16
16
|
"[::1]"
|
|
17
17
|
]);
|
|
18
|
-
const USER_AGENT = `Hydrogen 2026.10.0-preview.
|
|
18
|
+
const USER_AGENT = `Hydrogen 2026.10.0-preview.3`;
|
|
19
19
|
const CUSTOMER_ACCOUNT_GRAPHQL_PERSONALIZATION_REASON = "customer-account-graphql";
|
|
20
20
|
function createCustomerAccountClient({ shopId, customerApiVersion = CUSTOMER_ACCOUNT_API_VERSION, requestContext, fetch: customFetch, defaultTimeoutInMs = DEFAULT_TIMEOUT_IN_MS }) {
|
|
21
21
|
if (typeof document !== "undefined") throw new Error("Customer Account API tokens cannot be used in a browser context. Use this client from server or edge routes only.");
|
|
@@ -24,7 +24,7 @@ const AUTHORIZATION_CODE_GRANT_TYPE = "authorization_code";
|
|
|
24
24
|
const REFRESH_TOKEN_GRANT_TYPE = "refresh_token";
|
|
25
25
|
const CUSTOMER_ACCOUNT_SCOPE = "openid email customer-account-api:full";
|
|
26
26
|
const CODE_CHALLENGE_METHOD = "S256";
|
|
27
|
-
const USER_AGENT = `Hydrogen 2026.10.0-preview.
|
|
27
|
+
const USER_AGENT = `Hydrogen 2026.10.0-preview.3`;
|
|
28
28
|
const EXPIRY_BUFFER_IN_SECONDS = 120;
|
|
29
29
|
const PENDING_LOGIN_TTL_IN_MINUTES = 10;
|
|
30
30
|
const SECONDS_PER_MINUTE = 60;
|
|
@@ -182,6 +182,8 @@ function parseAddIntent(form, merchandiseId) {
|
|
|
182
182
|
quantity
|
|
183
183
|
};
|
|
184
184
|
if (sellingPlanId) line.sellingPlanId = sellingPlanId;
|
|
185
|
+
const rawAttributes = getCartAttributeFormEntries(form);
|
|
186
|
+
if (rawAttributes.length > 0) line.attributes = parseAttributes(rawAttributes);
|
|
185
187
|
return {
|
|
186
188
|
intent: "add",
|
|
187
189
|
lines: [line]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions.mjs","names":[],"sources":["../../../../src/core/cart/actions.ts"],"sourcesContent":["import { normalizeCartId } from \"./cookie\";\nimport { getCartAttributeFormEntries } from \"./form\";\n\nexport type CartAttributeInput = { key: string; value: string };\n\nexport type CartLineAddInput = {\n merchandiseId: string;\n quantity: number;\n attributes?: CartAttributeInput[];\n sellingPlanId?: string;\n};\n\nexport type CartLineUpdateInput = {\n id: string;\n quantity: number;\n attributes?: CartAttributeInput[];\n sellingPlanId?: string;\n};\n\nexport type CartAction =\n | { intent: \"add\"; lines: CartLineAddInput[] }\n | { intent: \"update\"; lines: CartLineUpdateInput[] }\n | { intent: \"remove\"; lineIds: string[] }\n | { intent: \"discount-update\"; discountCodes: string[] }\n | { intent: \"discount-apply\"; code: string }\n | { intent: \"discount-remove\"; code: string }\n | { intent: \"attributes-update\"; attributes: CartAttributeInput[] }\n | { intent: \"note-update\"; note: string };\n\ntype ParsedCartRequest = {\n action: CartAction;\n cartId: string | null;\n};\n\nclass CartActionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CartActionError\";\n }\n}\n\nexport async function parseCartRequest(request: Request): Promise<ParsedCartRequest> {\n const contentType = request.headers.get(\"content-type\") ?? \"\";\n\n if (contentType.includes(\"application/json\")) {\n return parseJsonBody(await request.json());\n }\n\n if (\n contentType.includes(\"application/x-www-form-urlencoded\") ||\n contentType.includes(\"multipart/form-data\")\n ) {\n return { action: parseFormData(await request.formData()), cartId: null };\n }\n\n throw new CartActionError(\n `Unsupported content-type: \"${contentType}\". Expected application/json or form data.`,\n );\n}\n\n// --- JSON parsing ---\n\nfunction parseJsonBody(body: unknown): ParsedCartRequest {\n assertObject(body);\n const cartId = typeof body.cartId === \"string\" ? normalizeCartId(body.cartId) : null;\n\n if (\"note\" in body && typeof body.note === \"string\") {\n return { action: { intent: \"note-update\", note: body.note }, cartId };\n }\n\n if (\"discountCodes\" in body && Array.isArray(body.discountCodes)) {\n for (const code of body.discountCodes) {\n assertString(code, \"discountCodes entries\");\n }\n return {\n action: { intent: \"discount-update\", discountCodes: body.discountCodes as string[] },\n cartId,\n };\n }\n\n if (\"attributes\" in body && Array.isArray(body.attributes)) {\n return {\n action: { intent: \"attributes-update\", attributes: parseAttributes(body.attributes) },\n cartId,\n };\n }\n\n if (!(\"lines\" in body) || !Array.isArray(body.lines)) {\n throw new CartActionError(\n 'Request body must contain \"lines\", \"discountCodes\", \"attributes\", or \"note\".',\n );\n }\n\n const lines = body.lines as unknown[];\n if (lines.length === 0) {\n throw new CartActionError(\"Lines array must not be empty.\");\n }\n\n return { action: partitionLines(lines), cartId };\n}\n\nfunction partitionLines(rawLines: unknown[]): CartAction {\n const adds: CartLineAddInput[] = [];\n const updates: CartLineUpdateInput[] = [];\n const removeIds: string[] = [];\n\n for (const raw of rawLines) {\n assertObject(raw);\n const line = raw as Record<string, unknown>;\n const hasId = \"id\" in line && typeof line.id === \"string\" && line.id !== \"\";\n const hasMerchandiseId =\n \"merchandiseId\" in line &&\n typeof line.merchandiseId === \"string\" &&\n line.merchandiseId !== \"\";\n\n assertNonNegativeInteger(line.quantity, \"quantity\");\n const quantity = line.quantity as number;\n\n if (!hasId && !hasMerchandiseId) {\n throw new CartActionError(\n 'Each line must have either \"id\" (for update/remove) or \"merchandiseId\" (for add).',\n );\n }\n\n if (hasId && (line.id as string) === \"\") {\n throw new CartActionError('Line \"id\" must not be empty.');\n }\n\n const optionalFields = extractOptionalLineFields(line);\n\n if (!hasId && hasMerchandiseId) {\n adds.push({\n merchandiseId: line.merchandiseId as string,\n quantity,\n ...optionalFields,\n });\n } else if (hasId && quantity === 0) {\n removeIds.push(line.id as string);\n } else if (hasId) {\n updates.push({\n id: line.id as string,\n quantity,\n ...optionalFields,\n });\n }\n }\n\n const populatedBuckets = [adds.length, updates.length, removeIds.length].filter(\n (n) => n > 0,\n ).length;\n\n if (populatedBuckets > 1) {\n throw new CartActionError(\n \"Mixed line operations are not allowed. Separate add, update, and remove into distinct requests.\",\n );\n }\n\n if (adds.length > 0) return { intent: \"add\", lines: adds };\n if (updates.length > 0) return { intent: \"update\", lines: updates };\n return { intent: \"remove\", lineIds: removeIds };\n}\n\nfunction extractOptionalLineFields(\n line: Record<string, unknown>,\n): Pick<CartLineAddInput, \"attributes\" | \"sellingPlanId\"> {\n const result: Pick<CartLineAddInput, \"attributes\" | \"sellingPlanId\"> = {};\n\n if (\"attributes\" in line && Array.isArray(line.attributes)) {\n result.attributes = line.attributes as CartAttributeInput[];\n }\n\n if (\"sellingPlanId\" in line && typeof line.sellingPlanId === \"string\") {\n result.sellingPlanId = line.sellingPlanId;\n }\n\n return result;\n}\n\n// --- FormData parsing ---\n//\n// HTML forms can't express the same structures as JSON. Key compromises:\n//\n// Add line: single item per submission (forms can't batch multiple adds)\n// Update qty: server does the math — form sends current qty, server increments/decrements\n// Remove line: explicit intent field (can't express \"quantity=0 means remove\" implicitly)\n// Discount bulk: no FormData equivalent — forms handle one code at a time\n// Cart attributes: attributes.<key> fields preserve each key/value relationship\n// Selling plan: add only — no way to change selling plan on existing line via form\n//\n// Design decisions:\n// - `intent` field disambiguates operations (forms need explicit routing; JSON infers from structure)\n// - Field names match where possible: merchandiseId, quantity, lineId, discountCode, note\n// - Form submissions get 303 redirect to referer; JSON gets JSON response\n// - `increase`/`decrease` intents exist because forms can't atomically read-modify-write quantity\n\nconst LINE_INTENTS = new Set([\"increase\", \"decrease\", \"remove\", \"set\"]);\nconst DISCOUNT_INTENTS = new Set([\"discount-apply\", \"discount-remove\"]);\n\nfunction parseFormData(form: FormData): CartAction {\n const intent = formString(form, \"intent\");\n const merchandiseId = formString(form, \"merchandiseId\");\n\n if (!intent && merchandiseId) {\n return parseAddIntent(form, merchandiseId);\n }\n\n if (intent === \"add\") {\n if (!merchandiseId) {\n throw new CartActionError('Intent \"add\" requires a \"merchandiseId\" field.');\n }\n return parseAddIntent(form, merchandiseId);\n }\n\n if (!intent) {\n throw new CartActionError(\n 'FormData must include an \"intent\" field or a \"merchandiseId\" field.',\n );\n }\n\n if (LINE_INTENTS.has(intent)) {\n return parseLineIntent(intent, form);\n }\n\n if (DISCOUNT_INTENTS.has(intent)) {\n return parseDiscountIntent(intent as \"discount-apply\" | \"discount-remove\", form);\n }\n\n if (intent === \"note-update\") {\n return parseNoteIntent(form);\n }\n\n if (intent === \"attributes-update\") {\n return parseAttributesIntent(form);\n }\n\n throw new CartActionError(\n `Unknown intent \"${intent}\". Expected one of: add, increase, decrease, remove, set, discount-apply, discount-remove, attributes-update, note-update.`,\n );\n}\n\nfunction parseLineIntent(intent: string, form: FormData): CartAction {\n const lineId = formString(form, \"lineId\");\n if (!lineId) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"lineId\" field.`);\n }\n\n if (intent === \"remove\") {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n\n const quantity = formInt(form, \"quantity\");\n if (quantity === null) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"quantity\" field.`);\n }\n\n if (intent === \"set\") {\n if (quantity <= 0) {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n return { intent: \"update\", lines: [{ id: lineId, quantity }] };\n }\n\n if (intent === \"increase\") {\n return { intent: \"update\", lines: [{ id: lineId, quantity: quantity + 1 }] };\n }\n\n const decremented = quantity - 1;\n if (decremented <= 0) {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n return { intent: \"update\", lines: [{ id: lineId, quantity: decremented }] };\n}\n\nfunction parseDiscountIntent(\n intent: \"discount-apply\" | \"discount-remove\",\n form: FormData,\n): CartAction {\n const code = formString(form, \"discountCode\");\n if (!code) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"discountCode\" field.`);\n }\n return { intent, code };\n}\n\nfunction parseAddIntent(form: FormData, merchandiseId: string): CartAction {\n const quantity = formInt(form, \"quantity\") ?? 1;\n const sellingPlanId = formString(form, \"sellingPlanId\");\n\n const line: CartLineAddInput = { merchandiseId, quantity };\n if (sellingPlanId) line.sellingPlanId = sellingPlanId;\n\n return { intent: \"add\", lines: [line] };\n}\n\nfunction parseNoteIntent(form: FormData): CartAction {\n const noteValue = form.get(\"note\");\n if (noteValue === null) {\n throw new CartActionError('Intent \"note-update\" requires a \"note\" field.');\n }\n return { intent: \"note-update\", note: String(noteValue) };\n}\n\nfunction parseAttributesIntent(form: FormData): CartAction {\n return {\n intent: \"attributes-update\",\n attributes: parseAttributes(getCartAttributeFormEntries(form)),\n };\n}\n\nfunction parseAttributes(values: unknown[]): CartAttributeInput[] {\n return values.map((value, index) => {\n assertObject(value);\n assertString(value.key, `attributes[${index}].key`);\n assertString(value.value, `attributes[${index}].value`);\n if (!value.key) {\n throw new CartActionError(`Expected \"attributes[${index}].key\" not to be empty.`);\n }\n return { key: value.key, value: value.value };\n });\n}\n\n// --- Assertion helpers ---\n\nfunction assertObject(value: unknown): asserts value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new CartActionError(\"Expected an object.\");\n }\n}\n\nfunction assertString(value: unknown, label: string): asserts value is string {\n if (typeof value !== \"string\") {\n throw new CartActionError(`Expected \"${label}\" to be a string.`);\n }\n}\n\nfunction assertNonNegativeInteger(value: unknown, label: string): asserts value is number {\n if (typeof value !== \"number\" || !Number.isInteger(value) || value < 0) {\n throw new CartActionError(\n `Expected \"${label}\" to be a non-negative integer, got ${JSON.stringify(value)}.`,\n );\n }\n}\n\n// --- FormData field helpers ---\n\nfunction formString(form: FormData, key: string): string | null {\n const value = form.get(key);\n if (value === null || typeof value !== \"string\") return null;\n return value || null;\n}\n\nfunction formInt(form: FormData, key: string): number | null {\n const raw = form.get(key);\n if (raw === null || typeof raw !== \"string\") return null;\n const parsed = parseInt(raw, 10);\n if (Number.isNaN(parsed)) return null;\n return parsed;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"actions.mjs","names":[],"sources":["../../../../src/core/cart/actions.ts"],"sourcesContent":["import { normalizeCartId } from \"./cookie\";\nimport { getCartAttributeFormEntries } from \"./form\";\n\nexport type CartAttributeInput = { key: string; value: string };\n\nexport type CartLineAddInput = {\n merchandiseId: string;\n quantity: number;\n attributes?: CartAttributeInput[];\n sellingPlanId?: string;\n};\n\nexport type CartLineUpdateInput = {\n id: string;\n quantity: number;\n attributes?: CartAttributeInput[];\n sellingPlanId?: string;\n};\n\nexport type CartAction =\n | { intent: \"add\"; lines: CartLineAddInput[] }\n | { intent: \"update\"; lines: CartLineUpdateInput[] }\n | { intent: \"remove\"; lineIds: string[] }\n | { intent: \"discount-update\"; discountCodes: string[] }\n | { intent: \"discount-apply\"; code: string }\n | { intent: \"discount-remove\"; code: string }\n | { intent: \"attributes-update\"; attributes: CartAttributeInput[] }\n | { intent: \"note-update\"; note: string };\n\ntype ParsedCartRequest = {\n action: CartAction;\n cartId: string | null;\n};\n\nclass CartActionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CartActionError\";\n }\n}\n\nexport async function parseCartRequest(request: Request): Promise<ParsedCartRequest> {\n const contentType = request.headers.get(\"content-type\") ?? \"\";\n\n if (contentType.includes(\"application/json\")) {\n return parseJsonBody(await request.json());\n }\n\n if (\n contentType.includes(\"application/x-www-form-urlencoded\") ||\n contentType.includes(\"multipart/form-data\")\n ) {\n return { action: parseFormData(await request.formData()), cartId: null };\n }\n\n throw new CartActionError(\n `Unsupported content-type: \"${contentType}\". Expected application/json or form data.`,\n );\n}\n\n// --- JSON parsing ---\n\nfunction parseJsonBody(body: unknown): ParsedCartRequest {\n assertObject(body);\n const cartId = typeof body.cartId === \"string\" ? normalizeCartId(body.cartId) : null;\n\n if (\"note\" in body && typeof body.note === \"string\") {\n return { action: { intent: \"note-update\", note: body.note }, cartId };\n }\n\n if (\"discountCodes\" in body && Array.isArray(body.discountCodes)) {\n for (const code of body.discountCodes) {\n assertString(code, \"discountCodes entries\");\n }\n return {\n action: { intent: \"discount-update\", discountCodes: body.discountCodes as string[] },\n cartId,\n };\n }\n\n if (\"attributes\" in body && Array.isArray(body.attributes)) {\n return {\n action: { intent: \"attributes-update\", attributes: parseAttributes(body.attributes) },\n cartId,\n };\n }\n\n if (!(\"lines\" in body) || !Array.isArray(body.lines)) {\n throw new CartActionError(\n 'Request body must contain \"lines\", \"discountCodes\", \"attributes\", or \"note\".',\n );\n }\n\n const lines = body.lines as unknown[];\n if (lines.length === 0) {\n throw new CartActionError(\"Lines array must not be empty.\");\n }\n\n return { action: partitionLines(lines), cartId };\n}\n\nfunction partitionLines(rawLines: unknown[]): CartAction {\n const adds: CartLineAddInput[] = [];\n const updates: CartLineUpdateInput[] = [];\n const removeIds: string[] = [];\n\n for (const raw of rawLines) {\n assertObject(raw);\n const line = raw as Record<string, unknown>;\n const hasId = \"id\" in line && typeof line.id === \"string\" && line.id !== \"\";\n const hasMerchandiseId =\n \"merchandiseId\" in line &&\n typeof line.merchandiseId === \"string\" &&\n line.merchandiseId !== \"\";\n\n assertNonNegativeInteger(line.quantity, \"quantity\");\n const quantity = line.quantity as number;\n\n if (!hasId && !hasMerchandiseId) {\n throw new CartActionError(\n 'Each line must have either \"id\" (for update/remove) or \"merchandiseId\" (for add).',\n );\n }\n\n if (hasId && (line.id as string) === \"\") {\n throw new CartActionError('Line \"id\" must not be empty.');\n }\n\n const optionalFields = extractOptionalLineFields(line);\n\n if (!hasId && hasMerchandiseId) {\n adds.push({\n merchandiseId: line.merchandiseId as string,\n quantity,\n ...optionalFields,\n });\n } else if (hasId && quantity === 0) {\n removeIds.push(line.id as string);\n } else if (hasId) {\n updates.push({\n id: line.id as string,\n quantity,\n ...optionalFields,\n });\n }\n }\n\n const populatedBuckets = [adds.length, updates.length, removeIds.length].filter(\n (n) => n > 0,\n ).length;\n\n if (populatedBuckets > 1) {\n throw new CartActionError(\n \"Mixed line operations are not allowed. Separate add, update, and remove into distinct requests.\",\n );\n }\n\n if (adds.length > 0) return { intent: \"add\", lines: adds };\n if (updates.length > 0) return { intent: \"update\", lines: updates };\n return { intent: \"remove\", lineIds: removeIds };\n}\n\nfunction extractOptionalLineFields(\n line: Record<string, unknown>,\n): Pick<CartLineAddInput, \"attributes\" | \"sellingPlanId\"> {\n const result: Pick<CartLineAddInput, \"attributes\" | \"sellingPlanId\"> = {};\n\n if (\"attributes\" in line && Array.isArray(line.attributes)) {\n result.attributes = line.attributes as CartAttributeInput[];\n }\n\n if (\"sellingPlanId\" in line && typeof line.sellingPlanId === \"string\") {\n result.sellingPlanId = line.sellingPlanId;\n }\n\n return result;\n}\n\n// --- FormData parsing ---\n//\n// HTML forms can't express the same structures as JSON. Key compromises:\n//\n// Add line: single item per submission (forms can't batch multiple adds)\n// Update qty: server does the math — form sends current qty, server increments/decrements\n// Remove line: explicit intent field (can't express \"quantity=0 means remove\" implicitly)\n// Discount bulk: no FormData equivalent — forms handle one code at a time\n// Cart attributes: attributes.<key> fields preserve each key/value relationship\n// Selling plan: add only — no way to change selling plan on existing line via form\n//\n// Design decisions:\n// - `intent` field disambiguates operations (forms need explicit routing; JSON infers from structure)\n// - Field names match where possible: merchandiseId, quantity, lineId, discountCode, note\n// - Form submissions get 303 redirect to referer; JSON gets JSON response\n// - `increase`/`decrease` intents exist because forms can't atomically read-modify-write quantity\n\nconst LINE_INTENTS = new Set([\"increase\", \"decrease\", \"remove\", \"set\"]);\nconst DISCOUNT_INTENTS = new Set([\"discount-apply\", \"discount-remove\"]);\n\nfunction parseFormData(form: FormData): CartAction {\n const intent = formString(form, \"intent\");\n const merchandiseId = formString(form, \"merchandiseId\");\n\n if (!intent && merchandiseId) {\n return parseAddIntent(form, merchandiseId);\n }\n\n if (intent === \"add\") {\n if (!merchandiseId) {\n throw new CartActionError('Intent \"add\" requires a \"merchandiseId\" field.');\n }\n return parseAddIntent(form, merchandiseId);\n }\n\n if (!intent) {\n throw new CartActionError(\n 'FormData must include an \"intent\" field or a \"merchandiseId\" field.',\n );\n }\n\n if (LINE_INTENTS.has(intent)) {\n return parseLineIntent(intent, form);\n }\n\n if (DISCOUNT_INTENTS.has(intent)) {\n return parseDiscountIntent(intent as \"discount-apply\" | \"discount-remove\", form);\n }\n\n if (intent === \"note-update\") {\n return parseNoteIntent(form);\n }\n\n if (intent === \"attributes-update\") {\n return parseAttributesIntent(form);\n }\n\n throw new CartActionError(\n `Unknown intent \"${intent}\". Expected one of: add, increase, decrease, remove, set, discount-apply, discount-remove, attributes-update, note-update.`,\n );\n}\n\nfunction parseLineIntent(intent: string, form: FormData): CartAction {\n const lineId = formString(form, \"lineId\");\n if (!lineId) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"lineId\" field.`);\n }\n\n if (intent === \"remove\") {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n\n const quantity = formInt(form, \"quantity\");\n if (quantity === null) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"quantity\" field.`);\n }\n\n if (intent === \"set\") {\n if (quantity <= 0) {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n return { intent: \"update\", lines: [{ id: lineId, quantity }] };\n }\n\n if (intent === \"increase\") {\n return { intent: \"update\", lines: [{ id: lineId, quantity: quantity + 1 }] };\n }\n\n const decremented = quantity - 1;\n if (decremented <= 0) {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n return { intent: \"update\", lines: [{ id: lineId, quantity: decremented }] };\n}\n\nfunction parseDiscountIntent(\n intent: \"discount-apply\" | \"discount-remove\",\n form: FormData,\n): CartAction {\n const code = formString(form, \"discountCode\");\n if (!code) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"discountCode\" field.`);\n }\n return { intent, code };\n}\n\nfunction parseAddIntent(form: FormData, merchandiseId: string): CartAction {\n const quantity = formInt(form, \"quantity\") ?? 1;\n const sellingPlanId = formString(form, \"sellingPlanId\");\n\n const line: CartLineAddInput = { merchandiseId, quantity };\n if (sellingPlanId) line.sellingPlanId = sellingPlanId;\n\n const rawAttributes = getCartAttributeFormEntries(form);\n if (rawAttributes.length > 0) {\n line.attributes = parseAttributes(rawAttributes);\n }\n\n return { intent: \"add\", lines: [line] };\n}\n\nfunction parseNoteIntent(form: FormData): CartAction {\n const noteValue = form.get(\"note\");\n if (noteValue === null) {\n throw new CartActionError('Intent \"note-update\" requires a \"note\" field.');\n }\n return { intent: \"note-update\", note: String(noteValue) };\n}\n\nfunction parseAttributesIntent(form: FormData): CartAction {\n return {\n intent: \"attributes-update\",\n attributes: parseAttributes(getCartAttributeFormEntries(form)),\n };\n}\n\nfunction parseAttributes(values: unknown[]): CartAttributeInput[] {\n return values.map((value, index) => {\n assertObject(value);\n assertString(value.key, `attributes[${index}].key`);\n assertString(value.value, `attributes[${index}].value`);\n if (!value.key) {\n throw new CartActionError(`Expected \"attributes[${index}].key\" not to be empty.`);\n }\n return { key: value.key, value: value.value };\n });\n}\n\n// --- Assertion helpers ---\n\nfunction assertObject(value: unknown): asserts value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new CartActionError(\"Expected an object.\");\n }\n}\n\nfunction assertString(value: unknown, label: string): asserts value is string {\n if (typeof value !== \"string\") {\n throw new CartActionError(`Expected \"${label}\" to be a string.`);\n }\n}\n\nfunction assertNonNegativeInteger(value: unknown, label: string): asserts value is number {\n if (typeof value !== \"number\" || !Number.isInteger(value) || value < 0) {\n throw new CartActionError(\n `Expected \"${label}\" to be a non-negative integer, got ${JSON.stringify(value)}.`,\n );\n }\n}\n\n// --- FormData field helpers ---\n\nfunction formString(form: FormData, key: string): string | null {\n const value = form.get(key);\n if (value === null || typeof value !== \"string\") return null;\n return value || null;\n}\n\nfunction formInt(form: FormData, key: string): number | null {\n const raw = form.get(key);\n if (raw === null || typeof raw !== \"string\") return null;\n const parsed = parseInt(raw, 10);\n if (Number.isNaN(parsed)) return null;\n return parsed;\n}\n"],"mappings":";;;;AAkCA,IAAM,kBAAN,cAA8B,MAAM;CAClC,YAAY,SAAiB;AAC3B,QAAM,QAAQ;AACd,OAAK,OAAO;;;AAIhB,eAAsB,iBAAiB,SAA8C;CACnF,MAAM,cAAc,QAAQ,QAAQ,IAAI,eAAe,IAAI;AAE3D,KAAI,YAAY,SAAS,mBAAmB,CAC1C,QAAO,cAAc,MAAM,QAAQ,MAAM,CAAC;AAG5C,KACE,YAAY,SAAS,oCAAoC,IACzD,YAAY,SAAS,sBAAsB,CAE3C,QAAO;EAAE,QAAQ,cAAc,MAAM,QAAQ,UAAU,CAAC;EAAE,QAAQ;EAAM;AAG1E,OAAM,IAAI,gBACR,8BAA8B,YAAY,4CAC3C;;AAKH,SAAS,cAAc,MAAkC;AACvD,cAAa,KAAK;CAClB,MAAM,SAAS,OAAO,KAAK,WAAW,WAAW,gBAAgB,KAAK,OAAO,GAAG;AAEhF,KAAI,UAAU,QAAQ,OAAO,KAAK,SAAS,SACzC,QAAO;EAAE,QAAQ;GAAE,QAAQ;GAAe,MAAM,KAAK;GAAM;EAAE;EAAQ;AAGvE,KAAI,mBAAmB,QAAQ,MAAM,QAAQ,KAAK,cAAc,EAAE;AAChE,OAAK,MAAM,QAAQ,KAAK,cACtB,cAAa,MAAM,wBAAwB;AAE7C,SAAO;GACL,QAAQ;IAAE,QAAQ;IAAmB,eAAe,KAAK;IAA2B;GACpF;GACD;;AAGH,KAAI,gBAAgB,QAAQ,MAAM,QAAQ,KAAK,WAAW,CACxD,QAAO;EACL,QAAQ;GAAE,QAAQ;GAAqB,YAAY,gBAAgB,KAAK,WAAW;GAAE;EACrF;EACD;AAGH,KAAI,EAAE,WAAW,SAAS,CAAC,MAAM,QAAQ,KAAK,MAAM,CAClD,OAAM,IAAI,gBACR,uFACD;CAGH,MAAM,QAAQ,KAAK;AACnB,KAAI,MAAM,WAAW,EACnB,OAAM,IAAI,gBAAgB,iCAAiC;AAG7D,QAAO;EAAE,QAAQ,eAAe,MAAM;EAAE;EAAQ;;AAGlD,SAAS,eAAe,UAAiC;CACvD,MAAM,OAA2B,EAAE;CACnC,MAAM,UAAiC,EAAE;CACzC,MAAM,YAAsB,EAAE;AAE9B,MAAK,MAAM,OAAO,UAAU;AAC1B,eAAa,IAAI;EACjB,MAAM,OAAO;EACb,MAAM,QAAQ,QAAQ,QAAQ,OAAO,KAAK,OAAO,YAAY,KAAK,OAAO;EACzE,MAAM,mBACJ,mBAAmB,QACnB,OAAO,KAAK,kBAAkB,YAC9B,KAAK,kBAAkB;AAEzB,2BAAyB,KAAK,UAAU,WAAW;EACnD,MAAM,WAAW,KAAK;AAEtB,MAAI,CAAC,SAAS,CAAC,iBACb,OAAM,IAAI,gBACR,wFACD;AAGH,MAAI,SAAU,KAAK,OAAkB,GACnC,OAAM,IAAI,gBAAgB,iCAA+B;EAG3D,MAAM,iBAAiB,0BAA0B,KAAK;AAEtD,MAAI,CAAC,SAAS,iBACZ,MAAK,KAAK;GACR,eAAe,KAAK;GACpB;GACA,GAAG;GACJ,CAAC;WACO,SAAS,aAAa,EAC/B,WAAU,KAAK,KAAK,GAAa;WACxB,MACT,SAAQ,KAAK;GACX,IAAI,KAAK;GACT;GACA,GAAG;GACJ,CAAC;;AAQN,KAJyB;EAAC,KAAK;EAAQ,QAAQ;EAAQ,UAAU;EAAO,CAAC,QACtE,MAAM,IAAI,EACZ,CAAC,SAEqB,EACrB,OAAM,IAAI,gBACR,kGACD;AAGH,KAAI,KAAK,SAAS,EAAG,QAAO;EAAE,QAAQ;EAAO,OAAO;EAAM;AAC1D,KAAI,QAAQ,SAAS,EAAG,QAAO;EAAE,QAAQ;EAAU,OAAO;EAAS;AACnE,QAAO;EAAE,QAAQ;EAAU,SAAS;EAAW;;AAGjD,SAAS,0BACP,MACwD;CACxD,MAAM,SAAiE,EAAE;AAEzE,KAAI,gBAAgB,QAAQ,MAAM,QAAQ,KAAK,WAAW,CACxD,QAAO,aAAa,KAAK;AAG3B,KAAI,mBAAmB,QAAQ,OAAO,KAAK,kBAAkB,SAC3D,QAAO,gBAAgB,KAAK;AAG9B,QAAO;;AAoBT,MAAM,eAAe,IAAI,IAAI;CAAC;CAAY;CAAY;CAAU;CAAM,CAAC;AACvE,MAAM,mBAAmB,IAAI,IAAI,CAAC,kBAAkB,kBAAkB,CAAC;AAEvE,SAAS,cAAc,MAA4B;CACjD,MAAM,SAAS,WAAW,MAAM,SAAS;CACzC,MAAM,gBAAgB,WAAW,MAAM,gBAAgB;AAEvD,KAAI,CAAC,UAAU,cACb,QAAO,eAAe,MAAM,cAAc;AAG5C,KAAI,WAAW,OAAO;AACpB,MAAI,CAAC,cACH,OAAM,IAAI,gBAAgB,qDAAiD;AAE7E,SAAO,eAAe,MAAM,cAAc;;AAG5C,KAAI,CAAC,OACH,OAAM,IAAI,gBACR,0EACD;AAGH,KAAI,aAAa,IAAI,OAAO,CAC1B,QAAO,gBAAgB,QAAQ,KAAK;AAGtC,KAAI,iBAAiB,IAAI,OAAO,CAC9B,QAAO,oBAAoB,QAAgD,KAAK;AAGlF,KAAI,WAAW,cACb,QAAO,gBAAgB,KAAK;AAG9B,KAAI,WAAW,oBACb,QAAO,sBAAsB,KAAK;AAGpC,OAAM,IAAI,gBACR,mBAAmB,OAAO,4HAC3B;;AAGH,SAAS,gBAAgB,QAAgB,MAA4B;CACnE,MAAM,SAAS,WAAW,MAAM,SAAS;AACzC,KAAI,CAAC,OACH,OAAM,IAAI,gBAAgB,WAAW,OAAO,8BAA8B;AAG5E,KAAI,WAAW,SACb,QAAO;EAAE,QAAQ;EAAU,SAAS,CAAC,OAAO;EAAE;CAGhD,MAAM,WAAW,QAAQ,MAAM,WAAW;AAC1C,KAAI,aAAa,KACf,OAAM,IAAI,gBAAgB,WAAW,OAAO,gCAAgC;AAG9E,KAAI,WAAW,OAAO;AACpB,MAAI,YAAY,EACd,QAAO;GAAE,QAAQ;GAAU,SAAS,CAAC,OAAO;GAAE;AAEhD,SAAO;GAAE,QAAQ;GAAU,OAAO,CAAC;IAAE,IAAI;IAAQ;IAAU,CAAC;GAAE;;AAGhE,KAAI,WAAW,WACb,QAAO;EAAE,QAAQ;EAAU,OAAO,CAAC;GAAE,IAAI;GAAQ,UAAU,WAAW;GAAG,CAAC;EAAE;CAG9E,MAAM,cAAc,WAAW;AAC/B,KAAI,eAAe,EACjB,QAAO;EAAE,QAAQ;EAAU,SAAS,CAAC,OAAO;EAAE;AAEhD,QAAO;EAAE,QAAQ;EAAU,OAAO,CAAC;GAAE,IAAI;GAAQ,UAAU;GAAa,CAAC;EAAE;;AAG7E,SAAS,oBACP,QACA,MACY;CACZ,MAAM,OAAO,WAAW,MAAM,eAAe;AAC7C,KAAI,CAAC,KACH,OAAM,IAAI,gBAAgB,WAAW,OAAO,oCAAoC;AAElF,QAAO;EAAE;EAAQ;EAAM;;AAGzB,SAAS,eAAe,MAAgB,eAAmC;CACzE,MAAM,WAAW,QAAQ,MAAM,WAAW,IAAI;CAC9C,MAAM,gBAAgB,WAAW,MAAM,gBAAgB;CAEvD,MAAM,OAAyB;EAAE;EAAe;EAAU;AAC1D,KAAI,cAAe,MAAK,gBAAgB;CAExC,MAAM,gBAAgB,4BAA4B,KAAK;AACvD,KAAI,cAAc,SAAS,EACzB,MAAK,aAAa,gBAAgB,cAAc;AAGlD,QAAO;EAAE,QAAQ;EAAO,OAAO,CAAC,KAAK;EAAE;;AAGzC,SAAS,gBAAgB,MAA4B;CACnD,MAAM,YAAY,KAAK,IAAI,OAAO;AAClC,KAAI,cAAc,KAChB,OAAM,IAAI,gBAAgB,oDAAgD;AAE5E,QAAO;EAAE,QAAQ;EAAe,MAAM,OAAO,UAAU;EAAE;;AAG3D,SAAS,sBAAsB,MAA4B;AACzD,QAAO;EACL,QAAQ;EACR,YAAY,gBAAgB,4BAA4B,KAAK,CAAC;EAC/D;;AAGH,SAAS,gBAAgB,QAAyC;AAChE,QAAO,OAAO,KAAK,OAAO,UAAU;AAClC,eAAa,MAAM;AACnB,eAAa,MAAM,KAAK,cAAc,MAAM,OAAO;AACnD,eAAa,MAAM,OAAO,cAAc,MAAM,SAAS;AACvD,MAAI,CAAC,MAAM,IACT,OAAM,IAAI,gBAAgB,wBAAwB,MAAM,yBAAyB;AAEnF,SAAO;GAAE,KAAK,MAAM;GAAK,OAAO,MAAM;GAAO;GAC7C;;AAKJ,SAAS,aAAa,OAA0D;AAC9E,KAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,MAAM,CACrE,OAAM,IAAI,gBAAgB,sBAAsB;;AAIpD,SAAS,aAAa,OAAgB,OAAwC;AAC5E,KAAI,OAAO,UAAU,SACnB,OAAM,IAAI,gBAAgB,aAAa,MAAM,mBAAmB;;AAIpE,SAAS,yBAAyB,OAAgB,OAAwC;AACxF,KAAI,OAAO,UAAU,YAAY,CAAC,OAAO,UAAU,MAAM,IAAI,QAAQ,EACnE,OAAM,IAAI,gBACR,aAAa,MAAM,sCAAsC,KAAK,UAAU,MAAM,CAAC,GAChF;;AAML,SAAS,WAAW,MAAgB,KAA4B;CAC9D,MAAM,QAAQ,KAAK,IAAI,IAAI;AAC3B,KAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AACxD,QAAO,SAAS;;AAGlB,SAAS,QAAQ,MAAgB,KAA4B;CAC3D,MAAM,MAAM,KAAK,IAAI,IAAI;AACzB,KAAI,QAAQ,QAAQ,OAAO,QAAQ,SAAU,QAAO;CACpD,MAAM,SAAS,SAAS,KAAK,GAAG;AAChC,KAAI,OAAO,MAAM,OAAO,CAAE,QAAO;AACjC,QAAO"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getLogger } from "../logging/logging.mjs";
|
|
2
2
|
import { createObservable } from "../observable.mjs";
|
|
3
|
-
import { SHOPIFY_STOREFRONT_STANDARD_ACTIONS_SCRIPT, VISITOR_CONSENT_COLLECTED_EVENT } from "../shopify-scripts/constants.mjs";
|
|
3
|
+
import { CONSENT_TRACKING_API_LOADED_EVENT, SHOPIFY_STOREFRONT_STANDARD_ACTIONS_SCRIPT, VISITOR_CONSENT_COLLECTED_EVENT } from "../shopify-scripts/constants.mjs";
|
|
4
4
|
import { getCartAttributeFormEntries } from "./form.mjs";
|
|
5
5
|
import { DEFAULT_MINIMUM_QUANTITY, sanitizeQuantity } from "./quantity.mjs";
|
|
6
6
|
import { createCartState, createEmptyCartState, createEmptyErrorGroup } from "./state.mjs";
|
|
@@ -949,6 +949,7 @@ function enqueueTransaction(store, type, payload, promise, reservedTransaction,
|
|
|
949
949
|
const expectedEventIndex = store.expectedEvents.findIndex((expected) => expected.token === eventToken);
|
|
950
950
|
if (expectedEventIndex !== -1) {
|
|
951
951
|
store.expectedEvents.splice(expectedEventIndex, 1);
|
|
952
|
+
promise.then(NOOP, NOOP);
|
|
952
953
|
return;
|
|
953
954
|
}
|
|
954
955
|
if (store.observedPromises.has(promise)) return;
|
|
@@ -1229,17 +1230,20 @@ function destroyCartStore(store, handlers) {
|
|
|
1229
1230
|
store.cartSyncAttached = false;
|
|
1230
1231
|
publishVisibleState(store);
|
|
1231
1232
|
}
|
|
1232
|
-
function
|
|
1233
|
+
function handleConsentStateChanged() {
|
|
1233
1234
|
revalidateConnectedCartCheckoutUrls();
|
|
1234
1235
|
}
|
|
1235
1236
|
function attachCartConsentListener() {
|
|
1236
1237
|
if (cartConsentListenerAttached) return;
|
|
1237
|
-
document.addEventListener(
|
|
1238
|
+
document.addEventListener(CONSENT_TRACKING_API_LOADED_EVENT, handleConsentStateChanged);
|
|
1239
|
+
document.addEventListener(VISITOR_CONSENT_COLLECTED_EVENT, handleConsentStateChanged);
|
|
1238
1240
|
cartConsentListenerAttached = true;
|
|
1241
|
+
if (window.Shopify?.customerPrivacy?.consentStatus === "loaded") revalidateConnectedCartCheckoutUrls();
|
|
1239
1242
|
}
|
|
1240
1243
|
function detachCartConsentListenerIfIdle() {
|
|
1241
1244
|
if (!cartConsentListenerAttached || connectedCartStores.size > 0) return;
|
|
1242
|
-
document.removeEventListener(
|
|
1245
|
+
document.removeEventListener(CONSENT_TRACKING_API_LOADED_EVENT, handleConsentStateChanged);
|
|
1246
|
+
document.removeEventListener(VISITOR_CONSENT_COLLECTED_EVENT, handleConsentStateChanged);
|
|
1243
1247
|
cartConsentListenerAttached = false;
|
|
1244
1248
|
}
|
|
1245
1249
|
function createCartObservable(initialState, onReadyPromiseRemoved) {
|
|
@@ -1476,11 +1480,17 @@ function getAddPayload(formData, eventDetail) {
|
|
|
1476
1480
|
const rawQuantity = Number.parseInt(formData.get("quantity"), 10);
|
|
1477
1481
|
const quantity = Number.isNaN(rawQuantity) ? DEFAULT_ADD_QUANTITY : Math.max(DEFAULT_ADD_QUANTITY, rawQuantity);
|
|
1478
1482
|
const sellingPlanId = formData.get("sellingPlanId") || void 0;
|
|
1483
|
+
const rawAttributes = getCartAttributeFormEntries(formData);
|
|
1484
|
+
const attributes = rawAttributes.length > 0 ? rawAttributes.filter(({ key }) => key !== "").map(({ key, value }) => ({
|
|
1485
|
+
key,
|
|
1486
|
+
value: String(value)
|
|
1487
|
+
})) : void 0;
|
|
1479
1488
|
return {
|
|
1480
1489
|
lines: [{
|
|
1481
1490
|
merchandiseId,
|
|
1482
1491
|
quantity,
|
|
1483
|
-
...sellingPlanId ? { sellingPlanId } : {}
|
|
1492
|
+
...sellingPlanId ? { sellingPlanId } : {},
|
|
1493
|
+
...attributes ? { attributes } : {}
|
|
1484
1494
|
}],
|
|
1485
1495
|
products: extractProductDetails(eventDetail),
|
|
1486
1496
|
...eventDetail ? { eventDetail } : {}
|
|
@@ -1505,7 +1515,7 @@ async function handleFormSubmitInStore(store, handlers, event, eventDetail) {
|
|
|
1505
1515
|
if (LINE_INTENTS.has(intent)) return submitLineIntent(store, intent, formData);
|
|
1506
1516
|
if (DISCOUNT_INTENTS.has(intent)) return submitDiscountIntent(store, intent, formData);
|
|
1507
1517
|
if (intent === "note-update") return dispatchTransaction(store, "set_note", { note: formData.get("note") ?? "" });
|
|
1508
|
-
if (intent === "attributes-update") return dispatchTransaction(store, "set_attributes", { attributes: getCartAttributeFormEntries(formData).map(({ key, value }) => ({
|
|
1518
|
+
if (intent === "attributes-update") return dispatchTransaction(store, "set_attributes", { attributes: getCartAttributeFormEntries(formData).filter(({ key }) => key !== "").map(({ key, value }) => ({
|
|
1509
1519
|
key,
|
|
1510
1520
|
value: String(value)
|
|
1511
1521
|
})) });
|