@cartbase/storefront 0.19.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 (105) 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/reviews.ts +22 -1
  20. package/src/api/search.ts +163 -163
  21. package/src/api/store.ts +35 -35
  22. package/src/api/types.ts +91 -91
  23. package/src/cart-drawer/context.tsx +778 -778
  24. package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
  25. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
  26. package/src/cart-drawer/gift-wrap.tsx +82 -82
  27. package/src/cart-drawer/item/index.tsx +162 -162
  28. package/src/cart-drawer/item/upsell.tsx +110 -110
  29. package/src/cart-drawer/labels.ts +123 -123
  30. package/src/cart-drawer/mutation-queue.ts +78 -78
  31. package/src/cart-drawer/notes.tsx +131 -131
  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/index.ts +20 -20
  49. package/src/common/market-select.tsx +57 -57
  50. package/src/index.ts +12 -12
  51. package/src/lib/country-name.ts +59 -59
  52. package/src/lib/get-product-price.ts +133 -133
  53. package/src/lib/media-image.tsx +39 -39
  54. package/src/lib/payment-constants.ts +53 -53
  55. package/src/lib/platform.ts +13 -13
  56. package/src/lib/price.tsx +39 -39
  57. package/src/lib/store-api-error.ts +36 -36
  58. package/src/lib/variant-caption.ts +32 -32
  59. package/src/locales/bg.ts +3 -2
  60. package/src/locales/context.ts +37 -37
  61. package/src/locales/en.ts +26 -26
  62. package/src/locales/es.ts +3 -2
  63. package/src/locales/index.ts +19 -19
  64. package/src/locales/provider.tsx +59 -59
  65. package/src/locales/types.ts +77 -77
  66. package/src/order/index.ts +62 -62
  67. package/src/order/labels.ts +79 -79
  68. package/src/order/order-totals.tsx +250 -250
  69. package/src/primitives/select-field.tsx +93 -93
  70. package/src/products/image-gallery.tsx +43 -43
  71. package/src/products/option-select.tsx +67 -67
  72. package/src/products/product-info.tsx +5 -5
  73. package/src/products/product-price.tsx +69 -69
  74. package/src/products/product-promises.tsx +61 -61
  75. package/src/products/product-specs.tsx +65 -65
  76. package/src/products/product-tabs.tsx +123 -123
  77. package/src/products/purchase-options.tsx +130 -130
  78. package/src/products/sets.ts +110 -110
  79. package/src/products/variant-matching.ts +71 -71
  80. package/src/products/variant-url.ts +74 -74
  81. package/src/reviews-ui/index.ts +12 -0
  82. package/src/reviews-ui/labels.ts +5 -2
  83. package/src/reviews-ui/lightbox-state.ts +46 -46
  84. package/src/reviews-ui/review-header.tsx +166 -0
  85. package/src/reviews-ui/review-lightbox.tsx +271 -271
  86. package/src/reviews-ui/review-list.tsx +193 -191
  87. package/src/reviews-ui/review-widget.tsx +63 -71
  88. package/src/reviews-ui/star-badge.tsx +6 -4
  89. package/src/reviews-ui/widget-options.ts +55 -0
  90. package/src/store/category-template.tsx +136 -136
  91. package/src/store/index.ts +40 -40
  92. package/src/tracking/chatgpt-pixel.tsx +99 -99
  93. package/src/tracking/consent-init.tsx +62 -62
  94. package/src/tracking/events.ts +348 -348
  95. package/src/tracking/ga4.tsx +93 -93
  96. package/src/tracking/google-ads.ts +84 -84
  97. package/src/tracking/gtm.tsx +60 -60
  98. package/src/tracking/inline-script.ts +49 -49
  99. package/src/tracking/oaiq.ts +206 -206
  100. package/src/tracking/tiktok-pixel.tsx +91 -91
  101. package/src/tracking/track-init.tsx +56 -56
  102. package/src/tracking/track-order-purchase.tsx +122 -122
  103. package/src/tracking/ttq.ts +180 -180
  104. package/src/tracking/use-tracking-config.ts +54 -54
  105. package/theme/index.css +25 -25
