@cartbase/storefront 0.20.0 → 0.20.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.
- package/LICENSE +21 -21
- package/package.json +258 -258
- package/src/api/auth.ts +108 -108
- package/src/api/carts.ts +523 -523
- package/src/api/categories.ts +184 -184
- package/src/api/checkout.ts +526 -526
- package/src/api/collections.ts +130 -130
- package/src/api/consent.ts +75 -75
- package/src/api/content.ts +125 -125
- package/src/api/customers.ts +303 -303
- package/src/api/gift-cards.ts +112 -112
- package/src/api/http.ts +180 -180
- package/src/api/index.ts +30 -30
- package/src/api/menus.ts +77 -77
- package/src/api/metaobjects.ts +136 -136
- package/src/api/orders.ts +290 -290
- package/src/api/redirects.ts +37 -37
- package/src/api/regions.ts +200 -200
- package/src/api/search.ts +163 -163
- package/src/api/store.ts +35 -35
- package/src/api/types.ts +91 -91
- package/src/cart-drawer/context.tsx +778 -778
- package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
- package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
- package/src/cart-drawer/gift-wrap.tsx +82 -82
- package/src/cart-drawer/item/index.tsx +162 -162
- package/src/cart-drawer/item/upsell.tsx +110 -110
- package/src/cart-drawer/labels.ts +123 -123
- package/src/cart-drawer/mutation-queue.ts +78 -78
- package/src/cart-drawer/notes.tsx +131 -131
- package/src/cart-drawer/sticky-footer.tsx +73 -73
- package/src/cart-drawer/summary-breakdown.tsx +197 -197
- package/src/checkout/address-error-copy.ts +117 -117
- package/src/checkout/boxnow-locker-selector.tsx +410 -410
- package/src/checkout/compare-addresses.ts +40 -40
- package/src/checkout/discount-section.tsx +218 -218
- package/src/checkout/error-copy-codes.ts +63 -63
- package/src/checkout/geocode.ts +154 -154
- package/src/checkout/gift-card-section.tsx +224 -224
- package/src/checkout/index.ts +72 -72
- package/src/checkout/labels.ts +495 -495
- package/src/checkout/payment-button.tsx +372 -372
- package/src/checkout/payment-error-copy.ts +154 -154
- package/src/checkout/promotion-error-copy.ts +91 -91
- package/src/common/country-flag.tsx +52 -52
- package/src/common/country-select.tsx +11 -11
- package/src/common/index.ts +20 -20
- package/src/common/market-select.tsx +57 -57
- package/src/index.ts +12 -12
- package/src/lib/country-name.ts +59 -59
- package/src/lib/get-product-price.ts +133 -133
- package/src/lib/media-image.tsx +39 -39
- package/src/lib/payment-constants.ts +53 -53
- package/src/lib/platform.ts +13 -13
- package/src/lib/price.tsx +39 -39
- package/src/lib/store-api-error.ts +36 -36
- package/src/lib/variant-caption.ts +32 -32
- package/src/locales/context.ts +37 -37
- package/src/locales/en.ts +26 -26
- package/src/locales/index.ts +19 -19
- package/src/locales/provider.tsx +59 -59
- package/src/locales/types.ts +77 -77
- package/src/order/index.ts +62 -62
- package/src/order/labels.ts +79 -79
- package/src/order/order-totals.tsx +250 -250
- package/src/primitives/select-field.tsx +93 -93
- package/src/products/image-gallery.tsx +43 -43
- package/src/products/option-select.tsx +67 -67
- package/src/products/product-info.tsx +5 -5
- package/src/products/product-price.tsx +69 -69
- package/src/products/product-promises.tsx +61 -61
- package/src/products/product-specs.tsx +65 -65
- package/src/products/product-tabs.tsx +123 -123
- package/src/products/purchase-options.tsx +130 -130
- package/src/products/sets.ts +110 -110
- package/src/products/variant-matching.ts +71 -71
- package/src/products/variant-url.ts +74 -74
- package/src/reviews-ui/lightbox-state.ts +46 -46
- package/src/reviews-ui/review-header.tsx +166 -166
- package/src/reviews-ui/review-lightbox.tsx +271 -271
- package/src/reviews-ui/review-list.tsx +193 -193
- package/src/reviews-ui/review-widget.tsx +219 -218
- package/src/reviews-ui/widget-options.ts +55 -55
- package/src/store/category-template.tsx +136 -136
- package/src/store/index.ts +40 -40
- package/src/tracking/chatgpt-pixel.tsx +99 -99
- package/src/tracking/consent-init.tsx +62 -62
- package/src/tracking/events.ts +348 -348
- package/src/tracking/ga4.tsx +93 -93
- package/src/tracking/google-ads.ts +84 -84
- package/src/tracking/gtm.tsx +60 -60
- package/src/tracking/inline-script.ts +49 -49
- package/src/tracking/oaiq.ts +206 -206
- package/src/tracking/tiktok-pixel.tsx +91 -91
- package/src/tracking/track-init.tsx +56 -56
- package/src/tracking/track-order-purchase.tsx +122 -122
- package/src/tracking/ttq.ts +180 -180
- package/src/tracking/use-tracking-config.ts +54 -54
- package/theme/index.css +25 -25
package/src/tracking/oaiq.ts
CHANGED
|
@@ -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
|
+
}
|