@cartbase/storefront 0.8.0 → 0.10.0

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 (50) hide show
  1. package/package.json +7 -8
  2. package/src/cart-drawer/cross-sell-carousel.tsx +2 -3
  3. package/src/cart-drawer/gift-wrap.tsx +82 -83
  4. package/src/cart-drawer/index.ts +0 -1
  5. package/src/cart-drawer/item/index.tsx +4 -6
  6. package/src/cart-drawer/sticky-footer.tsx +73 -73
  7. package/src/checkout/address-error-copy.ts +117 -119
  8. package/src/checkout/discount-section.tsx +218 -218
  9. package/src/checkout/error-copy-codes.ts +63 -0
  10. package/src/checkout/gift-card-section.tsx +4 -4
  11. package/src/checkout/index.ts +4 -5
  12. package/src/checkout/labels.ts +218 -0
  13. package/src/checkout/line-item-card.tsx +3 -3
  14. package/src/checkout/order-summary.tsx +11 -12
  15. package/src/checkout/payment-button.tsx +372 -373
  16. package/src/checkout/payment-error-copy.ts +154 -269
  17. package/src/checkout/promotion-error-copy.ts +91 -124
  18. package/src/checkout/shipping-method-list.tsx +2 -2
  19. package/src/checkout/use-checkout-orchestration.ts +10 -8
  20. package/src/lib/money.ts +1 -1
  21. package/src/lib/price.tsx +39 -0
  22. package/src/locales/bg.ts +468 -0
  23. package/src/locales/en.ts +26 -0
  24. package/src/locales/es.ts +467 -0
  25. package/src/locales/index.ts +19 -0
  26. package/src/locales/provider.tsx +63 -0
  27. package/src/locales/types.ts +49 -0
  28. package/src/order/index.ts +0 -1
  29. package/src/order/order-delivery-card.tsx +2 -2
  30. package/src/order/order-item.tsx +3 -3
  31. package/src/order/order-totals.tsx +4 -4
  32. package/src/products/index.ts +0 -1
  33. package/src/reviews-ui/index.ts +0 -1
  34. package/src/store/index.ts +0 -1
  35. package/src/tracking/attribution.ts +13 -0
  36. package/src/tracking/chatgpt-pixel.tsx +92 -0
  37. package/src/tracking/consent-banner.tsx +5 -1
  38. package/src/tracking/consent.ts +12 -0
  39. package/src/tracking/events.ts +56 -2
  40. package/src/tracking/index.ts +12 -0
  41. package/src/tracking/oaiq.ts +206 -0
  42. package/src/tracking/storefront-tags.tsx +2 -0
  43. package/src/tracking/types.ts +4 -0
  44. package/src/cart-drawer/labels-bg.ts +0 -72
  45. package/src/checkout/labels-bg.ts +0 -128
  46. package/src/lib/dual-price.tsx +0 -73
  47. package/src/order/labels-bg.ts +0 -39
  48. package/src/products/labels-bg.ts +0 -35
  49. package/src/reviews-ui/labels-bg.ts +0 -91
  50. package/src/store/labels-bg.ts +0 -22
@@ -1,4 +1,4 @@
1
- import { DualPrice } from "../lib/dual-price"
1
+ import { Price } from "../lib/price"
2
2
  import { findFeeLine, type LineLike } from "../lib/cart-helpers"
3
3
  import { defaultOrderLabels, type OrderLabels } from "./labels"
4
4
 
