@cartbase/storefront 0.20.0 → 0.21.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 (109) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +4 -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 +181 -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 +56 -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/payment-badges.tsx +36 -96
  32. package/src/cart-drawer/sticky-footer.tsx +73 -73
  33. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  34. package/src/checkout/address-error-copy.ts +117 -117
  35. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  36. package/src/checkout/compare-addresses.ts +40 -40
  37. package/src/checkout/discount-section.tsx +218 -218
  38. package/src/checkout/error-copy-codes.ts +63 -63
  39. package/src/checkout/geocode.ts +154 -154
  40. package/src/checkout/gift-card-section.tsx +224 -224
  41. package/src/checkout/index.ts +72 -72
  42. package/src/checkout/labels.ts +495 -495
  43. package/src/checkout/payment-button.tsx +372 -372
  44. package/src/checkout/payment-error-copy.ts +154 -154
  45. package/src/checkout/promotion-error-copy.ts +91 -91
  46. package/src/common/country-flag.tsx +52 -52
  47. package/src/common/country-select.tsx +11 -11
  48. package/src/common/icons/cartbase-mark.ts +9 -0
  49. package/src/common/icons/payment-marks.ts +73 -0
  50. package/src/common/icons/social-marks.ts +61 -0
  51. package/src/common/index.ts +35 -20
  52. package/src/common/market-select.tsx +57 -57
  53. package/src/common/payment-icons.tsx +54 -0
  54. package/src/common/powered-by-cartbase.tsx +47 -0
  55. package/src/common/social-links.tsx +65 -0
  56. package/src/index.ts +12 -12
  57. package/src/lib/country-name.ts +59 -59
  58. package/src/lib/get-product-price.ts +133 -133
  59. package/src/lib/media-image.tsx +39 -39
  60. package/src/lib/payment-constants.ts +53 -53
  61. package/src/lib/platform.ts +13 -13
  62. package/src/lib/price.tsx +39 -39
  63. package/src/lib/store-api-error.ts +36 -36
  64. package/src/lib/variant-caption.ts +32 -32
  65. package/src/locales/bg.ts +3 -0
  66. package/src/locales/context.ts +37 -37
  67. package/src/locales/en.ts +26 -26
  68. package/src/locales/es.ts +3 -0
  69. package/src/locales/index.ts +19 -19
  70. package/src/locales/provider.tsx +59 -59
  71. package/src/locales/types.ts +77 -77
  72. package/src/order/index.ts +62 -62
  73. package/src/order/labels.ts +79 -79
  74. package/src/order/order-totals.tsx +250 -250
  75. package/src/primitives/select-field.tsx +93 -93
  76. package/src/products/image-gallery.tsx +43 -43
  77. package/src/products/option-select.tsx +67 -67
  78. package/src/products/product-info.tsx +5 -5
  79. package/src/products/product-price.tsx +69 -69
  80. package/src/products/product-promises.tsx +61 -61
  81. package/src/products/product-specs.tsx +65 -65
  82. package/src/products/product-tabs.tsx +123 -123
  83. package/src/products/purchase-options.tsx +130 -130
  84. package/src/products/sets.ts +110 -110
  85. package/src/products/variant-matching.ts +71 -71
  86. package/src/products/variant-url.ts +74 -74
  87. package/src/reviews-ui/lightbox-state.ts +46 -46
  88. package/src/reviews-ui/review-header.tsx +166 -166
  89. package/src/reviews-ui/review-lightbox.tsx +271 -271
  90. package/src/reviews-ui/review-list.tsx +193 -193
  91. package/src/reviews-ui/review-widget.tsx +219 -218
  92. package/src/reviews-ui/widget-options.ts +55 -55
  93. package/src/store/category-template.tsx +136 -136
  94. package/src/store/index.ts +40 -40
  95. package/src/store/labels.ts +10 -0
  96. package/src/tracking/chatgpt-pixel.tsx +99 -99
  97. package/src/tracking/consent-init.tsx +62 -62
  98. package/src/tracking/events.ts +348 -348
  99. package/src/tracking/ga4.tsx +93 -93
  100. package/src/tracking/google-ads.ts +84 -84
  101. package/src/tracking/gtm.tsx +60 -60
  102. package/src/tracking/inline-script.ts +49 -49
  103. package/src/tracking/oaiq.ts +206 -206
  104. package/src/tracking/tiktok-pixel.tsx +91 -91
  105. package/src/tracking/track-init.tsx +56 -56
  106. package/src/tracking/track-order-purchase.tsx +122 -122
  107. package/src/tracking/ttq.ts +180 -180
  108. package/src/tracking/use-tracking-config.ts +54 -54
  109. package/theme/index.css +25 -25
