@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.
Files changed (99) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +258 -258
  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/redirects.ts +37 -37
  18. package/src/api/regions.ts +200 -200
  19. package/src/api/search.ts +163 -163
  20. package/src/api/store.ts +35 -35
  21. package/src/api/types.ts +91 -91
  22. package/src/cart-drawer/context.tsx +778 -778
  23. package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
  24. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
  25. package/src/cart-drawer/gift-wrap.tsx +82 -82
  26. package/src/cart-drawer/item/index.tsx +162 -162
  27. package/src/cart-drawer/item/upsell.tsx +110 -110
  28. package/src/cart-drawer/labels.ts +123 -123
  29. package/src/cart-drawer/mutation-queue.ts +78 -78
  30. package/src/cart-drawer/notes.tsx +131 -131
  31. package/src/cart-drawer/sticky-footer.tsx +73 -73
  32. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  33. package/src/checkout/address-error-copy.ts +117 -117
  34. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  35. package/src/checkout/compare-addresses.ts +40 -40
  36. package/src/checkout/discount-section.tsx +218 -218
  37. package/src/checkout/error-copy-codes.ts +63 -63
  38. package/src/checkout/geocode.ts +154 -154
  39. package/src/checkout/gift-card-section.tsx +224 -224
  40. package/src/checkout/index.ts +72 -72
  41. package/src/checkout/labels.ts +495 -495
  42. package/src/checkout/payment-button.tsx +372 -372
  43. package/src/checkout/payment-error-copy.ts +154 -154
  44. package/src/checkout/promotion-error-copy.ts +91 -91
  45. package/src/common/country-flag.tsx +52 -52
  46. package/src/common/country-select.tsx +11 -11
  47. package/src/common/index.ts +20 -20
  48. package/src/common/market-select.tsx +57 -57
  49. package/src/index.ts +12 -12
  50. package/src/lib/country-name.ts +59 -59
  51. package/src/lib/get-product-price.ts +133 -133
  52. package/src/lib/media-image.tsx +39 -39
  53. package/src/lib/payment-constants.ts +53 -53
  54. package/src/lib/platform.ts +13 -13
  55. package/src/lib/price.tsx +39 -39
  56. package/src/lib/store-api-error.ts +36 -36
  57. package/src/lib/variant-caption.ts +32 -32
  58. package/src/locales/context.ts +37 -37
  59. package/src/locales/en.ts +26 -26
  60. package/src/locales/index.ts +19 -19
  61. package/src/locales/provider.tsx +59 -59
  62. package/src/locales/types.ts +77 -77
  63. package/src/order/index.ts +62 -62
  64. package/src/order/labels.ts +79 -79
  65. package/src/order/order-totals.tsx +250 -250
  66. package/src/primitives/select-field.tsx +93 -93
  67. package/src/products/image-gallery.tsx +43 -43
  68. package/src/products/option-select.tsx +67 -67
  69. package/src/products/product-info.tsx +5 -5
  70. package/src/products/product-price.tsx +69 -69
  71. package/src/products/product-promises.tsx +61 -61
  72. package/src/products/product-specs.tsx +65 -65
  73. package/src/products/product-tabs.tsx +123 -123
  74. package/src/products/purchase-options.tsx +130 -130
  75. package/src/products/sets.ts +110 -110
  76. package/src/products/variant-matching.ts +71 -71
  77. package/src/products/variant-url.ts +74 -74
  78. package/src/reviews-ui/lightbox-state.ts +46 -46
  79. package/src/reviews-ui/review-header.tsx +166 -166
  80. package/src/reviews-ui/review-lightbox.tsx +271 -271
  81. package/src/reviews-ui/review-list.tsx +193 -193
  82. package/src/reviews-ui/review-widget.tsx +219 -218
  83. package/src/reviews-ui/widget-options.ts +55 -55
  84. package/src/store/category-template.tsx +136 -136
  85. package/src/store/index.ts +40 -40
  86. package/src/tracking/chatgpt-pixel.tsx +99 -99
  87. package/src/tracking/consent-init.tsx +62 -62
  88. package/src/tracking/events.ts +348 -348
  89. package/src/tracking/ga4.tsx +93 -93
  90. package/src/tracking/google-ads.ts +84 -84
  91. package/src/tracking/gtm.tsx +60 -60
  92. package/src/tracking/inline-script.ts +49 -49
  93. package/src/tracking/oaiq.ts +206 -206
  94. package/src/tracking/tiktok-pixel.tsx +91 -91
  95. package/src/tracking/track-init.tsx +56 -56
  96. package/src/tracking/track-order-purchase.tsx +122 -122
  97. package/src/tracking/ttq.ts +180 -180
  98. package/src/tracking/use-tracking-config.ts +54 -54
  99. package/theme/index.css +25 -25