@@ -217,14 +217,14 @@ export function OrderTotals({
217
217
  ) : row.negative ? (
218
218
  <span className="text-sm text-success">
219
219
  -{" "}
220
- <DualPrice
220
+ <Price
221
221
  amount={row.amount}
222
222
  currencyCode={currencyCode}
223
223
  className="text-sm text-success"
224
224
  />
225
225
  </span>
226
226
  ) : (
227
- <DualPrice
227
+ <Price
228
228
  amount={row.amount}
229
229
  currencyCode={currencyCode}
230
230
  className="text-sm text-foreground"
@@ -237,7 +237,7 @@ export function OrderTotals({
237
237
  <div className="h-px bg-border my-3" />
238
238
  <div className="flex justify-between items-baseline">
239
239
  <span className="text-[15px] font-bold text-foreground">{l.total}</span>
240
- <DualPrice
240
+ <Price
241
241
  amount={total}
242
242
  currencyCode={currencyCode}
243
243
  className="text-xl font-bold text-foreground tracking-tight"
@@ -5,7 +5,6 @@
5
5
  */
6
6
  export { ProductLabelsProvider, useProductLabels } from "./context"
7
7
  export { defaultProductLabels, type ProductLabels } from "./labels"
8
- export { bulgarianProductLabels } from "./labels-bg"
9
8
 
10
9
  export {
11
10
  optionsAsKeymap,
@@ -10,7 +10,6 @@ export {
10
10
  defaultReviewsUiLabels,
11
11
  type ReviewsUiLabels,
12
12
  } from "./labels"
13
- export { bulgarianReviewsUiLabels } from "./labels-bg"
14
13
 
15
14
  export {
16
15
  formatLabel,
@@ -8,7 +8,6 @@ export {
8
8
  sortOptionLabelKeys,
9
9
  type StoreLabels,
10
10
  } from "./labels"
11
- export { bulgarianStoreLabels } from "./labels-bg"
12
11
 
13
12
  export { Pagination } from "./pagination"
14
13
  export { SortSelect, type SortSelectProps } from "./sort-select"
@@ -496,6 +496,16 @@ const TTCLID_COOKIE = "_1c_ttclid"
496
496
  const GCLID_COOKIE = "_1c_gclid"
497
497
  const GBRAID_COOKIE = "_1c_gbraid"
498
498
  const WBRAID_COOKIE = "_1c_wbraid"
499
+ /**
500
+ * OpenAI's click id, from `oppref` on the landing URL.
501
+ *
502
+ * Their own SDK stores the same parameter in a first-party `__oppref`
503
+ * cookie at init, and we capture it here anyway: the Conversions API takes
504
+ * `oppref` on the server event explicitly, and this module runs on the
505
+ * landing page whether or not their script loaded or was consented to. Ours
506
+ * is the one the server reads.
507
+ */
508
+ const OPPREF_COOKIE = "_1c_oppref"
499
509
 
500
510
  /** TikTok's ttclid lifetime follows the CTA window in Attribution
501
511
  * Manager; Google's conversion window tops out at 90 days. One TTL
@@ -509,6 +519,7 @@ const CLICK_ID_CAPTURE: ReadonlyArray<readonly [string, string]> = [
509
519
  [GCLID_COOKIE, "gclid"],
510
520
  [GBRAID_COOKIE, "gbraid"],
511
521
  [WBRAID_COOKIE, "wbraid"],
522
+ [OPPREF_COOKIE, "oppref"],
512
523
  ]
513
524
 
514
525
  /**
@@ -522,6 +533,7 @@ export const CLICK_ID_COOKIES = {
522
533
  gbraid: GBRAID_COOKIE,
523
534
  wbraid: WBRAID_COOKIE,
524
535
  ttp: "_ttp",
536
+ oppref: OPPREF_COOKIE,
525
537
  } as const
526
538
 
527
539
  /** cookie name → order.metadata key (code truth for the server side:
@@ -532,6 +544,7 @@ export const CLICK_ID_METADATA_KEYS: ReadonlyArray<readonly [string, string]> =
532
544
  [CLICK_ID_COOKIES.gclid, "google_gclid"],
533
545
  [CLICK_ID_COOKIES.gbraid, "google_gbraid"],
534
546
  [CLICK_ID_COOKIES.wbraid, "google_wbraid"],
547
+ [CLICK_ID_COOKIES.oppref, "oai_oppref"],
535
548
  ]
536
549
 
537
550
  /**
@@ -0,0 +1,92 @@
1
+ "use client"
2
+
3
+ import Script from "next/script"
4
+
5
+ import { CONSENT_COOKIE } from "./consent"
6
+ import { jsStringLiteral } from "./inline-script"
7
+
8
+ /**
9
+ * ChatGptPixel — the OpenAI measurement pixel, the ChatGPT Ads twin of
10
+ * <MetaPixel> and <TikTokPixel>.
11
+ *
12
+ * The loader is OpenAI's own, verbatim from
13
+ * https://developers.openai.com/ads/measurement-pixel: a command queue on
14
+ * `window.oaiq` that buffers calls until the SDK arrives from
15
+ * bzrcdn.openai.com, injected with `<Script strategy="afterInteractive">`.
16
+ *
17
+ * CONSENT IS CLOSED BEFORE INIT, and that order is the whole point. Their
18
+ * consent default is TRUE, so an unconsented visitor is measured unless we
19
+ * say otherwise first:
20
+ *
21
+ * oaiq("consent", false) // queued BEFORE init — no pings
22
+ * oaiq("init", { pixelId })
23
+ * oaiq("consent", true) // only once the visitor accepted
24
+ *
25
+ * The script is LOADED either way, like the Meta and TikTok gates beside
26
+ * it, so a later grant costs no round trip. But the page view is fired
27
+ * only AFTER a grant, and that is deliberate rather than cautious: their
28
+ * docs state that with consent false the pixel sends no measurement
29
+ * pings, and they do NOT document whether a call made while consent was
30
+ * false is released on a later grant. TikTok documents exactly that
31
+ * (`holdConsent` queues, `grantConsent` flushes) and our TikTok gate
32
+ * relies on it. Nothing here relies on behaviour OpenAI has not written
33
+ * down: an unconsented page view is not sent, and not counted on later.
34
+ *
35
+ * The decision is read from the shared `_1c_consent` cookie inside the
36
+ * snippet, before anything can be sent, and live changes arrive through
37
+ * `applyConsent()` in ./consent.ts — there is no cookie watcher.
38
+ *
39
+ * The page view is ours to fire at all: their pixel sends nothing at init,
40
+ * unlike Meta's and TikTok's base code.
41
+ *
42
+ * Their SDK also captures `oppref` off the landing URL into a first-party
43
+ * `__oppref` cookie by itself. Our attribution module captures the same
44
+ * parameter into `_1c_oppref` independently, so the server-side
45
+ * Conversions API event can carry the click even on a visit where this
46
+ * script never loaded at all.
47
+ *
48
+ * Renders nothing when `pixelId` is falsy, so a layout can mount it
49
+ * unconditionally.
50
+ */
51
+ export function ChatGptPixel({ pixelId }: { pixelId?: string }) {
52
+ if (!pixelId) return null
53
+
54
+ const id = jsStringLiteral(pixelId)
55
+
56
+ const initSnippet = `
57
+ (function (w, d, s, u) {
58
+ if (w.oaiq) return;
59
+ var q = function () { q.q.push(arguments); };
60
+ q.q = [];
61
+ w.oaiq = q;
62
+ var js = d.createElement(s);
63
+ js.async = true;
64
+ js.src = u;
65
+ var f = d.getElementsByTagName(s)[0];
66
+ f.parentNode.insertBefore(js, f);
67
+ })(window, document, "script", "https://bzrcdn.openai.com/sdk/oaiq.min.js");
68
+
69
+ oaiq("consent", false);
70
+ oaiq("init", { pixelId: ${id} });
71
+
72
+ (function (d) {
73
+ var ads = false;
74
+ try {
75
+ var m = d.cookie.match(/(?:^|; )${CONSENT_COOKIE}=([^;]*)/);
76
+ if (m) { ads = !!JSON.parse(decodeURIComponent(m[1])).ads; }
77
+ } catch (e) {}
78
+ if (ads) {
79
+ oaiq("consent", true);
80
+ oaiq("measure", "page_viewed", { type: "contents" });
81
+ }
82
+ })(document);
83
+ `.trim()
84
+
85
+ return (
86
+ <Script
87
+ id="chatgpt-pixel-init"
88
+ strategy="afterInteractive"
89
+ dangerouslySetInnerHTML={{ __html: initSnippet }}
90
+ />
91
+ )
92
+ }
@@ -341,7 +341,11 @@ function ConsentRow({
341
341
  * its settings layer ("withdraw consent as easily as it was given").
342
342
  */
343
343
  export function ConsentSettingsLink({
344
- children = "Настройки на бисквитките",
344
+ // English, like every other default in the library. It shipped as
345
+ // "Настройки на бисквитките" until 2026-09-13, which put Bulgarian in
346
+ // the footer of every store that mounted this link. Pass children to
347
+ // translate it; the consent banner has no label pack of its own.
348
+ children = "Cookie settings",
345
349
  className,
346
350
  }: {
347
351
  children?: React.ReactNode
@@ -131,6 +131,18 @@ export function applyConsent(choices: ConsentChoices): void {
131
131
  if (choices.ads) w.ttq.grantConsent()
132
132
  else w.ttq.revokeConsent()
133
133
  }
134
+
135
+ // ChatGPT Ads. One command on their own queue, `oaiq("consent", bool)`,
136
+ // which works before the SDK has arrived because the queue buffers it.
137
+ //
138
+ // A grant also fires the page view the loader withheld: their docs say
139
+ // an unconsented event sends no ping and do not promise that a held call
140
+ // is released later, so the visitor who accepts on their third page is
141
+ // counted from the grant onward rather than not at all.
142
+ if (typeof w.oaiq === "function") {
143
+ w.oaiq("consent", choices.ads)
144
+ if (choices.ads) w.oaiq("measure", "page_viewed", { type: "contents" })
145
+ }
134
146
  }
135
147
 
136
148
  /**
@@ -24,6 +24,14 @@ import {
24
24
  trackTikTokPurchase,
25
25
  trackTikTokViewContent,
26
26
  } from "./ttq"
27
+ import {
28
+ minorUnits,
29
+ trackOpenAiCheckoutStarted,
30
+ trackOpenAiContentsViewed,
31
+ trackOpenAiItemsAdded,
32
+ trackOpenAiOrderCreated,
33
+ type OpenAiContentItem,
34
+ } from "./oaiq"
27
35
  import { googleAdsPurchaseSendTo, trackGoogleAdsPurchase } from "./google-ads"
28
36
  import type { TrackingConfig } from "./types"
29
37
 
@@ -118,6 +126,25 @@ const tiktokContents = (lines: TrackedLine[]) =>
118
126
  const unitCount = (lines: TrackedLine[]) =>
119
127
  lines.reduce((sum, line) => sum + (Number(line.quantity) || 0), 0)
120
128
 
129
+ /**
130
+ * ChatGPT Ads contents. The shape is close to TikTok's, but every amount
131
+ * is an INTEGER IN MINOR UNITS on this vendor, which is why the mapping
132
+ * cannot be shared with `tiktokContents` above: the same numbers in the
133
+ * same field names would be a hundredfold under-report.
134
+ */
135
+ const openAiContents = (
136
+ lines: TrackedLine[],
137
+ currency: string
138
+ ): OpenAiContentItem[] =>
139
+ lines.map((line) => ({
140
+ id: line.productId,
141
+ name: line.title,
142
+ content_type: "product",
143
+ quantity: line.quantity,
144
+ amount: minorUnits(line.price),
145
+ currency,
146
+ }))
147
+
121
148
  /** Product page view. */
122
149
  export function trackProductView(input: {
123
150
  line: TrackedLine
@@ -137,6 +164,12 @@ export function trackProductView(input: {
137
164
  currency,
138
165
  value,
139
166
  })
167
+ trackOpenAiContentsViewed({
168
+ contentId: line.productId,
169
+ contentName: line.title,
170
+ currency,
171
+ value,
172
+ })
140
173
  trackGAViewItem({ currency, value, items: gaItems([line], currency) })
141
174
  trackRybbitViewItem({
142
175
  item_id: line.variantId || line.productId,
@@ -168,6 +201,14 @@ export function trackCartAdd(input: {
168
201
  currency,
169
202
  value,
170
203
  })
204
+ trackOpenAiItemsAdded({
205
+ contentId: line.productId,
206
+ contentName: line.title,
207
+ quantity: line.quantity,
208
+ price: line.price,
209
+ currency,
210
+ value,
211
+ })
171
212
  trackGAAddToCart({ currency, value, items: gaItems([line], currency) })
172
213
  trackRybbitAddToCart({
173
214
  item_id: line.variantId || line.productId,
@@ -199,6 +240,11 @@ export function trackCheckoutStart(input: {
199
240
  currency,
200
241
  value,
201
242
  })
243
+ trackOpenAiCheckoutStarted({
244
+ contents: openAiContents(lines, currency),
245
+ currency,
246
+ value,
247
+ })
202
248
  trackGABeginCheckout({
203
249
  currency,
204
250
  value,
@@ -222,8 +268,9 @@ export function trackCheckoutStart(input: {
222
268
  * malformed `send_to` is accepted by Google and silently dropped.
223
269
  *
224
270
  * Everything here is deduped against the server: Meta on
225
- * `purchase_<displayId>`, TikTok on `tt_purchase_<displayId>`, GA4 and
226
- * Google Ads on the transaction id, which is `displayId` itself.
271
+ * `purchase_<displayId>`, TikTok on `tt_purchase_<displayId>`, ChatGPT Ads
272
+ * on `oai_order_<displayId>`, GA4 and Google Ads on the transaction id,
273
+ * which is `displayId` itself.
227
274
  */
228
275
  export function trackOrderPurchase(
229
276
  order: TrackedOrder,
@@ -261,6 +308,13 @@ export function trackOrderPurchase(
261
308
  customerType,
262
309
  })
263
310
 
311
+ trackOpenAiOrderCreated({
312
+ contents: openAiContents(lines, currency),
313
+ currency,
314
+ value,
315
+ displayId,
316
+ })
317
+
264
318
  trackGAPurchase({
265
319
  transaction_id: transactionId,
266
320
  currency,
@@ -57,6 +57,7 @@ export { MetaPixel, updatePixelAdvancedMatching } from "./meta-pixel"
57
57
  export { GA4, GoogleTag } from "./ga4"
58
58
  export { Gtm } from "./gtm"
59
59
  export { TikTokPixel } from "./tiktok-pixel"
60
+ export { ChatGptPixel } from "./chatgpt-pixel"
60
61
  export { StorefrontTags } from "./storefront-tags"
61
62
  export { TrackInit } from "./track-init"
62
63
  // The live-visitor heartbeat. Mounted by <StorefrontTags> so a storefront
@@ -134,6 +135,17 @@ export {
134
135
  tiktokPurchaseEventId,
135
136
  type TikTokContentItem,
136
137
  } from "./ttq"
138
+ export {
139
+ trackOpenAiPageView,
140
+ trackOpenAiContentsViewed,
141
+ trackOpenAiItemsAdded,
142
+ trackOpenAiCheckoutStarted,
143
+ trackOpenAiOrderCreated,
144
+ applyOpenAiConsent,
145
+ openAiOrderEventId,
146
+ minorUnits,
147
+ type OpenAiContentItem,
148
+ } from "./oaiq"
137
149
  export {
138
150
  trackGoogleAdsPurchase,
139
151
  googleAdsPurchaseSendTo,
@@ -0,0 +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
+ }
@@ -4,6 +4,7 @@ import { PageViews } from "./page-views"
4
4
  import { Gtm } from "./gtm"
5
5
  import { MetaPixel } from "./meta-pixel"
6
6
  import { TikTokPixel } from "./tiktok-pixel"
7
+ import { ChatGptPixel } from "./chatgpt-pixel"
7
8
  import { getTrackingConfig } from "./get-tracking-config"
8
9
  import type { StorefrontClient } from "../api/http"
9
10
  import type { TrackingConfig } from "./types"
@@ -65,6 +66,7 @@ export async function StorefrontTags({
65
66
  <LiveHeartbeat />
66
67
  <MetaPixel pixelId={tracking.facebookPixel?.pixelId} />
67
68
  <TikTokPixel pixelId={tracking.tiktok?.pixelId} />
69
+ <ChatGptPixel pixelId={tracking.chatgptAds?.pixelId} />
68
70
  <GoogleTag
69
71
  measurementId={tracking.ga4?.measurementId}
70
72
  adsConversionId={tracking.googleAds?.conversionId}
@@ -44,6 +44,10 @@ export type TrackingConfig = {
44
44
  * read by the order.placed forwarder, exactly like Meta's CAPI token,
45
45
  * which has never traversed this surface. */
46
46
  tiktok?: { pixelId: string }
47
+ /** ChatGPT Ads, pixel id ONLY — the Conversions API key is a bearer
48
+ * token and stays on the platform, read by the order.placed forwarder
49
+ * (chatgpt-ads-pixel card). */
50
+ chatgptAds?: { pixelId: string }
47
51
  /** True when the store's consent CMP is enabled — client tags must
48
52
  * mount through the consent gate (`_1c_consent` / Consent Mode v2). */
49
53
  consent_required?: boolean
@@ -1,72 +0,0 @@
1
- import type { CartDrawerLabels } from "./labels"
2
-
3
- /**
4
- * Bulgarian translations for the cart drawer.
5
- *
6
- * Ships alongside the English defaults so Bulgarian stores get the
7
- * translated copy with a single import instead of hand-writing every
8
- * string. Strings match the originals from mindpages-storefront, plus the
9
- * Cartbase summary-breakdown row additions.
10
- *
11
- * Usage in a store's layout:
12
- *
13
- * import {
14
- * CartDrawerProvider,
15
- * bulgarianCartDrawerLabels,
16
- * } from "@cartbase/storefront/cart-drawer"
17
- *
18
- * <CartDrawerProvider cart={cart} labels={bulgarianCartDrawerLabels}>
19
- * ...
20
- * </CartDrawerProvider>
21
- *
22
- * For multi-language stores, pick the matching labels object at render
23
- * time based on the active locale (the store owns its locale cookie; see
24
- * docs/storefront/regions.md for the locale catalog).
25
- */
26
-
27
- export const bulgarianCartDrawerLabels: CartDrawerLabels = {
28
- yourCart: "Вашата кошница",
29
- item: "артикул",
30
- items: "артикула",
31
- closeCart: "Затвори кошницата",
32
-
33
- emptyCartTitle: "Кошницата е празна",
34
- emptyCartMessage: "Все още нямате добавени продукти.",
35
- browseProducts: "Разгледай продуктите",
36
-
37
- awayFromFreeShipping: "до безплатна доставка",
38
- allRewardsUnlocked: "Всички награди са отключени",
39
-
40
- addGiftWrap: "Добави подаръчна опаковка",
41
- giftNote: "Картичка",
42
- orderNote: "Бележка",
43
- giftNotePlaceholder: "Добави лично съобщение...",
44
- orderNotePlaceholder: "Специални инструкции за поръчката...",
45
-
46
- estimatedRewardPoints: "Очаквани точки за награда",
47
-
48
- pairsWellWith: "Подходящи допълнения",
49
- youllLoveThis: "Ще ви хареса",
50
- youMightAlsoLike: "Може да ви хареса",
51
- addToCart: "Добави",
52
-
53
- subtotal: "Междинна сума",
54
- taxAndShipping: "С включен ДДС. Доставката се изчислява при плащане.",
55
- shipping: "Доставка",
56
- free: "БЕЗПЛАТНА",
57
- calculatedAtCheckout: "Изчислява се при плащане",
58
- total: "Общо",
59
- secureCheckout: "Към поръчка",
60
- shippingAndTaxCalculatedAtCheckout:
61
- "Доставката и данъкът се изчисляват при плащане",
62
-
63
- discount: "Отстъпка",
64
- tax: "ДДС",
65
- paymentMethodFee: "Такса за плащане",
66
- giftCard: "Подаръчна карта",
67
- remainingToPay: "Остава за плащане",
68
-
69
- continueShopping: "Продължи пазаруването",
70
-
71
- remove: "Премахни",
72
- }