@cartbase/storefront 0.17.0 → 0.18.1

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.
Files changed (108) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +3 -1
  3. package/src/api/auth.ts +108 -108
  4. package/src/api/carts.ts +523 -523
  5. package/src/api/categories.ts +184 -184
  6. package/src/api/checkout.ts +526 -526
  7. package/src/api/collections.ts +130 -130
  8. package/src/api/consent.ts +75 -75
  9. package/src/api/content.ts +125 -125
  10. package/src/api/customers.ts +303 -303
  11. package/src/api/gift-cards.ts +112 -112
  12. package/src/api/http.ts +180 -180
  13. package/src/api/index.ts +30 -30
  14. package/src/api/menus.ts +77 -77
  15. package/src/api/metaobjects.ts +136 -136
  16. package/src/api/orders.ts +290 -290
  17. package/src/api/products.ts +43 -1
  18. package/src/api/redirects.ts +37 -37
  19. package/src/api/regions.ts +200 -200
  20. package/src/api/reviews.ts +259 -259
  21. package/src/api/search.ts +163 -163
  22. package/src/api/store.ts +35 -35
  23. package/src/api/types.ts +91 -91
  24. package/src/cart-drawer/context.tsx +367 -168
  25. package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
  26. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
  27. package/src/cart-drawer/gift-wrap.tsx +82 -82
  28. package/src/cart-drawer/index.ts +73 -68
  29. package/src/cart-drawer/item/index.tsx +162 -162
  30. package/src/cart-drawer/item/quantity.tsx +8 -14
  31. package/src/cart-drawer/item/upsell.tsx +110 -110
  32. package/src/cart-drawer/labels.ts +123 -123
  33. package/src/cart-drawer/mutation-queue.ts +78 -0
  34. package/src/cart-drawer/notes.tsx +131 -131
  35. package/src/cart-drawer/sticky-footer.tsx +73 -73
  36. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  37. package/src/checkout/address-error-copy.ts +117 -117
  38. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  39. package/src/checkout/compare-addresses.ts +40 -40
  40. package/src/checkout/discount-section.tsx +218 -218
  41. package/src/checkout/error-copy-codes.ts +63 -63
  42. package/src/checkout/geocode.ts +154 -154
  43. package/src/checkout/gift-card-section.tsx +224 -224
  44. package/src/checkout/index.ts +72 -72
  45. package/src/checkout/labels.ts +495 -495
  46. package/src/checkout/payment-button.tsx +372 -372
  47. package/src/checkout/payment-error-copy.ts +154 -154
  48. package/src/checkout/promotion-error-copy.ts +91 -91
  49. package/src/checkout/use-checkout-orchestration.ts +1604 -1592
  50. package/src/common/country-flag.tsx +52 -52
  51. package/src/common/country-select.tsx +11 -11
  52. package/src/common/index.ts +20 -20
  53. package/src/common/market-select.tsx +57 -57
  54. package/src/index.ts +12 -12
  55. package/src/lib/cookie-names.ts +14 -0
  56. package/src/lib/country-name.ts +59 -59
  57. package/src/lib/get-product-price.ts +133 -133
  58. package/src/lib/media-image.tsx +39 -39
  59. package/src/lib/payment-constants.ts +53 -53
  60. package/src/lib/platform.ts +13 -13
  61. package/src/lib/price.tsx +39 -39
  62. package/src/lib/store-api-error.ts +36 -36
  63. package/src/lib/variant-caption.ts +32 -32
  64. package/src/locales/bg.ts +467 -467
  65. package/src/locales/context.ts +37 -37
  66. package/src/locales/en.ts +26 -26
  67. package/src/locales/es.ts +466 -466
  68. package/src/locales/index.ts +19 -19
  69. package/src/locales/provider.tsx +59 -59
  70. package/src/locales/types.ts +77 -77
  71. package/src/order/index.ts +62 -62
  72. package/src/order/labels.ts +79 -79
  73. package/src/order/order-totals.tsx +250 -250
  74. package/src/primitives/select-field.tsx +93 -93
  75. package/src/products/image-gallery.tsx +43 -43
  76. package/src/products/index.ts +68 -59
  77. package/src/products/option-select.tsx +67 -67
  78. package/src/products/product-actions-wrapper.tsx +63 -62
  79. package/src/products/product-actions.tsx +151 -150
  80. package/src/products/product-price.tsx +69 -69
  81. package/src/products/product-promises.tsx +61 -61
  82. package/src/products/product-specs.tsx +65 -65
  83. package/src/products/product-tabs.tsx +123 -123
  84. package/src/products/product-template.tsx +163 -162
  85. package/src/products/purchase-options.tsx +130 -130
  86. package/src/products/sets.ts +110 -0
  87. package/src/products/use-product-actions.ts +199 -164
  88. package/src/products/variant-matching.ts +71 -71
  89. package/src/products/variant-url.ts +74 -74
  90. package/src/reviews-ui/index.ts +73 -73
  91. package/src/store/category-template.tsx +136 -136
  92. package/src/store/index.ts +40 -40
  93. package/src/tracking/chatgpt-pixel.tsx +99 -99
  94. package/src/tracking/consent-init.tsx +62 -62
  95. package/src/tracking/events.ts +348 -348
  96. package/src/tracking/ga4.tsx +93 -93
  97. package/src/tracking/google-ads.ts +84 -84
  98. package/src/tracking/gtm.tsx +60 -60
  99. package/src/tracking/inline-script.ts +49 -49
  100. package/src/tracking/oaiq.ts +206 -206
  101. package/src/tracking/tiktok-pixel.tsx +91 -91
  102. package/src/tracking/track-init.tsx +56 -56
  103. package/src/tracking/track-order-purchase.tsx +122 -122
  104. package/src/tracking/ttq.ts +180 -180
  105. package/src/tracking/use-tracking-config.ts +54 -54
  106. package/theme/index.css +25 -25
  107. package/theme/theme.css +150 -150
  108. package/theme/tokens.css +106 -106
