@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,206 +1,206 @@
1
- "use client"
2
-
3
- /**
4
- * Typed wrappers around the global `oaiq` command queue from the OpenAI
5
- * measurement pixel — the ChatGPT Ads twin of ./ttq.ts and ./fbq.ts, and
6
- * deliberately the same shape so a surface that fires one can fire the
7
- * other on the line below.
8
- *
9
- * Every helper no-ops when `window.oaiq` is absent (pixel not configured,
10
- * server render, script blocked), so callers need no guards.
11
- *
12
- * The call signature is theirs, from
13
- * https://developers.openai.com/ads/measurement-pixel:
14
- *
15
- * oaiq("measure", eventName, eventData, { event_id })
16
- *
17
- * Two things here are NOT like the other vendors, and both are silent when
18
- * wrong:
19
- *
20
- * 1. MONEY IS AN INTEGER IN MINOR UNITS. Meta, TikTok and GA4 all read
21
- * `value: 25.99`; OpenAI reads `amount: 2599`. Passing a decimal
22
- * under-reports revenue by a factor of a hundred with nothing to show
23
- * for it, so every amount goes through `minorUnits` below and no
24
- * caller passes `amount` itself.
25
- * 2. EVENT DATA CARRIES ITS OWN `type`. Each event name is bound to one
26
- * of four data shapes; the five commerce events we fire all take
27
- * `contents`, so the wrappers set it and callers cannot get it wrong.
28
- *
29
- * Event names are theirs and are not interchangeable with Meta's or
30
- * TikTok's: `contents_viewed`, `items_added`, `checkout_started`,
31
- * `order_created`, `page_viewed`.
32
- */
33
-
34
- /** One product line inside `contents`. */
35
- export type OpenAiContentItem = {
36
- id: string
37
- name?: string
38
- content_type?: string
39
- quantity?: number
40
- /** Integer, minor units. */
41
- amount?: number
42
- currency?: string
43
- }
44
-
45
- type OpenAiEventData = {
46
- type: "contents"
47
- /** Integer, minor units. */
48
- amount?: number
49
- currency?: string
50
- contents?: OpenAiContentItem[]
51
- }
52
-
53
- type OaiqFn = (
54
- command: "init" | "measure" | "consent",
55
- ...args: unknown[]
56
- ) => void
57
-
58
- function safeOaiq(): OaiqFn | null {
59
- if (typeof window === "undefined") return null
60
- const oaiq = (window as unknown as { oaiq?: OaiqFn }).oaiq
61
- return typeof oaiq === "function" ? oaiq : null
62
- }
63
-
64
- /**
65
- * Major units to their integer minor units: 25.99 becomes 2599.
66
- *
67
- * The twin of `openAiMinorUnits` in the platform's
68
- * src/lib/tracking/constants.ts. Duplicated rather than imported because
69
- * this package cannot reach into the platform, and rounded rather than
70
- * truncated because 0.1 + 0.2 is 0.30000000000000004 in binary floating
71
- * point and truncation would report 29 cents for 30.
72
- */
73
- export function minorUnits(amountInMajorUnits: number): number {
74
- return Math.round((Number(amountInMajorUnits) || 0) * 100)
75
- }
76
-
77
- /**
78
- * THE order dedup key — must equal what the backend forwarder sends
79
- * (`openAiOrderEventId` in the platform's src/lib/tracking/constants.ts).
80
- * Built from display_id here so the format cannot drift between the two
81
- * sides, the same guarantee `trackPurchase` gives for Meta and
82
- * `tiktokPurchaseEventId` for TikTok.
83
- *
84
- * OpenAI deduplicates on pixel id + event name + id, keeping the FIRST
85
- * event it receives, so the browser and the server collapse into one
86
- * conversion whichever arrives first.
87
- */
88
- export function openAiOrderEventId(displayId: string | number): string {
89
- return `oai_order_${displayId}`
90
- }
91
-
92
- /** A page view. Their pixel fires nothing on its own at init. */
93
- export function trackOpenAiPageView(): void {
94
- const oaiq = safeOaiq()
95
- if (!oaiq) return
96
- oaiq("measure", "page_viewed", { type: "contents" } satisfies OpenAiEventData)
97
- }
98
-
99
- export function trackOpenAiContentsViewed(data: {
100
- contentId: string
101
- contentName?: string
102
- currency: string
103
- /** Major units — converted here. */
104
- value: number
105
- }): void {
106
- const oaiq = safeOaiq()
107
- if (!oaiq) return
108
- oaiq("measure", "contents_viewed", {
109
- type: "contents",
110
- amount: minorUnits(data.value),
111
- currency: data.currency,
112
- contents: [
113
- {
114
- id: data.contentId,
115
- name: data.contentName,
116
- content_type: "product",
117
- quantity: 1,
118
- },
119
- ],
120
- } satisfies OpenAiEventData)
121
- }
122
-
123
- export function trackOpenAiItemsAdded(data: {
124
- contentId: string
125
- contentName?: string
126
- quantity: number
127
- /** Unit price, major units. */
128
- price: number
129
- currency: string
130
- /** Line value, major units. */
131
- value: number
132
- }): void {
133
- const oaiq = safeOaiq()
134
- if (!oaiq) return
135
- oaiq("measure", "items_added", {
136
- type: "contents",
137
- amount: minorUnits(data.value),
138
- currency: data.currency,
139
- contents: [
140
- {
141
- id: data.contentId,
142
- name: data.contentName,
143
- content_type: "product",
144
- quantity: data.quantity,
145
- amount: minorUnits(data.price),
146
- currency: data.currency,
147
- },
148
- ],
149
- } satisfies OpenAiEventData)
150
- }
151
-
152
- export function trackOpenAiCheckoutStarted(data: {
153
- contents: OpenAiContentItem[]
154
- currency: string
155
- /** Cart value, major units. */
156
- value: number
157
- }): void {
158
- const oaiq = safeOaiq()
159
- if (!oaiq) return
160
- oaiq("measure", "checkout_started", {
161
- type: "contents",
162
- amount: minorUnits(data.value),
163
- currency: data.currency,
164
- contents: data.contents,
165
- } satisfies OpenAiEventData)
166
- }
167
-
168
- export function trackOpenAiOrderCreated(data: {
169
- contents: OpenAiContentItem[]
170
- currency: string
171
- /** Order total, major units. */
172
- value: number
173
- /** The order's display id — the dedup key shared with the server. */
174
- displayId: string | number
175
- }): void {
176
- const oaiq = safeOaiq()
177
- if (!oaiq) return
178
- oaiq(
179
- "measure",
180
- "order_created",
181
- {
182
- type: "contents",
183
- amount: minorUnits(data.value),
184
- currency: data.currency,
185
- contents: data.contents,
186
- } satisfies OpenAiEventData,
187
- { event_id: openAiOrderEventId(data.displayId) }
188
- )
189
- }
190
-
191
- /**
192
- * Push a live consent decision to the pixel.
193
- *
194
- * Normally you do NOT call this: `applyConsent()` in ./consent.ts relays
195
- * every decision to gtag, fbq, ttq and oaiq together. Exported for a host
196
- * app driving the pixel from a consent layer of its own.
197
- *
198
- * Their own gate, from the measurement-pixel docs: with consent false the
199
- * pixel sends no measurement pings, and the default is TRUE, which is why
200
- * the loader closes it explicitly before init rather than trusting it.
201
- */
202
- export function applyOpenAiConsent(adsGranted: boolean): void {
203
- const oaiq = safeOaiq()
204
- if (!oaiq) return
205
- oaiq("consent", adsGranted)
206
- }
1
+ "use client"
2
+
3
+ /**
4
+ * Typed wrappers around the global `oaiq` command queue from the OpenAI
5
+ * measurement pixel — the ChatGPT Ads twin of ./ttq.ts and ./fbq.ts, and
6
+ * deliberately the same shape so a surface that fires one can fire the
7
+ * other on the line below.
8
+ *
9
+ * Every helper no-ops when `window.oaiq` is absent (pixel not configured,
10
+ * server render, script blocked), so callers need no guards.
11
+ *
12
+ * The call signature is theirs, from
13
+ * https://developers.openai.com/ads/measurement-pixel:
14
+ *
15
+ * oaiq("measure", eventName, eventData, { event_id })
16
+ *
17
+ * Two things here are NOT like the other vendors, and both are silent when
18
+ * wrong:
19
+ *
20
+ * 1. MONEY IS AN INTEGER IN MINOR UNITS. Meta, TikTok and GA4 all read
21
+ * `value: 25.99`; OpenAI reads `amount: 2599`. Passing a decimal
22
+ * under-reports revenue by a factor of a hundred with nothing to show
23
+ * for it, so every amount goes through `minorUnits` below and no
24
+ * caller passes `amount` itself.
25
+ * 2. EVENT DATA CARRIES ITS OWN `type`. Each event name is bound to one
26
+ * of four data shapes; the five commerce events we fire all take
27
+ * `contents`, so the wrappers set it and callers cannot get it wrong.
28
+ *
29
+ * Event names are theirs and are not interchangeable with Meta's or
30
+ * TikTok's: `contents_viewed`, `items_added`, `checkout_started`,
31
+ * `order_created`, `page_viewed`.
32
+ */
33
+
34
+ /** One product line inside `contents`. */
35
+ export type OpenAiContentItem = {
36
+ id: string
37
+ name?: string
38
+ content_type?: string
39
+ quantity?: number
40
+ /** Integer, minor units. */
41
+ amount?: number
42
+ currency?: string
43
+ }
44
+
45
+ type OpenAiEventData = {
46
+ type: "contents"
47
+ /** Integer, minor units. */
48
+ amount?: number
49
+ currency?: string
50
+ contents?: OpenAiContentItem[]
51
+ }
52
+
53
+ type OaiqFn = (
54
+ command: "init" | "measure" | "consent",
55
+ ...args: unknown[]
56
+ ) => void
57
+
58
+ function safeOaiq(): OaiqFn | null {
59
+ if (typeof window === "undefined") return null
60
+ const oaiq = (window as unknown as { oaiq?: OaiqFn }).oaiq
61
+ return typeof oaiq === "function" ? oaiq : null
62
+ }
63
+
64
+ /**
65
+ * Major units to their integer minor units: 25.99 becomes 2599.
66
+ *
67
+ * The twin of `openAiMinorUnits` in the platform's
68
+ * src/lib/tracking/constants.ts. Duplicated rather than imported because
69
+ * this package cannot reach into the platform, and rounded rather than
70
+ * truncated because 0.1 + 0.2 is 0.30000000000000004 in binary floating
71
+ * point and truncation would report 29 cents for 30.
72
+ */
73
+ export function minorUnits(amountInMajorUnits: number): number {
74
+ return Math.round((Number(amountInMajorUnits) || 0) * 100)
75
+ }
76
+
77
+ /**
78
+ * THE order dedup key — must equal what the backend forwarder sends
79
+ * (`openAiOrderEventId` in the platform's src/lib/tracking/constants.ts).
80
+ * Built from display_id here so the format cannot drift between the two
81
+ * sides, the same guarantee `trackPurchase` gives for Meta and
82
+ * `tiktokPurchaseEventId` for TikTok.
83
+ *
84
+ * OpenAI deduplicates on pixel id + event name + id, keeping the FIRST
85
+ * event it receives, so the browser and the server collapse into one
86
+ * conversion whichever arrives first.
87
+ */
88
+ export function openAiOrderEventId(displayId: string | number): string {
89
+ return `oai_order_${displayId}`
90
+ }
91
+
92
+ /** A page view. Their pixel fires nothing on its own at init. */
93
+ export function trackOpenAiPageView(): void {
94
+ const oaiq = safeOaiq()
95
+ if (!oaiq) return
96
+ oaiq("measure", "page_viewed", { type: "contents" } satisfies OpenAiEventData)
97
+ }
98
+
99
+ export function trackOpenAiContentsViewed(data: {
100
+ contentId: string
101
+ contentName?: string
102
+ currency: string
103
+ /** Major units — converted here. */
104
+ value: number
105
+ }): void {
106
+ const oaiq = safeOaiq()
107
+ if (!oaiq) return
108
+ oaiq("measure", "contents_viewed", {
109
+ type: "contents",
110
+ amount: minorUnits(data.value),
111
+ currency: data.currency,
112
+ contents: [
113
+ {
114
+ id: data.contentId,
115
+ name: data.contentName,
116
+ content_type: "product",
117
+ quantity: 1,
118
+ },
119
+ ],
120
+ } satisfies OpenAiEventData)
121
+ }
122
+
123
+ export function trackOpenAiItemsAdded(data: {
124
+ contentId: string
125
+ contentName?: string
126
+ quantity: number
127
+ /** Unit price, major units. */
128
+ price: number
129
+ currency: string
130
+ /** Line value, major units. */
131
+ value: number
132
+ }): void {
133
+ const oaiq = safeOaiq()
134
+ if (!oaiq) return
135
+ oaiq("measure", "items_added", {
136
+ type: "contents",
137
+ amount: minorUnits(data.value),
138
+ currency: data.currency,
139
+ contents: [
140
+ {
141
+ id: data.contentId,
142
+ name: data.contentName,
143
+ content_type: "product",
144
+ quantity: data.quantity,
145
+ amount: minorUnits(data.price),
146
+ currency: data.currency,
147
+ },
148
+ ],
149
+ } satisfies OpenAiEventData)
150
+ }
151
+
152
+ export function trackOpenAiCheckoutStarted(data: {
153
+ contents: OpenAiContentItem[]
154
+ currency: string
155
+ /** Cart value, major units. */
156
+ value: number
157
+ }): void {
158
+ const oaiq = safeOaiq()
159
+ if (!oaiq) return
160
+ oaiq("measure", "checkout_started", {
161
+ type: "contents",
162
+ amount: minorUnits(data.value),
163
+ currency: data.currency,
164
+ contents: data.contents,
165
+ } satisfies OpenAiEventData)
166
+ }
167
+
168
+ export function trackOpenAiOrderCreated(data: {
169
+ contents: OpenAiContentItem[]
170
+ currency: string
171
+ /** Order total, major units. */
172
+ value: number
173
+ /** The order's display id — the dedup key shared with the server. */
174
+ displayId: string | number
175
+ }): void {
176
+ const oaiq = safeOaiq()
177
+ if (!oaiq) return
178
+ oaiq(
179
+ "measure",
180
+ "order_created",
181
+ {
182
+ type: "contents",
183
+ amount: minorUnits(data.value),
184
+ currency: data.currency,
185
+ contents: data.contents,
186
+ } satisfies OpenAiEventData,
187
+ { event_id: openAiOrderEventId(data.displayId) }
188
+ )
189
+ }
190
+
191
+ /**
192
+ * Push a live consent decision to the pixel.
193
+ *
194
+ * Normally you do NOT call this: `applyConsent()` in ./consent.ts relays
195
+ * every decision to gtag, fbq, ttq and oaiq together. Exported for a host
196
+ * app driving the pixel from a consent layer of its own.
197
+ *
198
+ * Their own gate, from the measurement-pixel docs: with consent false the
199
+ * pixel sends no measurement pings, and the default is TRUE, which is why
200
+ * the loader closes it explicitly before init rather than trusting it.
201
+ */
202
+ export function applyOpenAiConsent(adsGranted: boolean): void {
203
+ const oaiq = safeOaiq()
204
+ if (!oaiq) return
205
+ oaiq("consent", adsGranted)
206
+ }
@@ -1,91 +1,91 @@
1
- "use client"
2
-
3
- import Script from "next/script"
4
-
5
- import { adsConsentSnippet } from "./consent"
6
- import { jsStringLiteral } from "./inline-script"
7
-
8
- /**
9
- * TikTokPixel — client-side base pixel injector, the TikTok twin of
10
- * <MetaPixel>.
11
- *
12
- * Injects TikTok's own base snippet with `<Script
13
- * strategy="afterInteractive">`, resolves consent from the `_1c_consent`
14
- * cookie BEFORE the pixel is allowed to write cookies, then loads and
15
- * fires the initial page view.
16
- *
17
- * Consent model. TikTok's base code registers `holdConsent`,
18
- * `grantConsent` and `revokeConsent` on `ttq` alongside `page` and
19
- * `track`, and `setAndDefer` queues every call onto the `ttq` array
20
- * before the SDK is fetched. So the Meta pattern transfers exactly:
21
- *
22
- * ttq.holdConsent() // queued before load — cookies withheld
23
- * ttq.load(pixelId)
24
- * ttq.page()
25
- * ttq.grantConsent() // visitor accepted marketing
26
- * ttq.revokeConsent() // visitor declined, or withdrew later
27
- *
28
- * HOLDING rather than withholding the script is the point, and it is the
29
- * same reasoning as the Meta gate above it: under load-on-consent, a
30
- * visitor who browses three products and only then accepts gives TikTok
31
- * nothing for those three pages. Holding queues them and the grant
32
- * releases them.
33
- *
34
- * Live decisions arrive through `applyConsent()` in ./consent.ts, which
35
- * relays to gtag, fbq and ttq together — there is no cookie watcher.
36
- * The first decision is the store's default overridden by the visitor's
37
- * stored choice (`adsConsentSnippet`): revoked on a store that collects
38
- * consent, granted on a store with no consent gate, the cookie winning
39
- * either way. `consentRequired` is `tracking.consent_required`, passed by
40
- * <StorefrontTags>, and required for the reason <ConsentInit> gives.
41
- *
42
- * One dependency outside the code: Cookie Consent Mode must be switched
43
- * on for this pixel in TikTok Events Manager. With it off, `holdConsent`
44
- * is an inert queued call and the pixel behaves like any unconsented
45
- * pixel — no error, no crash, and no gate. That switch belongs to whoever
46
- * owns the ad account.
47
- *
48
- * Renders nothing when `pixelId` is falsy, so a layout can mount it
49
- * unconditionally.
50
- */
51
- export function TikTokPixel({
52
- pixelId,
53
- consentRequired,
54
- }: {
55
- pixelId?: string
56
- /** `tracking.consent_required`: does this store collect consent before it tracks? */
57
- consentRequired: boolean
58
- }) {
59
- if (!pixelId) return null
60
-
61
- const id = jsStringLiteral(pixelId)
62
-
63
- const initSnippet = `
64
- !function (w, d, t) {
65
- w.TiktokAnalyticsObject=t;
66
- var ttq=w[t]=w[t]||[];
67
- ttq.methods=["page","track","identify","instances","debug","on","off","once","ready","alias","group","enableCookie","disableCookie","holdConsent","revokeConsent","grantConsent"];
68
- ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};
69
- for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);
70
- ttq.instance=function(t){var e=ttq._i[t]||[];for(var n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e};
71
- ttq.load=function(e,n){var r="https://analytics.tiktok.com/i18n/pixel/events.js";ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};var o=d.createElement("script");o.type="text/javascript",o.async=!0,o.src=r+"?sdkid="+e+"&lib="+t;var a=d.getElementsByTagName("script")[0];a.parentNode.insertBefore(o,a)};
72
-
73
- ttq.holdConsent();
74
- ttq.load(${id});
75
- ttq.page();
76
-
77
- (function(){
78
- ${adsConsentSnippet(consentRequired, "d")}
79
- if(ads){ttq.grantConsent();}else{ttq.revokeConsent();}
80
- })();
81
- }(window, document, 'ttq');
82
- `.trim()
83
-
84
- return (
85
- <Script
86
- id="tiktok-pixel-init"
87
- strategy="afterInteractive"
88
- dangerouslySetInnerHTML={{ __html: initSnippet }}
89
- />
90
- )
91
- }
1
+ "use client"
2
+
3
+ import Script from "next/script"
4
+
5
+ import { adsConsentSnippet } from "./consent"
6
+ import { jsStringLiteral } from "./inline-script"
7
+
8
+ /**
9
+ * TikTokPixel — client-side base pixel injector, the TikTok twin of
10
+ * <MetaPixel>.
11
+ *
12
+ * Injects TikTok's own base snippet with `<Script
13
+ * strategy="afterInteractive">`, resolves consent from the `_1c_consent`
14
+ * cookie BEFORE the pixel is allowed to write cookies, then loads and
15
+ * fires the initial page view.
16
+ *
17
+ * Consent model. TikTok's base code registers `holdConsent`,
18
+ * `grantConsent` and `revokeConsent` on `ttq` alongside `page` and
19
+ * `track`, and `setAndDefer` queues every call onto the `ttq` array
20
+ * before the SDK is fetched. So the Meta pattern transfers exactly:
21
+ *
22
+ * ttq.holdConsent() // queued before load — cookies withheld
23
+ * ttq.load(pixelId)
24
+ * ttq.page()
25
+ * ttq.grantConsent() // visitor accepted marketing
26
+ * ttq.revokeConsent() // visitor declined, or withdrew later
27
+ *
28
+ * HOLDING rather than withholding the script is the point, and it is the
29
+ * same reasoning as the Meta gate above it: under load-on-consent, a
30
+ * visitor who browses three products and only then accepts gives TikTok
31
+ * nothing for those three pages. Holding queues them and the grant
32
+ * releases them.
33
+ *
34
+ * Live decisions arrive through `applyConsent()` in ./consent.ts, which
35
+ * relays to gtag, fbq and ttq together — there is no cookie watcher.
36
+ * The first decision is the store's default overridden by the visitor's
37
+ * stored choice (`adsConsentSnippet`): revoked on a store that collects
38
+ * consent, granted on a store with no consent gate, the cookie winning
39
+ * either way. `consentRequired` is `tracking.consent_required`, passed by
40
+ * <StorefrontTags>, and required for the reason <ConsentInit> gives.
41
+ *
42
+ * One dependency outside the code: Cookie Consent Mode must be switched
43
+ * on for this pixel in TikTok Events Manager. With it off, `holdConsent`
44
+ * is an inert queued call and the pixel behaves like any unconsented
45
+ * pixel — no error, no crash, and no gate. That switch belongs to whoever
46
+ * owns the ad account.
47
+ *
48
+ * Renders nothing when `pixelId` is falsy, so a layout can mount it
49
+ * unconditionally.
50
+ */
51
+ export function TikTokPixel({
52
+ pixelId,
53
+ consentRequired,
54
+ }: {
55
+ pixelId?: string
56
+ /** `tracking.consent_required`: does this store collect consent before it tracks? */
57
+ consentRequired: boolean
58
+ }) {
59
+ if (!pixelId) return null
60
+
61
+ const id = jsStringLiteral(pixelId)
62
+
63
+ const initSnippet = `
64
+ !function (w, d, t) {
65
+ w.TiktokAnalyticsObject=t;
66
+ var ttq=w[t]=w[t]||[];
67
+ ttq.methods=["page","track","identify","instances","debug","on","off","once","ready","alias","group","enableCookie","disableCookie","holdConsent","revokeConsent","grantConsent"];
68
+ ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};
69
+ for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);
70
+ ttq.instance=function(t){var e=ttq._i[t]||[];for(var n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e};
71
+ ttq.load=function(e,n){var r="https://analytics.tiktok.com/i18n/pixel/events.js";ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};var o=d.createElement("script");o.type="text/javascript",o.async=!0,o.src=r+"?sdkid="+e+"&lib="+t;var a=d.getElementsByTagName("script")[0];a.parentNode.insertBefore(o,a)};
72
+
73
+ ttq.holdConsent();
74
+ ttq.load(${id});
75
+ ttq.page();
76
+
77
+ (function(){
78
+ ${adsConsentSnippet(consentRequired, "d")}
79
+ if(ads){ttq.grantConsent();}else{ttq.revokeConsent();}
80
+ })();
81
+ }(window, document, 'ttq');
82
+ `.trim()
83
+
84
+ return (
85
+ <Script
86
+ id="tiktok-pixel-init"
87
+ strategy="afterInteractive"
88
+ dangerouslySetInnerHTML={{ __html: initSnippet }}
89
+ />
90
+ )
91
+ }