@@ -1,180 +1,180 @@
1
- "use client"
2
-
3
- /**
4
- * Typed wrappers around the global `ttq` from the TikTok Pixel — the
5
- * TikTok twin of ./fbq.ts, and deliberately the same shape so a surface
6
- * that fires one can fire the other on the line below.
7
- *
8
- * Every helper no-ops when `window.ttq` is absent (pixel not configured,
9
- * server render, script blocked), so callers need no guards.
10
- *
11
- * Event names are case-sensitive and taken from TikTok's supported pixel
12
- * events table:
13
- * https://business-api.tiktok.com/portal/docs/supported-pixel-events/v1.3
14
- * `Purchase` is the current name; `CompletePayment` and `PlaceAnOrder` do
15
- * not appear in the current tables at all.
16
- *
17
- * `event_id` is the THIRD argument, a separate object after the
18
- * properties, per TikTok's own implementation guide:
19
- *
20
- * ttq.track('Purchase', { ...properties }, { event_id: 'tt_purchase_1044' })
21
- *
22
- * TikTok deduplicates on event_source_id + event + event_id and discards
23
- * duplicates for 48 hours from the first event, which is how the browser
24
- * Purchase and the server-side Events API Purchase collapse into one.
25
- *
26
- * `content_id` must be the SAME identifier the product catalogue uses,
27
- * which on Cartbase is the PRODUCT id — the value the feed emits as
28
- * <g:id> and the value Meta already receives. A variant id here matches
29
- * no catalogue entry at all.
30
- */
31
-
32
- /** One product line inside `contents`. */
33
- export type TikTokContentItem = {
34
- content_id: string
35
- content_name?: string
36
- content_category?: string
37
- brand?: string
38
- quantity?: number
39
- price?: number
40
- }
41
-
42
- type TikTokProperties = {
43
- contents?: TikTokContentItem[]
44
- content_id?: string
45
- content_name?: string
46
- content_type?: "product" | "product_group"
47
- currency?: string
48
- value?: number
49
- quantity?: number
50
- description?: string
51
- order_id?: string
52
- /** TikTok's new-versus-returning signal. Same computed fact the backend
53
- * persists on the order and sends to Google Ads as `new_customer`. */
54
- customer_type?: "new" | "returning"
55
- }
56
-
57
- type TtqFn = {
58
- track: (
59
- event: string,
60
- properties?: TikTokProperties,
61
- options?: { event_id?: string }
62
- ) => void
63
- page: () => void
64
- identify: (data: Record<string, unknown>) => void
65
- grantConsent: () => void
66
- revokeConsent: () => void
67
- holdConsent: () => void
68
- }
69
-
70
- function safeTtq(): TtqFn | null {
71
- if (typeof window === "undefined") return null
72
- const ttq = (window as unknown as { ttq?: Partial<TtqFn> }).ttq
73
- if (!ttq || typeof ttq.track !== "function") return null
74
- return ttq as TtqFn
75
- }
76
-
77
- /**
78
- * THE purchase dedup key — must equal what the backend forwarder sends
79
- * (`tiktokPurchaseEventId` in src/lib/tracking/constants.ts). Built from
80
- * display_id here so the format cannot drift between the two sides, the
81
- * same guarantee `trackPurchase` gives for Meta.
82
- */
83
- export function tiktokPurchaseEventId(displayId: string | number): string {
84
- return `tt_purchase_${displayId}`
85
- }
86
-
87
- export function trackTikTokViewContent(data: {
88
- contentId: string
89
- contentName?: string
90
- currency: string
91
- value: number
92
- }): void {
93
- const ttq = safeTtq()
94
- if (!ttq) return
95
- ttq.track("ViewContent", {
96
- content_id: data.contentId,
97
- content_type: "product",
98
- content_name: data.contentName,
99
- currency: data.currency,
100
- value: data.value,
101
- })
102
- }
103
-
104
- export function trackTikTokAddToCart(data: {
105
- contentId: string
106
- contentName?: string
107
- quantity: number
108
- price: number
109
- currency: string
110
- value: number
111
- }): void {
112
- const ttq = safeTtq()
113
- if (!ttq) return
114
- ttq.track("AddToCart", {
115
- contents: [
116
- {
117
- content_id: data.contentId,
118
- content_name: data.contentName,
119
- quantity: data.quantity,
120
- price: data.price,
121
- },
122
- ],
123
- content_type: "product",
124
- currency: data.currency,
125
- value: data.value,
126
- })
127
- }
128
-
129
- export function trackTikTokInitiateCheckout(data: {
130
- contents: TikTokContentItem[]
131
- currency: string
132
- value: number
133
- }): void {
134
- const ttq = safeTtq()
135
- if (!ttq) return
136
- ttq.track("InitiateCheckout", {
137
- contents: data.contents,
138
- content_type: "product",
139
- currency: data.currency,
140
- value: data.value,
141
- })
142
- }
143
-
144
- export function trackTikTokPurchase(data: {
145
- contents: TikTokContentItem[]
146
- currency: string
147
- value: number
148
- /** The order's display id — the dedup key and the order_id TikTok shows. */
149
- displayId: string | number
150
- customerType?: "new" | "returning"
151
- }): void {
152
- const ttq = safeTtq()
153
- if (!ttq) return
154
- ttq.track(
155
- "Purchase",
156
- {
157
- contents: data.contents,
158
- content_type: "product",
159
- currency: data.currency,
160
- value: data.value,
161
- order_id: String(data.displayId),
162
- ...(data.customerType ? { customer_type: data.customerType } : {}),
163
- },
164
- { event_id: tiktokPurchaseEventId(data.displayId) }
165
- )
166
- }
167
-
168
- /**
169
- * Push a live consent decision to the pixel.
170
- *
171
- * Normally you do NOT call this: `applyConsent()` in ./consent.ts already
172
- * relays every decision to gtag, fbq and ttq together. It is exported for
173
- * a host app driving the pixel from a consent layer of its own.
174
- */
175
- export function applyTikTokConsent(adsGranted: boolean): void {
176
- const ttq = safeTtq()
177
- if (!ttq) return
178
- if (adsGranted) ttq.grantConsent()
179
- else ttq.revokeConsent()
180
- }
1
+ "use client"
2
+
3
+ /**
4
+ * Typed wrappers around the global `ttq` from the TikTok Pixel — the
5
+ * TikTok twin of ./fbq.ts, and deliberately the same shape so a surface
6
+ * that fires one can fire the other on the line below.
7
+ *
8
+ * Every helper no-ops when `window.ttq` is absent (pixel not configured,
9
+ * server render, script blocked), so callers need no guards.
10
+ *
11
+ * Event names are case-sensitive and taken from TikTok's supported pixel
12
+ * events table:
13
+ * https://business-api.tiktok.com/portal/docs/supported-pixel-events/v1.3
14
+ * `Purchase` is the current name; `CompletePayment` and `PlaceAnOrder` do
15
+ * not appear in the current tables at all.
16
+ *
17
+ * `event_id` is the THIRD argument, a separate object after the
18
+ * properties, per TikTok's own implementation guide:
19
+ *
20
+ * ttq.track('Purchase', { ...properties }, { event_id: 'tt_purchase_1044' })
21
+ *
22
+ * TikTok deduplicates on event_source_id + event + event_id and discards
23
+ * duplicates for 48 hours from the first event, which is how the browser
24
+ * Purchase and the server-side Events API Purchase collapse into one.
25
+ *
26
+ * `content_id` must be the SAME identifier the product catalogue uses,
27
+ * which on Cartbase is the PRODUCT id — the value the feed emits as
28
+ * <g:id> and the value Meta already receives. A variant id here matches
29
+ * no catalogue entry at all.
30
+ */
31
+
32
+ /** One product line inside `contents`. */
33
+ export type TikTokContentItem = {
34
+ content_id: string
35
+ content_name?: string
36
+ content_category?: string
37
+ brand?: string
38
+ quantity?: number
39
+ price?: number
40
+ }
41
+
42
+ type TikTokProperties = {
43
+ contents?: TikTokContentItem[]
44
+ content_id?: string
45
+ content_name?: string
46
+ content_type?: "product" | "product_group"
47
+ currency?: string
48
+ value?: number
49
+ quantity?: number
50
+ description?: string
51
+ order_id?: string
52
+ /** TikTok's new-versus-returning signal. Same computed fact the backend
53
+ * persists on the order and sends to Google Ads as `new_customer`. */
54
+ customer_type?: "new" | "returning"
55
+ }
56
+
57
+ type TtqFn = {
58
+ track: (
59
+ event: string,
60
+ properties?: TikTokProperties,
61
+ options?: { event_id?: string }
62
+ ) => void
63
+ page: () => void
64
+ identify: (data: Record<string, unknown>) => void
65
+ grantConsent: () => void
66
+ revokeConsent: () => void
67
+ holdConsent: () => void
68
+ }
69
+
70
+ function safeTtq(): TtqFn | null {
71
+ if (typeof window === "undefined") return null
72
+ const ttq = (window as unknown as { ttq?: Partial<TtqFn> }).ttq
73
+ if (!ttq || typeof ttq.track !== "function") return null
74
+ return ttq as TtqFn
75
+ }
76
+
77
+ /**
78
+ * THE purchase dedup key — must equal what the backend forwarder sends
79
+ * (`tiktokPurchaseEventId` in src/lib/tracking/constants.ts). Built from
80
+ * display_id here so the format cannot drift between the two sides, the
81
+ * same guarantee `trackPurchase` gives for Meta.
82
+ */
83
+ export function tiktokPurchaseEventId(displayId: string | number): string {
84
+ return `tt_purchase_${displayId}`
85
+ }
86
+
87
+ export function trackTikTokViewContent(data: {
88
+ contentId: string
89
+ contentName?: string
90
+ currency: string
91
+ value: number
92
+ }): void {
93
+ const ttq = safeTtq()
94
+ if (!ttq) return
95
+ ttq.track("ViewContent", {
96
+ content_id: data.contentId,
97
+ content_type: "product",
98
+ content_name: data.contentName,
99
+ currency: data.currency,
100
+ value: data.value,
101
+ })
102
+ }
103
+
104
+ export function trackTikTokAddToCart(data: {
105
+ contentId: string
106
+ contentName?: string
107
+ quantity: number
108
+ price: number
109
+ currency: string
110
+ value: number
111
+ }): void {
112
+ const ttq = safeTtq()
113
+ if (!ttq) return
114
+ ttq.track("AddToCart", {
115
+ contents: [
116
+ {
117
+ content_id: data.contentId,
118
+ content_name: data.contentName,
119
+ quantity: data.quantity,
120
+ price: data.price,
121
+ },
122
+ ],
123
+ content_type: "product",
124
+ currency: data.currency,
125
+ value: data.value,
126
+ })
127
+ }
128
+
129
+ export function trackTikTokInitiateCheckout(data: {
130
+ contents: TikTokContentItem[]
131
+ currency: string
132
+ value: number
133
+ }): void {
134
+ const ttq = safeTtq()
135
+ if (!ttq) return
136
+ ttq.track("InitiateCheckout", {
137
+ contents: data.contents,
138
+ content_type: "product",
139
+ currency: data.currency,
140
+ value: data.value,
141
+ })
142
+ }
143
+
144
+ export function trackTikTokPurchase(data: {
145
+ contents: TikTokContentItem[]
146
+ currency: string
147
+ value: number
148
+ /** The order's display id — the dedup key and the order_id TikTok shows. */
149
+ displayId: string | number
150
+ customerType?: "new" | "returning"
151
+ }): void {
152
+ const ttq = safeTtq()
153
+ if (!ttq) return
154
+ ttq.track(
155
+ "Purchase",
156
+ {
157
+ contents: data.contents,
158
+ content_type: "product",
159
+ currency: data.currency,
160
+ value: data.value,
161
+ order_id: String(data.displayId),
162
+ ...(data.customerType ? { customer_type: data.customerType } : {}),
163
+ },
164
+ { event_id: tiktokPurchaseEventId(data.displayId) }
165
+ )
166
+ }
167
+
168
+ /**
169
+ * Push a live consent decision to the pixel.
170
+ *
171
+ * Normally you do NOT call this: `applyConsent()` in ./consent.ts already
172
+ * relays every decision to gtag, fbq and ttq together. It is exported for
173
+ * a host app driving the pixel from a consent layer of its own.
174
+ */
175
+ export function applyTikTokConsent(adsGranted: boolean): void {
176
+ const ttq = safeTtq()
177
+ if (!ttq) return
178
+ if (adsGranted) ttq.grantConsent()
179
+ else ttq.revokeConsent()
180
+ }
@@ -1,54 +1,54 @@
1
- "use client"
2
-
3
- import { useEffect, useState } from "react"
4
-
5
- import type { StorefrontClient } from "../api/http"
6
- import { getTrackingConfig } from "./get-tracking-config"
7
- import type { TrackingConfig } from "./types"
8
-
9
- /**
10
- * The store's public tag config, from a client component.
11
- *
12
- * `<StorefrontTags>` reads the same config on the SERVER, which is right
13
- * for mounting tags in a layout. This hook exists for the one place that
14
- * genuinely cannot: a client page that needs the config to decide what to
15
- * send, the order confirmation being the case that matters, because
16
- * Google Ads needs the account id and the purchase label to build a valid
17
- * `send_to`.
18
- *
19
- * Cached per browser session in module scope: the config changes only
20
- * when a merchant edits Settings → Integrations, and re-fetching it on
21
- * every mount would put a request in front of the purchase event.
22
- *
23
- * Returns undefined until the fetch resolves, and `{}` if it fails, so a
24
- * caller renders and fires the vendors that need no config either way.
25
- */
26
-
27
- let cached: TrackingConfig | undefined
28
- let inFlight: Promise<TrackingConfig> | undefined
29
-
30
- export function useTrackingConfig(
31
- client: StorefrontClient
32
- ): TrackingConfig | undefined {
33
- const [config, setConfig] = useState<TrackingConfig | undefined>(cached)
34
-
35
- useEffect(() => {
36
- if (cached) return
37
- let active = true
38
- inFlight =
39
- inFlight ??
40
- getTrackingConfig(client).then((value) => {
41
- cached = value
42
- inFlight = undefined
43
- return value
44
- })
45
- inFlight.then((value) => {
46
- if (active) setConfig(value)
47
- })
48
- return () => {
49
- active = false
50
- }
51
- }, [client])
52
-
53
- return config
54
- }
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+
5
+ import type { StorefrontClient } from "../api/http"
6
+ import { getTrackingConfig } from "./get-tracking-config"
7
+ import type { TrackingConfig } from "./types"
8
+
9
+ /**
10
+ * The store's public tag config, from a client component.
11
+ *
12
+ * `<StorefrontTags>` reads the same config on the SERVER, which is right
13
+ * for mounting tags in a layout. This hook exists for the one place that
14
+ * genuinely cannot: a client page that needs the config to decide what to
15
+ * send, the order confirmation being the case that matters, because
16
+ * Google Ads needs the account id and the purchase label to build a valid
17
+ * `send_to`.
18
+ *
19
+ * Cached per browser session in module scope: the config changes only
20
+ * when a merchant edits Settings → Integrations, and re-fetching it on
21
+ * every mount would put a request in front of the purchase event.
22
+ *
23
+ * Returns undefined until the fetch resolves, and `{}` if it fails, so a
24
+ * caller renders and fires the vendors that need no config either way.
25
+ */
26
+
27
+ let cached: TrackingConfig | undefined
28
+ let inFlight: Promise<TrackingConfig> | undefined
29
+
30
+ export function useTrackingConfig(
31
+ client: StorefrontClient
32
+ ): TrackingConfig | undefined {
33
+ const [config, setConfig] = useState<TrackingConfig | undefined>(cached)
34
+
35
+ useEffect(() => {
36
+ if (cached) return
37
+ let active = true
38
+ inFlight =
39
+ inFlight ??
40
+ getTrackingConfig(client).then((value) => {
41
+ cached = value
42
+ inFlight = undefined
43
+ return value
44
+ })
45
+ inFlight.then((value) => {
46
+ if (active) setConfig(value)
47
+ })
48
+ return () => {
49
+ active = false
50
+ }
51
+ }, [client])
52
+
53
+ return config
54
+ }
package/theme/index.css CHANGED
@@ -1,25 +1,25 @@
1
- /*
2
- * Cartbase storefront theme — the one line a storefront imports.
3
- *
4
- * @import "tailwindcss";
5
- * @import "@cartbase/storefront/theme";
6
- *
7
- * That is the whole setup. There is no Tailwind config file, no preset to
8
- * register, and no list of token names to copy: Tailwind 4 reads the theme
9
- * from CSS, and this ships filled, so a fresh install renders as a designed
10
- * store rather than grey boxes.
11
- *
12
- * To retheme, override the values from tokens.css in your own stylesheet
13
- * AFTER this import. Never edit the vocabulary — components reference those
14
- * names, and changing one moves every storefront at once.
15
- */
16
- @import "./tokens.css";
17
- @import "./theme.css";
18
-
19
- /* The page's own ground. Without this the body inherits the browser default
20
- and every surface token above renders against white regardless of theme. */
21
- body {
22
- background-color: var(--background);
23
- color: var(--foreground);
24
- font-family: var(--font-body-family);
25
- }
1
+ /*
2
+ * Cartbase storefront theme — the one line a storefront imports.
3
+ *
4
+ * @import "tailwindcss";
5
+ * @import "@cartbase/storefront/theme";
6
+ *
7
+ * That is the whole setup. There is no Tailwind config file, no preset to
8
+ * register, and no list of token names to copy: Tailwind 4 reads the theme
9
+ * from CSS, and this ships filled, so a fresh install renders as a designed
10
+ * store rather than grey boxes.
11
+ *
12
+ * To retheme, override the values from tokens.css in your own stylesheet
13
+ * AFTER this import. Never edit the vocabulary — components reference those
14
+ * names, and changing one moves every storefront at once.
15
+ */
16
+ @import "./tokens.css";
17
+ @import "./theme.css";
18
+
19
+ /* The page's own ground. Without this the body inherits the browser default
20
+ and every surface token above renders against white regardless of theme. */
21
+ body {
22
+ background-color: var(--background);
23
+ color: var(--foreground);
24
+ font-family: var(--font-body-family);
25
+ }