@@ -1,56 +1,56 @@
1
- "use client"
2
-
3
- import { useEffect } from "react"
4
-
5
- import {
6
- captureClickIdsFromUrl,
7
- captureUtmsFromUrl,
8
- getOrCreateAnonId,
9
- setTrackingDefaults,
10
- } from "./attribution"
11
- import { initEngagementTime } from "./use-engagement-time"
12
-
13
- /**
14
- * TrackInit — mounts once in the root layout and does the four things
15
- * that must happen on the FIRST page of a visit, not at checkout:
16
- *
17
- * 1. Capture UTM parameters into the first-touch and last-touch
18
- * cookies. First-touch is written once and survives a year, so the
19
- * campaign that acquired the visitor is still attributable when they
20
- * convert months later.
21
- * 2. Capture the ad-click identifiers — TikTok's `ttclid`, Google's
22
- * `gclid` and the two iOS variants `gbraid` / `wbraid`. These arrive
23
- * ONLY on the landing URL of an ad click. Miss them there and they
24
- * are gone: the visitor navigates, the parameter disappears, and
25
- * neither platform can ever tie the eventual order back to the click
26
- * that paid for it.
27
- * 3. Create the anonymous visitor id, so events before signup can be
28
- * joined to the customer afterwards.
29
- * 4. Start the engagement clock, so time-on-session is measured from
30
- * the landing page rather than lazily from the first checkout call.
31
- *
32
- * All four are idempotent and safe on every mount: a URL with no
33
- * parameters leaves every existing cookie untouched, so an organic visit
34
- * never clears the click that acquired the visitor.
35
- *
36
- * Cookies only — nothing is sent from here. The server reads them at
37
- * checkout through `getTrackingAttribution()` and writes them onto the
38
- * cart, which carries them to the order, which is where the platform's
39
- * server-side Purchase events read them.
40
- *
41
- * `country` is the storefront's default market, used when an event has no
42
- * address to derive one from (a product view by an anonymous visitor).
43
- * Passing it lifts country coverage from the fraction of events that
44
- * carry an address to effectively all of them.
45
- */
46
- export function TrackInit({ country }: { country?: string } = {}) {
47
- useEffect(() => {
48
- if (country) setTrackingDefaults({ country })
49
- captureUtmsFromUrl()
50
- captureClickIdsFromUrl()
51
- getOrCreateAnonId()
52
- initEngagementTime()
53
- }, [country])
54
-
55
- return null
56
- }
1
+ "use client"
2
+
3
+ import { useEffect } from "react"
4
+
5
+ import {
6
+ captureClickIdsFromUrl,
7
+ captureUtmsFromUrl,
8
+ getOrCreateAnonId,
9
+ setTrackingDefaults,
10
+ } from "./attribution"
11
+ import { initEngagementTime } from "./use-engagement-time"
12
+
13
+ /**
14
+ * TrackInit — mounts once in the root layout and does the four things
15
+ * that must happen on the FIRST page of a visit, not at checkout:
16
+ *
17
+ * 1. Capture UTM parameters into the first-touch and last-touch
18
+ * cookies. First-touch is written once and survives a year, so the
19
+ * campaign that acquired the visitor is still attributable when they
20
+ * convert months later.
21
+ * 2. Capture the ad-click identifiers — TikTok's `ttclid`, Google's
22
+ * `gclid` and the two iOS variants `gbraid` / `wbraid`. These arrive
23
+ * ONLY on the landing URL of an ad click. Miss them there and they
24
+ * are gone: the visitor navigates, the parameter disappears, and
25
+ * neither platform can ever tie the eventual order back to the click
26
+ * that paid for it.
27
+ * 3. Create the anonymous visitor id, so events before signup can be
28
+ * joined to the customer afterwards.
29
+ * 4. Start the engagement clock, so time-on-session is measured from
30
+ * the landing page rather than lazily from the first checkout call.
31
+ *
32
+ * All four are idempotent and safe on every mount: a URL with no
33
+ * parameters leaves every existing cookie untouched, so an organic visit
34
+ * never clears the click that acquired the visitor.
35
+ *
36
+ * Cookies only — nothing is sent from here. The server reads them at
37
+ * checkout through `getTrackingAttribution()` and writes them onto the
38
+ * cart, which carries them to the order, which is where the platform's
39
+ * server-side Purchase events read them.
40
+ *
41
+ * `country` is the storefront's default market, used when an event has no
42
+ * address to derive one from (a product view by an anonymous visitor).
43
+ * Passing it lifts country coverage from the fraction of events that
44
+ * carry an address to effectively all of them.
45
+ */
46
+ export function TrackInit({ country }: { country?: string } = {}) {
47
+ useEffect(() => {
48
+ if (country) setTrackingDefaults({ country })
49
+ captureUtmsFromUrl()
50
+ captureClickIdsFromUrl()
51
+ getOrCreateAnonId()
52
+ initEngagementTime()
53
+ }, [country])
54
+
55
+ return null
56
+ }
@@ -1,122 +1,122 @@
1
- "use client"
2
-
3
- import { useEffect, useRef } from "react"
4
-
5
- import type { CartLineItem, CompletedOrder } from "../api/carts"
6
- import { trackOrderPurchase, type TrackedLine } from "./events"
7
- import type { TrackingConfig } from "./types"
8
-
9
- /**
10
- * TrackOrderPurchase — fires the purchase event to every configured
11
- * vendor from the order confirmation page.
12
- *
13
- * A component rather than four calls in the app, because purchase is the
14
- * one event nobody may get wrong and it is the one with the most ways to
15
- * go wrong: the dedup keys must match the server's exactly, the ids must
16
- * be the CATALOGUE's, and every vendor must fire from the same place.
17
- * A storefront mounts this and is done.
18
- *
19
- * FIRES ONCE PER ORDER, per browser. The guard is real, not defensive
20
- * tidiness: this page is linked from every order email, so a buyer opens
21
- * it again days later, and React strict mode double-mounts effects in
22
- * development. The vendors dedupe too (Meta and TikTok on the shared
23
- * event id, GA4 and Google Ads on the transaction id) and the platform's
24
- * server side carries its own idempotency flag, so this is the third of
25
- * three layers — but it is the only one that stops the request being made
26
- * at all.
27
- *
28
- * `customerType` comes from `order.metadata.customer_type`, which the
29
- * platform's order.placed forwarder computes ONCE and persists. Reading
30
- * it from the order rather than deriving it in the browser is what keeps
31
- * the browser and the server from telling Google and TikTok different
32
- * things about the same buyer.
33
- */
34
-
35
- const FIRED_KEY_PREFIX = "_cb_purchase_tracked_"
36
-
37
- function alreadyFired(key: string): boolean {
38
- try {
39
- return sessionStorage.getItem(key) === "1"
40
- } catch {
41
- // Private mode / blocked storage: fall back to the in-memory ref
42
- // only. Worst case is a second event the vendors then dedupe.
43
- return false
44
- }
45
- }
46
-
47
- function markFired(key: string): void {
48
- try {
49
- sessionStorage.setItem(key, "1")
50
- } catch {
51
- // Non-fatal — see above.
52
- }
53
- }
54
-
55
- export function TrackOrderPurchase({
56
- order,
57
- items,
58
- config,
59
- }: {
60
- order: CompletedOrder
61
- /** The cart lines as completed. Carry product_id — it is the catalogue
62
- * key Meta and TikTok match on, and the flattened order items may not
63
- * have it. */
64
- items: CartLineItem[]
65
- /** The store's tracking config. Google Ads fires only when it carries
66
- * both the account id and the purchase label. */
67
- config?: TrackingConfig
68
- }) {
69
- const firedRef = useRef(false)
70
-
71
- useEffect(() => {
72
- const displayId = order.display_id
73
- // No display id means no dedup key, and an event that cannot dedupe
74
- // against the server's is worse than no event: it double-counts.
75
- if (displayId === undefined || displayId === null) return
76
-
77
- const key = `${FIRED_KEY_PREFIX}${displayId}`
78
- if (firedRef.current || alreadyFired(key)) return
79
- firedRef.current = true
80
- markFired(key)
81
-
82
- const lines: TrackedLine[] = items.map((item) => ({
83
- productId: item.product_id || item.variant_id || item.id,
84
- variantId: item.variant_id ?? undefined,
85
- title: item.product_title || item.title || "",
86
- quantity: Number(item.quantity) || 1,
87
- price: Number(item.unit_price) || 0,
88
- }))
89
-
90
- const summary = (Array.isArray(order.summary)
91
- ? order.summary[0]
92
- : order.summary) as Record<string, unknown> | null
93
- const totals = (summary?.totals ?? summary ?? {}) as Record<string, unknown>
94
- const num = (v: unknown): number | undefined =>
95
- typeof v === "number" ? v : undefined
96
-
97
- const value =
98
- num(totals.total) ??
99
- lines.reduce((sum, line) => sum + line.price * line.quantity, 0)
100
-
101
- const metadata = (order.metadata ?? {}) as Record<string, unknown>
102
- const customerType =
103
- metadata.customer_type === "new" || metadata.customer_type === "returning"
104
- ? metadata.customer_type
105
- : undefined
106
-
107
- trackOrderPurchase(
108
- {
109
- displayId,
110
- currency: (order.currency_code || "eur").toUpperCase(),
111
- value,
112
- tax: num(totals.tax_total),
113
- shipping: num(totals.shipping_total),
114
- lines,
115
- customerType,
116
- },
117
- config
118
- )
119
- }, [order, items, config])
120
-
121
- return null
122
- }
1
+ "use client"
2
+
3
+ import { useEffect, useRef } from "react"
4
+
5
+ import type { CartLineItem, CompletedOrder } from "../api/carts"
6
+ import { trackOrderPurchase, type TrackedLine } from "./events"
7
+ import type { TrackingConfig } from "./types"
8
+
9
+ /**
10
+ * TrackOrderPurchase — fires the purchase event to every configured
11
+ * vendor from the order confirmation page.
12
+ *
13
+ * A component rather than four calls in the app, because purchase is the
14
+ * one event nobody may get wrong and it is the one with the most ways to
15
+ * go wrong: the dedup keys must match the server's exactly, the ids must
16
+ * be the CATALOGUE's, and every vendor must fire from the same place.
17
+ * A storefront mounts this and is done.
18
+ *
19
+ * FIRES ONCE PER ORDER, per browser. The guard is real, not defensive
20
+ * tidiness: this page is linked from every order email, so a buyer opens
21
+ * it again days later, and React strict mode double-mounts effects in
22
+ * development. The vendors dedupe too (Meta and TikTok on the shared
23
+ * event id, GA4 and Google Ads on the transaction id) and the platform's
24
+ * server side carries its own idempotency flag, so this is the third of
25
+ * three layers — but it is the only one that stops the request being made
26
+ * at all.
27
+ *
28
+ * `customerType` comes from `order.metadata.customer_type`, which the
29
+ * platform's order.placed forwarder computes ONCE and persists. Reading
30
+ * it from the order rather than deriving it in the browser is what keeps
31
+ * the browser and the server from telling Google and TikTok different
32
+ * things about the same buyer.
33
+ */
34
+
35
+ const FIRED_KEY_PREFIX = "_cb_purchase_tracked_"
36
+
37
+ function alreadyFired(key: string): boolean {
38
+ try {
39
+ return sessionStorage.getItem(key) === "1"
40
+ } catch {
41
+ // Private mode / blocked storage: fall back to the in-memory ref
42
+ // only. Worst case is a second event the vendors then dedupe.
43
+ return false
44
+ }
45
+ }
46
+
47
+ function markFired(key: string): void {
48
+ try {
49
+ sessionStorage.setItem(key, "1")
50
+ } catch {
51
+ // Non-fatal — see above.
52
+ }
53
+ }
54
+
55
+ export function TrackOrderPurchase({
56
+ order,
57
+ items,
58
+ config,
59
+ }: {
60
+ order: CompletedOrder
61
+ /** The cart lines as completed. Carry product_id — it is the catalogue
62
+ * key Meta and TikTok match on, and the flattened order items may not
63
+ * have it. */
64
+ items: CartLineItem[]
65
+ /** The store's tracking config. Google Ads fires only when it carries
66
+ * both the account id and the purchase label. */
67
+ config?: TrackingConfig
68
+ }) {
69
+ const firedRef = useRef(false)
70
+
71
+ useEffect(() => {
72
+ const displayId = order.display_id
73
+ // No display id means no dedup key, and an event that cannot dedupe
74
+ // against the server's is worse than no event: it double-counts.
75
+ if (displayId === undefined || displayId === null) return
76
+
77
+ const key = `${FIRED_KEY_PREFIX}${displayId}`
78
+ if (firedRef.current || alreadyFired(key)) return
79
+ firedRef.current = true
80
+ markFired(key)
81
+
82
+ const lines: TrackedLine[] = items.map((item) => ({
83
+ productId: item.product_id || item.variant_id || item.id,
84
+ variantId: item.variant_id ?? undefined,
85
+ title: item.product_title || item.title || "",
86
+ quantity: Number(item.quantity) || 1,
87
+ price: Number(item.unit_price) || 0,
88
+ }))
89
+
90
+ const summary = (Array.isArray(order.summary)
91
+ ? order.summary[0]
92
+ : order.summary) as Record<string, unknown> | null
93
+ const totals = (summary?.totals ?? summary ?? {}) as Record<string, unknown>
94
+ const num = (v: unknown): number | undefined =>
95
+ typeof v === "number" ? v : undefined
96
+
97
+ const value =
98
+ num(totals.total) ??
99
+ lines.reduce((sum, line) => sum + line.price * line.quantity, 0)
100
+
101
+ const metadata = (order.metadata ?? {}) as Record<string, unknown>
102
+ const customerType =
103
+ metadata.customer_type === "new" || metadata.customer_type === "returning"
104
+ ? metadata.customer_type
105
+ : undefined
106
+
107
+ trackOrderPurchase(
108
+ {
109
+ displayId,
110
+ currency: (order.currency_code || "eur").toUpperCase(),
111
+ value,
112
+ tax: num(totals.tax_total),
113
+ shipping: num(totals.shipping_total),
114
+ lines,
115
+ customerType,
116
+ },
117
+ config
118
+ )
119
+ }, [order, items, config])
120
+
121
+ return null
122
+ }