@@ -1,136 +1,136 @@
1
- /**
2
- * Category page — ported from
3
- * `@1click/ui/src/store/category-template.tsx` (v2.3.1). Data seam:
4
- * `HttpTypes.StoreProductCategory` → Cartbase `StoreProductCategory`
5
- * (`@cartbase/storefront/api/categories`); products come from the plain
6
- * listing filtered by `category_id` (membership), via `PaginatedProducts`.
7
- *
8
- * Breadcrumbs need `parent_category` chains and the child list needs
9
- * `category_children` — fetch the category with `include_ancestors_tree` +
10
- * `include_descendants_tree` (`retrieveCategory` query flags), otherwise
11
- * those sections simply don't render. Skeleton count is the fixed default
12
- * (Cartbase categories don't embed a products array).
13
- */
14
- import { Suspense } from "react"
15
- import { notFound } from "next/navigation"
16
- import Link from "next/link"
17
- import type { StorefrontClient } from "../api/http"
18
- import type { PricingContextQuery } from "../api/types"
19
- import type { StoreProductCategory } from "../api/categories"
20
- import type { SortOptions } from "../lib/sort-products"
21
- import type { StoreLabels } from "./labels"
22
- import { SortSelect } from "./sort-select"
23
- import { PaginatedProducts, type ProductCardComponent } from "./paginated-products"
24
- import { SkeletonProductGrid } from "./skeleton-product-grid"
25
-
26
- type CategoryTemplateProps = {
27
- client: StorefrontClient
28
- category: StoreProductCategory
29
- sortBy?: SortOptions
30
- /** From the `page` query param. */
31
- page?: string
32
- pricingContext?: PricingContextQuery
33
- /**
34
- * The store's pack for this area, REQUIRED: this is a server component and
35
- * cannot read `StorefrontLocaleProvider`, so the page passes
36
- * `STORE_LOCALE.store` (`en.store` for an English store). Required since
37
- * 2026-09-14 so that forgetting it fails the build instead of shipping
38
- * silent English.
39
- */
40
- labels: StoreLabels
41
- /** Override the default ProductPreview with a store-specific card. */
42
- renderProduct?: ProductCardComponent
43
- }
44
-
45
- export function CategoryTemplate({
46
- client,
47
- category,
48
- sortBy,
49
- page,
50
- pricingContext,
51
- labels,
52
- renderProduct,
53
- }: CategoryTemplateProps) {
54
- const pageNumber = page ? parseInt(page) : 1
55
- const sort = sortBy || "created_at"
56
-
57
- if (!category) notFound()
58
-
59
- const parents: StoreProductCategory[] = []
60
- const getParents = (cat: StoreProductCategory) => {
61
- if (cat.parent_category) {
62
- parents.push(cat.parent_category)
63
- getParents(cat.parent_category)
64
- }
65
- }
66
- getParents(category)
67
-
68
- return (
69
- <div
70
- className="flex flex-col sm:flex-row sm:items-start py-6 max-w-7xl mx-auto px-4"
71
- data-testid="category-container"
72
- >
73
- <div className="flex sm:flex-col gap-12 py-4 mb-8 sm:px-0 pl-6 sm:min-w-[250px]">
74
- <SortSelect
75
- sortBy={sort}
76
- labels={labels}
77
- data-testid="sort-by-container"
78
- />
79
- </div>
80
- <div className="w-full">
81
- <div className="flex flex-row mb-8 gap-4">
82
- {parents.map((parent) => (
83
- <span key={parent.id} className="text-muted-foreground text-2xl">
84
- <Link
85
- className="mr-4 hover:text-foreground"
86
- href={`/categories/${parent.handle}`}
87
- >
88
- {parent.name}
89
- </Link>
90
- /
91
- </span>
92
- ))}
93
- <h1
94
- className="text-2xl font-semibold text-foreground"
95
- data-testid="category-page-title"
96
- >
97
- {category.name}
98
- </h1>
99
- </div>
100
- {category.description && (
101
- <div className="mb-8 text-sm text-muted-foreground">
102
- <p>{category.description}</p>
103
- </div>
104
- )}
105
- {category.category_children && (
106
- <div className="mb-8">
107
- <ul className="grid grid-cols-1 gap-2">
108
- {category.category_children.map((c) => (
109
- <li key={c.id}>
110
- <Link
111
- href={`/categories/${c.handle}`}
112
- className="text-primary hover:underline"
113
- >
114
- {c.name}
115
- </Link>
116
- </li>
117
- ))}
118
- </ul>
119
- </div>
120
- )}
121
- <Suspense fallback={<SkeletonProductGrid />}>
122
- <PaginatedProducts
123
- client={client}
124
- sortBy={sort}
125
- page={pageNumber}
126
- categoryId={category.id}
127
- pricingContext={pricingContext}
128
- renderProduct={renderProduct}
129
- />
130
- </Suspense>
131
- </div>
132
- </div>
133
- )
134
- }
135
-
136
- export { type CategoryTemplateProps }
1
+ /**
2
+ * Category page — ported from
3
+ * `@1click/ui/src/store/category-template.tsx` (v2.3.1). Data seam:
4
+ * `HttpTypes.StoreProductCategory` → Cartbase `StoreProductCategory`
5
+ * (`@cartbase/storefront/api/categories`); products come from the plain
6
+ * listing filtered by `category_id` (membership), via `PaginatedProducts`.
7
+ *
8
+ * Breadcrumbs need `parent_category` chains and the child list needs
9
+ * `category_children` — fetch the category with `include_ancestors_tree` +
10
+ * `include_descendants_tree` (`retrieveCategory` query flags), otherwise
11
+ * those sections simply don't render. Skeleton count is the fixed default
12
+ * (Cartbase categories don't embed a products array).
13
+ */
14
+ import { Suspense } from "react"
15
+ import { notFound } from "next/navigation"
16
+ import Link from "next/link"
17
+ import type { StorefrontClient } from "../api/http"
18
+ import type { PricingContextQuery } from "../api/types"
19
+ import type { StoreProductCategory } from "../api/categories"
20
+ import type { SortOptions } from "../lib/sort-products"
21
+ import type { StoreLabels } from "./labels"
22
+ import { SortSelect } from "./sort-select"
23
+ import { PaginatedProducts, type ProductCardComponent } from "./paginated-products"
24
+ import { SkeletonProductGrid } from "./skeleton-product-grid"
25
+
26
+ type CategoryTemplateProps = {
27
+ client: StorefrontClient
28
+ category: StoreProductCategory
29
+ sortBy?: SortOptions
30
+ /** From the `page` query param. */
31
+ page?: string
32
+ pricingContext?: PricingContextQuery
33
+ /**
34
+ * The store's pack for this area, REQUIRED: this is a server component and
35
+ * cannot read `StorefrontLocaleProvider`, so the page passes
36
+ * `STORE_LOCALE.store` (`en.store` for an English store). Required since
37
+ * 2026-09-14 so that forgetting it fails the build instead of shipping
38
+ * silent English.
39
+ */
40
+ labels: StoreLabels
41
+ /** Override the default ProductPreview with a store-specific card. */
42
+ renderProduct?: ProductCardComponent
43
+ }
44
+
45
+ export function CategoryTemplate({
46
+ client,
47
+ category,
48
+ sortBy,
49
+ page,
50
+ pricingContext,
51
+ labels,
52
+ renderProduct,
53
+ }: CategoryTemplateProps) {
54
+ const pageNumber = page ? parseInt(page) : 1
55
+ const sort = sortBy || "created_at"
56
+
57
+ if (!category) notFound()
58
+
59
+ const parents: StoreProductCategory[] = []
60
+ const getParents = (cat: StoreProductCategory) => {
61
+ if (cat.parent_category) {
62
+ parents.push(cat.parent_category)
63
+ getParents(cat.parent_category)
64
+ }
65
+ }
66
+ getParents(category)
67
+
68
+ return (
69
+ <div
70
+ className="flex flex-col sm:flex-row sm:items-start py-6 max-w-7xl mx-auto px-4"
71
+ data-testid="category-container"
72
+ >
73
+ <div className="flex sm:flex-col gap-12 py-4 mb-8 sm:px-0 pl-6 sm:min-w-[250px]">
74
+ <SortSelect
75
+ sortBy={sort}
76
+ labels={labels}
77
+ data-testid="sort-by-container"
78
+ />
79
+ </div>
80
+ <div className="w-full">
81
+ <div className="flex flex-row mb-8 gap-4">
82
+ {parents.map((parent) => (
83
+ <span key={parent.id} className="text-muted-foreground text-2xl">
84
+ <Link
85
+ className="mr-4 hover:text-foreground"
86
+ href={`/categories/${parent.handle}`}
87
+ >
88
+ {parent.name}
89
+ </Link>
90
+ /
91
+ </span>
92
+ ))}
93
+ <h1
94
+ className="text-2xl font-semibold text-foreground"
95
+ data-testid="category-page-title"
96
+ >
97
+ {category.name}
98
+ </h1>
99
+ </div>
100
+ {category.description && (
101
+ <div className="mb-8 text-sm text-muted-foreground">
102
+ <p>{category.description}</p>
103
+ </div>
104
+ )}
105
+ {category.category_children && (
106
+ <div className="mb-8">
107
+ <ul className="grid grid-cols-1 gap-2">
108
+ {category.category_children.map((c) => (
109
+ <li key={c.id}>
110
+ <Link
111
+ href={`/categories/${c.handle}`}
112
+ className="text-primary hover:underline"
113
+ >
114
+ {c.name}
115
+ </Link>
116
+ </li>
117
+ ))}
118
+ </ul>
119
+ </div>
120
+ )}
121
+ <Suspense fallback={<SkeletonProductGrid />}>
122
+ <PaginatedProducts
123
+ client={client}
124
+ sortBy={sort}
125
+ page={pageNumber}
126
+ categoryId={category.id}
127
+ pricingContext={pricingContext}
128
+ renderProduct={renderProduct}
129
+ />
130
+ </Suspense>
131
+ </div>
132
+ </div>
133
+ )
134
+ }
135
+
136
+ export { type CategoryTemplateProps }
@@ -1,40 +1,40 @@
1
- /**
2
- * @cartbase/storefront — store (listing) family barrel. Prefer subpath
3
- * imports in apps; the barrel exists for convenience and for the extra pure
4
- * module (search-params).
5
- */
6
- export {
7
- defaultStoreLabels,
8
- sortOptionLabelKeys,
9
- type StoreLabels,
10
- } from "./labels"
11
-
12
- export { Pagination } from "./pagination"
13
- export { SortSelect, type SortSelectProps } from "./sort-select"
14
- export { PaginatedProducts, type ProductCardComponent } from "./paginated-products"
15
- export { SkeletonProductGrid } from "./skeleton-product-grid"
16
- export { StoreTemplate, type StoreTemplateProps } from "./store-template"
17
- export {
18
- CollectionTemplate,
19
- type CollectionTemplateProps,
20
- } from "./collection-template"
21
- export {
22
- CategoryTemplate,
23
- type CategoryTemplateProps,
24
- } from "./category-template"
25
- export { SearchTemplate, type SearchTemplateProps } from "./search-template"
26
-
27
- export {
28
- parseSearchParams,
29
- buildSearchQueryString,
30
- fromQueryString,
31
- toggleFacetSelection,
32
- isFacetSelected,
33
- clearFilters,
34
- toSearchQuery,
35
- type SearchPageState,
36
- type RawSearchParams,
37
- type FacetBucketRef,
38
- } from "./search-params"
39
-
40
- export { type SortOptions } from "../lib/sort-products"
1
+ /**
2
+ * @cartbase/storefront — store (listing) family barrel. Prefer subpath
3
+ * imports in apps; the barrel exists for convenience and for the extra pure
4
+ * module (search-params).
5
+ */
6
+ export {
7
+ defaultStoreLabels,
8
+ sortOptionLabelKeys,
9
+ type StoreLabels,
10
+ } from "./labels"
11
+
12
+ export { Pagination } from "./pagination"
13
+ export { SortSelect, type SortSelectProps } from "./sort-select"
14
+ export { PaginatedProducts, type ProductCardComponent } from "./paginated-products"
15
+ export { SkeletonProductGrid } from "./skeleton-product-grid"
16
+ export { StoreTemplate, type StoreTemplateProps } from "./store-template"
17
+ export {
18
+ CollectionTemplate,
19
+ type CollectionTemplateProps,
20
+ } from "./collection-template"
21
+ export {
22
+ CategoryTemplate,
23
+ type CategoryTemplateProps,
24
+ } from "./category-template"
25
+ export { SearchTemplate, type SearchTemplateProps } from "./search-template"
26
+
27
+ export {
28
+ parseSearchParams,
29
+ buildSearchQueryString,
30
+ fromQueryString,
31
+ toggleFacetSelection,
32
+ isFacetSelected,
33
+ clearFilters,
34
+ toSearchQuery,
35
+ type SearchPageState,
36
+ type RawSearchParams,
37
+ type FacetBucketRef,
38
+ } from "./search-params"
39
+
40
+ export { type SortOptions } from "../lib/sort-products"
@@ -1,99 +1,99 @@
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
- * 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 the store's default overridden by the visitor's stored
36
- * choice (`adsConsentSnippet`), resolved inside the snippet before anything
37
- * can be sent: a store that collects consent starts closed, a store with
38
- * no consent gate starts open, the cookie winning either way. Live changes
39
- * arrive through `applyConsent()` in ./consent.ts — there is no cookie
40
- * watcher. `consentRequired` is `tracking.consent_required`, passed by
41
- * <StorefrontTags>, and required for the reason <ConsentInit> gives.
42
- *
43
- * The page view is ours to fire at all: their pixel sends nothing at init,
44
- * unlike Meta's and TikTok's base code.
45
- *
46
- * Their SDK also captures `oppref` off the landing URL into a first-party
47
- * `__oppref` cookie by itself. Our attribution module captures the same
48
- * parameter into `_1c_oppref` independently, so the server-side
49
- * Conversions API event can carry the click even on a visit where this
50
- * script never loaded at all.
51
- *
52
- * Renders nothing when `pixelId` is falsy, so a layout can mount it
53
- * unconditionally.
54
- */
55
- export function ChatGptPixel({
56
- pixelId,
57
- consentRequired,
58
- }: {
59
- pixelId?: string
60
- /** `tracking.consent_required`: does this store collect consent before it tracks? */
61
- consentRequired: boolean
62
- }) {
63
- if (!pixelId) return null
64
-
65
- const id = jsStringLiteral(pixelId)
66
-
67
- const initSnippet = `
68
- (function (w, d, s, u) {
69
- if (w.oaiq) return;
70
- var q = function () { q.q.push(arguments); };
71
- q.q = [];
72
- w.oaiq = q;
73
- var js = d.createElement(s);
74
- js.async = true;
75
- js.src = u;
76
- var f = d.getElementsByTagName(s)[0];
77
- f.parentNode.insertBefore(js, f);
78
- })(window, document, "script", "https://bzrcdn.openai.com/sdk/oaiq.min.js");
79
-
80
- oaiq("consent", false);
81
- oaiq("init", { pixelId: ${id} });
82
-
83
- (function (d) {
84
- ${adsConsentSnippet(consentRequired, "d")}
85
- if (ads) {
86
- oaiq("consent", true);
87
- oaiq("measure", "page_viewed", { type: "contents" });
88
- }
89
- })(document);
90
- `.trim()
91
-
92
- return (
93
- <Script
94
- id="chatgpt-pixel-init"
95
- strategy="afterInteractive"
96
- dangerouslySetInnerHTML={{ __html: initSnippet }}
97
- />
98
- )
99
- }
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
+ * 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 the store's default overridden by the visitor's stored
36
+ * choice (`adsConsentSnippet`), resolved inside the snippet before anything
37
+ * can be sent: a store that collects consent starts closed, a store with
38
+ * no consent gate starts open, the cookie winning either way. Live changes
39
+ * arrive through `applyConsent()` in ./consent.ts — there is no cookie
40
+ * watcher. `consentRequired` is `tracking.consent_required`, passed by
41
+ * <StorefrontTags>, and required for the reason <ConsentInit> gives.
42
+ *
43
+ * The page view is ours to fire at all: their pixel sends nothing at init,
44
+ * unlike Meta's and TikTok's base code.
45
+ *
46
+ * Their SDK also captures `oppref` off the landing URL into a first-party
47
+ * `__oppref` cookie by itself. Our attribution module captures the same
48
+ * parameter into `_1c_oppref` independently, so the server-side
49
+ * Conversions API event can carry the click even on a visit where this
50
+ * script never loaded at all.
51
+ *
52
+ * Renders nothing when `pixelId` is falsy, so a layout can mount it
53
+ * unconditionally.
54
+ */
55
+ export function ChatGptPixel({
56
+ pixelId,
57
+ consentRequired,
58
+ }: {
59
+ pixelId?: string
60
+ /** `tracking.consent_required`: does this store collect consent before it tracks? */
61
+ consentRequired: boolean
62
+ }) {
63
+ if (!pixelId) return null
64
+
65
+ const id = jsStringLiteral(pixelId)
66
+
67
+ const initSnippet = `
68
+ (function (w, d, s, u) {
69
+ if (w.oaiq) return;
70
+ var q = function () { q.q.push(arguments); };
71
+ q.q = [];
72
+ w.oaiq = q;
73
+ var js = d.createElement(s);
74
+ js.async = true;
75
+ js.src = u;
76
+ var f = d.getElementsByTagName(s)[0];
77
+ f.parentNode.insertBefore(js, f);
78
+ })(window, document, "script", "https://bzrcdn.openai.com/sdk/oaiq.min.js");
79
+
80
+ oaiq("consent", false);
81
+ oaiq("init", { pixelId: ${id} });
82
+
83
+ (function (d) {
84
+ ${adsConsentSnippet(consentRequired, "d")}
85
+ if (ads) {
86
+ oaiq("consent", true);
87
+ oaiq("measure", "page_viewed", { type: "contents" });
88
+ }
89
+ })(document);
90
+ `.trim()
91
+
92
+ return (
93
+ <Script
94
+ id="chatgpt-pixel-init"
95
+ strategy="afterInteractive"
96
+ dangerouslySetInnerHTML={{ __html: initSnippet }}
97
+ />
98
+ )
99
+ }