@@ -1,93 +1,93 @@
1
- "use client"
2
-
3
- import Script from "next/script"
4
-
5
- import { jsStringLiteral, urlParam } from "./inline-script"
6
-
7
- /**
8
- * GoogleTag — THE single Google tag for a storefront.
9
- *
10
- * Loads `https://www.googletagmanager.com/gtag/js` once and issues one
11
- * `config` per destination: GA4 for analytics, Google Ads for conversions
12
- * and remarketing audiences.
13
- *
14
- * ONE loader, TWO configs, and that is Google's own instruction: when a
15
- * page already carries a Google tag you add the Ads conversion ID with an
16
- * additional `config` command, never a second `<script src>`
17
- * (https://support.google.com/google-ads/answer/2476688). Every agency
18
- * onboarding email pastes a second loader anyway, contradicting the line
19
- * printed above it in the same email. Take the instruction, not the
20
- * snippet.
21
- *
22
- * The Ads `config` is also what powers Google Ads remarketing audiences.
23
- * It is not optional for an account that intends to retarget, and linking
24
- * GA4 to Ads does not substitute for it.
25
- *
26
- * Consent needs nothing here: <ConsentInit> sets the Consent Mode v2
27
- * defaults synchronously ahead of this loader, and because the Ads
28
- * destination rides the same gtag instance, `ad_storage` /
29
- * `ad_user_data` / `ad_personalization` gate it with no extra wiring.
30
- * That is the direct payoff of refusing the second script tag.
31
- *
32
- * Renders nothing when neither id is configured, so a layout can mount it
33
- * unconditionally.
34
- *
35
- * `send_page_view` stays at its default, so the initial page_view fires
36
- * on load. SPA route changes are NOT auto-tracked by gtag; a storefront
37
- * that wants per-route page_views calls `gtag('event', 'page_view', …)`
38
- * from a route-change effect. Take care doing that: a `router.replace`
39
- * that only rewrites a query parameter reads as a navigation to gtag, to
40
- * the Meta pixel and to TikTok alike, and every vendor then counts two
41
- * views of one page.
42
- */
43
- export function GoogleTag({
44
- measurementId,
45
- adsConversionId,
46
- }: {
47
- /** GA4 destination, e.g. "G-8BLJ6CW7VX". */
48
- measurementId?: string
49
- /** Google Ads destination, e.g. "AW-18150814603". */
50
- adsConversionId?: string
51
- }) {
52
- const destinations = [measurementId, adsConversionId].filter(
53
- (id): id is string => Boolean(id && id.trim())
54
- )
55
- if (destinations.length === 0) return null
56
-
57
- // Which id lands in the loader URL is cosmetic — gtag treats every
58
- // `config` equally once loaded. GA4 leads when present so the URL stays
59
- // what it has always been for stores that only run analytics.
60
- const loaderId = destinations[0]
61
-
62
- const initSnippet = `
63
- window.dataLayer = window.dataLayer || [];
64
- function gtag(){dataLayer.push(arguments);}
65
- gtag('js', new Date());
66
- ${destinations.map((id) => `gtag('config', ${jsStringLiteral(id)});`).join("\n")}
67
- `.trim()
68
-
69
- return (
70
- <>
71
- <Script
72
- id="google-tag-loader"
73
- strategy="afterInteractive"
74
- src={`https://www.googletagmanager.com/gtag/js?id=${urlParam(loaderId)}`}
75
- />
76
- <Script
77
- id="google-tag-init"
78
- strategy="afterInteractive"
79
- dangerouslySetInnerHTML={{ __html: initSnippet }}
80
- />
81
- </>
82
- )
83
- }
84
-
85
- /**
86
- * GA4 — the analytics-only door, kept because storefronts mount it by
87
- * this name. It is <GoogleTag> with one destination; a store that also
88
- * runs Google Ads should mount <GoogleTag> with both ids instead, since
89
- * two loaders on one page is the thing Google tells you not to do.
90
- */
91
- export function GA4({ measurementId }: { measurementId?: string }) {
92
- return <GoogleTag measurementId={measurementId} />
93
- }
1
+ "use client"
2
+
3
+ import Script from "next/script"
4
+
5
+ import { jsStringLiteral, urlParam } from "./inline-script"
6
+
7
+ /**
8
+ * GoogleTag — THE single Google tag for a storefront.
9
+ *
10
+ * Loads `https://www.googletagmanager.com/gtag/js` once and issues one
11
+ * `config` per destination: GA4 for analytics, Google Ads for conversions
12
+ * and remarketing audiences.
13
+ *
14
+ * ONE loader, TWO configs, and that is Google's own instruction: when a
15
+ * page already carries a Google tag you add the Ads conversion ID with an
16
+ * additional `config` command, never a second `<script src>`
17
+ * (https://support.google.com/google-ads/answer/2476688). Every agency
18
+ * onboarding email pastes a second loader anyway, contradicting the line
19
+ * printed above it in the same email. Take the instruction, not the
20
+ * snippet.
21
+ *
22
+ * The Ads `config` is also what powers Google Ads remarketing audiences.
23
+ * It is not optional for an account that intends to retarget, and linking
24
+ * GA4 to Ads does not substitute for it.
25
+ *
26
+ * Consent needs nothing here: <ConsentInit> sets the Consent Mode v2
27
+ * defaults synchronously ahead of this loader, and because the Ads
28
+ * destination rides the same gtag instance, `ad_storage` /
29
+ * `ad_user_data` / `ad_personalization` gate it with no extra wiring.
30
+ * That is the direct payoff of refusing the second script tag.
31
+ *
32
+ * Renders nothing when neither id is configured, so a layout can mount it
33
+ * unconditionally.
34
+ *
35
+ * `send_page_view` stays at its default, so the initial page_view fires
36
+ * on load. SPA route changes are NOT auto-tracked by gtag; a storefront
37
+ * that wants per-route page_views calls `gtag('event', 'page_view', …)`
38
+ * from a route-change effect. Take care doing that: a `router.replace`
39
+ * that only rewrites a query parameter reads as a navigation to gtag, to
40
+ * the Meta pixel and to TikTok alike, and every vendor then counts two
41
+ * views of one page.
42
+ */
43
+ export function GoogleTag({
44
+ measurementId,
45
+ adsConversionId,
46
+ }: {
47
+ /** GA4 destination, e.g. "G-8BLJ6CW7VX". */
48
+ measurementId?: string
49
+ /** Google Ads destination, e.g. "AW-18150814603". */
50
+ adsConversionId?: string
51
+ }) {
52
+ const destinations = [measurementId, adsConversionId].filter(
53
+ (id): id is string => Boolean(id && id.trim())
54
+ )
55
+ if (destinations.length === 0) return null
56
+
57
+ // Which id lands in the loader URL is cosmetic — gtag treats every
58
+ // `config` equally once loaded. GA4 leads when present so the URL stays
59
+ // what it has always been for stores that only run analytics.
60
+ const loaderId = destinations[0]
61
+
62
+ const initSnippet = `
63
+ window.dataLayer = window.dataLayer || [];
64
+ function gtag(){dataLayer.push(arguments);}
65
+ gtag('js', new Date());
66
+ ${destinations.map((id) => `gtag('config', ${jsStringLiteral(id)});`).join("\n")}
67
+ `.trim()
68
+
69
+ return (
70
+ <>
71
+ <Script
72
+ id="google-tag-loader"
73
+ strategy="afterInteractive"
74
+ src={`https://www.googletagmanager.com/gtag/js?id=${urlParam(loaderId)}`}
75
+ />
76
+ <Script
77
+ id="google-tag-init"
78
+ strategy="afterInteractive"
79
+ dangerouslySetInnerHTML={{ __html: initSnippet }}
80
+ />
81
+ </>
82
+ )
83
+ }
84
+
85
+ /**
86
+ * GA4 — the analytics-only door, kept because storefronts mount it by
87
+ * this name. It is <GoogleTag> with one destination; a store that also
88
+ * runs Google Ads should mount <GoogleTag> with both ids instead, since
89
+ * two loaders on one page is the thing Google tells you not to do.
90
+ */
91
+ export function GA4({ measurementId }: { measurementId?: string }) {
92
+ return <GoogleTag measurementId={measurementId} />
93
+ }
@@ -1,84 +1,84 @@
1
- "use client"
2
-
3
- import type { TrackingConfig } from "./types"
4
-
5
- /**
6
- * Google Ads conversion tracking — browser side.
7
- *
8
- * Rides the SAME gtag instance <GoogleTag> loads for GA4; there is
9
- * deliberately no loader in this file (see the comment block in ./ga4.tsx
10
- * and https://support.google.com/google-ads/answer/2476688).
11
- *
12
- * DEDUP, and it is the whole reason this file is careful. Google Ads
13
- * collapses two conversions that share a conversion action AND a
14
- * `transaction_id` (https://support.google.com/google-ads/answer/6386790).
15
- * Every vendor snippet ships `'transaction_id': ''`, and an empty value
16
- * dedupes nothing — so every re-open of the confirmation page counts
17
- * another purchase. That is not a hypothetical: order emails link back to
18
- * the confirmation page, so the page is re-opened days after the sale, by
19
- * the same buyer, routinely. We always send `String(order.display_id)`,
20
- * the same key GA4's Measurement Protocol already uses.
21
- *
22
- * No-ops when gtag is absent (Ads not configured, server render, blocked
23
- * script), so callers need no guards.
24
- */
25
-
26
- type GtagFn = (...args: unknown[]) => void
27
-
28
- function safeGtag(): GtagFn | null {
29
- if (typeof window === "undefined") return null
30
- const fn = (window as unknown as { gtag?: GtagFn }).gtag
31
- return typeof fn === "function" ? fn : null
32
- }
33
-
34
- /**
35
- * Build the `send_to` Google Ads expects for the purchase conversion:
36
- * "AW-XXXXXXXXXX/Label".
37
- *
38
- * Returns null when either half is missing, so callers skip the
39
- * conversion entirely instead of firing a malformed `send_to` that Google
40
- * accepts and silently drops. The label is genuinely optional in the
41
- * admin: a merchant can save the account id before creating the
42
- * conversion action.
43
- */
44
- export function googleAdsPurchaseSendTo(config: TrackingConfig): string | null {
45
- const conversionId = config.googleAds?.conversionId?.trim()
46
- const label = config.googleAds?.conversionLabel?.trim()
47
- if (!conversionId || !label) return null
48
- return `${conversionId}/${label}`
49
- }
50
-
51
- export type GoogleAdsPurchaseInput = {
52
- /** Full send_to value from `googleAdsPurchaseSendTo`. */
53
- sendTo: string
54
- /** Order total actually collected, in `currency`. */
55
- value: number
56
- /** ISO 4217, e.g. "EUR". */
57
- currency: string
58
- /** THE dedup key. Always `String(order.display_id)`. */
59
- transactionId: string
60
- /**
61
- * Whether this order is the buyer's first. Computed once server-side by
62
- * the order.placed forwarder and persisted on the order, so the browser
63
- * and the server cannot disagree. Omitted entirely when unknown: a
64
- * wrong value is worse than an absent one for new-customer bidding.
65
- */
66
- newCustomer?: boolean
67
- }
68
-
69
- export function trackGoogleAdsPurchase(input: GoogleAdsPurchaseInput): void {
70
- const gtag = safeGtag()
71
- if (!gtag || !input.sendTo) return
72
-
73
- const payload: Record<string, unknown> = {
74
- send_to: input.sendTo,
75
- value: input.value,
76
- currency: input.currency,
77
- transaction_id: input.transactionId,
78
- }
79
- if (typeof input.newCustomer === "boolean") {
80
- payload.new_customer = input.newCustomer
81
- }
82
-
83
- gtag("event", "conversion", payload)
84
- }
1
+ "use client"
2
+
3
+ import type { TrackingConfig } from "./types"
4
+
5
+ /**
6
+ * Google Ads conversion tracking — browser side.
7
+ *
8
+ * Rides the SAME gtag instance <GoogleTag> loads for GA4; there is
9
+ * deliberately no loader in this file (see the comment block in ./ga4.tsx
10
+ * and https://support.google.com/google-ads/answer/2476688).
11
+ *
12
+ * DEDUP, and it is the whole reason this file is careful. Google Ads
13
+ * collapses two conversions that share a conversion action AND a
14
+ * `transaction_id` (https://support.google.com/google-ads/answer/6386790).
15
+ * Every vendor snippet ships `'transaction_id': ''`, and an empty value
16
+ * dedupes nothing — so every re-open of the confirmation page counts
17
+ * another purchase. That is not a hypothetical: order emails link back to
18
+ * the confirmation page, so the page is re-opened days after the sale, by
19
+ * the same buyer, routinely. We always send `String(order.display_id)`,
20
+ * the same key GA4's Measurement Protocol already uses.
21
+ *
22
+ * No-ops when gtag is absent (Ads not configured, server render, blocked
23
+ * script), so callers need no guards.
24
+ */
25
+
26
+ type GtagFn = (...args: unknown[]) => void
27
+
28
+ function safeGtag(): GtagFn | null {
29
+ if (typeof window === "undefined") return null
30
+ const fn = (window as unknown as { gtag?: GtagFn }).gtag
31
+ return typeof fn === "function" ? fn : null
32
+ }
33
+
34
+ /**
35
+ * Build the `send_to` Google Ads expects for the purchase conversion:
36
+ * "AW-XXXXXXXXXX/Label".
37
+ *
38
+ * Returns null when either half is missing, so callers skip the
39
+ * conversion entirely instead of firing a malformed `send_to` that Google
40
+ * accepts and silently drops. The label is genuinely optional in the
41
+ * admin: a merchant can save the account id before creating the
42
+ * conversion action.
43
+ */
44
+ export function googleAdsPurchaseSendTo(config: TrackingConfig): string | null {
45
+ const conversionId = config.googleAds?.conversionId?.trim()
46
+ const label = config.googleAds?.conversionLabel?.trim()
47
+ if (!conversionId || !label) return null
48
+ return `${conversionId}/${label}`
49
+ }
50
+
51
+ export type GoogleAdsPurchaseInput = {
52
+ /** Full send_to value from `googleAdsPurchaseSendTo`. */
53
+ sendTo: string
54
+ /** Order total actually collected, in `currency`. */
55
+ value: number
56
+ /** ISO 4217, e.g. "EUR". */
57
+ currency: string
58
+ /** THE dedup key. Always `String(order.display_id)`. */
59
+ transactionId: string
60
+ /**
61
+ * Whether this order is the buyer's first. Computed once server-side by
62
+ * the order.placed forwarder and persisted on the order, so the browser
63
+ * and the server cannot disagree. Omitted entirely when unknown: a
64
+ * wrong value is worse than an absent one for new-customer bidding.
65
+ */
66
+ newCustomer?: boolean
67
+ }
68
+
69
+ export function trackGoogleAdsPurchase(input: GoogleAdsPurchaseInput): void {
70
+ const gtag = safeGtag()
71
+ if (!gtag || !input.sendTo) return
72
+
73
+ const payload: Record<string, unknown> = {
74
+ send_to: input.sendTo,
75
+ value: input.value,
76
+ currency: input.currency,
77
+ transaction_id: input.transactionId,
78
+ }
79
+ if (typeof input.newCustomer === "boolean") {
80
+ payload.new_customer = input.newCustomer
81
+ }
82
+
83
+ gtag("event", "conversion", payload)
84
+ }
@@ -1,60 +1,60 @@
1
- "use client"
2
-
3
- import Script from "next/script"
4
-
5
- import { jsStringLiteral, urlParam } from "./inline-script"
6
-
7
- /**
8
- * Gtm — Google Tag Manager container.
9
- *
10
- * The hub has served `tracking.gtm.containerId` to storefronts since the
11
- * tracking card shipped, and until now nothing in this package mounted
12
- * it, so a merchant who configured GTM got a container id in their page
13
- * data and no container. Same shape of defect as Google Ads was.
14
- *
15
- * GTM is the merchant's own escape hatch: whatever tag we have not built
16
- * a first-class integration for, they can deploy through their container
17
- * without waiting for us. That makes it worth mounting properly rather
18
- * than treating it as a lesser vendor.
19
- *
20
- * Consent: <ConsentInit> sets the Consent Mode v2 defaults synchronously
21
- * before this loads, so tags inside the container inherit the gate, and
22
- * `applyConsent()` pushes the visitor's decision to the same dataLayer.
23
- * Nothing extra to wire here.
24
- *
25
- * The <noscript> iframe is part of Google's documented snippet and is
26
- * what makes the container work for a scriptless visitor. Next renders
27
- * it in <body>, which is where Google puts it.
28
- *
29
- * Renders nothing when `containerId` is falsy.
30
- */
31
- export function Gtm({ containerId }: { containerId?: string }) {
32
- if (!containerId) return null
33
-
34
- const initSnippet = `
35
- (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
36
- new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
37
- j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
38
- 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
39
- })(window,document,'script','dataLayer',${jsStringLiteral(containerId)});
40
- `.trim()
41
-
42
- return (
43
- <>
44
- <Script
45
- id="gtm-init"
46
- strategy="afterInteractive"
47
- dangerouslySetInnerHTML={{ __html: initSnippet }}
48
- />
49
- <noscript>
50
- <iframe
51
- src={`https://www.googletagmanager.com/ns.html?id=${urlParam(containerId)}`}
52
- height="0"
53
- width="0"
54
- style={{ display: "none", visibility: "hidden" }}
55
- title="Google Tag Manager"
56
- />
57
- </noscript>
58
- </>
59
- )
60
- }
1
+ "use client"
2
+
3
+ import Script from "next/script"
4
+
5
+ import { jsStringLiteral, urlParam } from "./inline-script"
6
+
7
+ /**
8
+ * Gtm — Google Tag Manager container.
9
+ *
10
+ * The hub has served `tracking.gtm.containerId` to storefronts since the
11
+ * tracking card shipped, and until now nothing in this package mounted
12
+ * it, so a merchant who configured GTM got a container id in their page
13
+ * data and no container. Same shape of defect as Google Ads was.
14
+ *
15
+ * GTM is the merchant's own escape hatch: whatever tag we have not built
16
+ * a first-class integration for, they can deploy through their container
17
+ * without waiting for us. That makes it worth mounting properly rather
18
+ * than treating it as a lesser vendor.
19
+ *
20
+ * Consent: <ConsentInit> sets the Consent Mode v2 defaults synchronously
21
+ * before this loads, so tags inside the container inherit the gate, and
22
+ * `applyConsent()` pushes the visitor's decision to the same dataLayer.
23
+ * Nothing extra to wire here.
24
+ *
25
+ * The <noscript> iframe is part of Google's documented snippet and is
26
+ * what makes the container work for a scriptless visitor. Next renders
27
+ * it in <body>, which is where Google puts it.
28
+ *
29
+ * Renders nothing when `containerId` is falsy.
30
+ */
31
+ export function Gtm({ containerId }: { containerId?: string }) {
32
+ if (!containerId) return null
33
+
34
+ const initSnippet = `
35
+ (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
36
+ new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
37
+ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
38
+ 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
39
+ })(window,document,'script','dataLayer',${jsStringLiteral(containerId)});
40
+ `.trim()
41
+
42
+ return (
43
+ <>
44
+ <Script
45
+ id="gtm-init"
46
+ strategy="afterInteractive"
47
+ dangerouslySetInnerHTML={{ __html: initSnippet }}
48
+ />
49
+ <noscript>
50
+ <iframe
51
+ src={`https://www.googletagmanager.com/ns.html?id=${urlParam(containerId)}`}
52
+ height="0"
53
+ width="0"
54
+ style={{ display: "none", visibility: "hidden" }}
55
+ title="Google Tag Manager"
56
+ />
57
+ </noscript>
58
+ </>
59
+ )
60
+ }
@@ -1,49 +1,49 @@
1
- /**
2
- * Safe interpolation of a configured id into a vendor's inline snippet.
3
- *
4
- * Every tag component here injects the vendor's own bootstrap through
5
- * `dangerouslySetInnerHTML`, with the merchant's pixel / measurement /
6
- * conversion id written into it. Those ids arrive from the store's
7
- * integration settings, which means they are typed by a person in the
8
- * admin, and a value carrying a quote would close the string literal and
9
- * run whatever follows it on every page of that storefront.
10
- *
11
- * On a single-merchant site that is only self-inflicted. On a platform it
12
- * is one tenant's admin user writing script into that tenant's public
13
- * storefront, so the escape is not optional.
14
- *
15
- * Returns the value as a COMPLETE single-quoted JavaScript literal,
16
- * quotes included, so callers cannot forget to quote it themselves:
17
- *
18
- * `fbq('init', ${jsStringLiteral(pixelId)});`
19
- */
20
- const LINE_SEPARATORS = new RegExp("[\u2028\u2029]", "g")
21
-
22
- export function jsStringLiteral(value: string): string {
23
- const escaped = String(value)
24
- .replace(/\\/g, "\\\\")
25
- .replace(/'/g, "\\'")
26
- .replace(/\r/g, "\\r")
27
- .replace(/\n/g, "\\n")
28
- // `</script` inside an inline script closes the element wherever it
29
- // appears, quoted or not.
30
- .replace(/<\//g, "<\\/")
31
- // U+2028 and U+2029 are line terminators to a JavaScript parser, so
32
- // they break a string literal exactly like a newline does. The class
33
- // is built from escapes rather than written literally, because a
34
- // source file carrying those characters raw is its own version of
35
- // this problem (it breaks the file that fixes it).
36
- .replace(LINE_SEPARATORS, (ch) =>
37
- ch.charCodeAt(0) === 0x2028 ? "\\u2028" : "\\u2029"
38
- )
39
- return `'${escaped}'`
40
- }
41
-
42
- /**
43
- * The same value for a URL query parameter (the loader `src`), where the
44
- * hazard is a crafted id breaking out of the parameter rather than out of
45
- * a string literal.
46
- */
47
- export function urlParam(value: string): string {
48
- return encodeURIComponent(String(value))
49
- }
1
+ /**
2
+ * Safe interpolation of a configured id into a vendor's inline snippet.
3
+ *
4
+ * Every tag component here injects the vendor's own bootstrap through
5
+ * `dangerouslySetInnerHTML`, with the merchant's pixel / measurement /
6
+ * conversion id written into it. Those ids arrive from the store's
7
+ * integration settings, which means they are typed by a person in the
8
+ * admin, and a value carrying a quote would close the string literal and
9
+ * run whatever follows it on every page of that storefront.
10
+ *
11
+ * On a single-merchant site that is only self-inflicted. On a platform it
12
+ * is one tenant's admin user writing script into that tenant's public
13
+ * storefront, so the escape is not optional.
14
+ *
15
+ * Returns the value as a COMPLETE single-quoted JavaScript literal,
16
+ * quotes included, so callers cannot forget to quote it themselves:
17
+ *
18
+ * `fbq('init', ${jsStringLiteral(pixelId)});`
19
+ */
20
+ const LINE_SEPARATORS = new RegExp("[\u2028\u2029]", "g")
21
+
22
+ export function jsStringLiteral(value: string): string {
23
+ const escaped = String(value)
24
+ .replace(/\\/g, "\\\\")
25
+ .replace(/'/g, "\\'")
26
+ .replace(/\r/g, "\\r")
27
+ .replace(/\n/g, "\\n")
28
+ // `</script` inside an inline script closes the element wherever it
29
+ // appears, quoted or not.
30
+ .replace(/<\//g, "<\\/")
31
+ // U+2028 and U+2029 are line terminators to a JavaScript parser, so
32
+ // they break a string literal exactly like a newline does. The class
33
+ // is built from escapes rather than written literally, because a
34
+ // source file carrying those characters raw is its own version of
35
+ // this problem (it breaks the file that fixes it).
36
+ .replace(LINE_SEPARATORS, (ch) =>
37
+ ch.charCodeAt(0) === 0x2028 ? "\\u2028" : "\\u2029"
38
+ )
39
+ return `'${escaped}'`
40
+ }
41
+
42
+ /**
43
+ * The same value for a URL query parameter (the loader `src`), where the
44
+ * hazard is a crafted id breaking out of the parameter rather than out of
45
+ * a string literal.
46
+ */
47
+ export function urlParam(value: string): string {
48
+ return encodeURIComponent(String(value))
49
+ }