@cartbase/storefront 0.21.0 → 0.22.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.
- package/package.json +14 -1
- package/src/api/checkout.ts +15 -0
- package/src/api/http.ts +9 -0
- package/src/api/integrations.ts +121 -0
- package/src/checkout/card-offer.ts +68 -0
- package/src/checkout/carrier-marks.ts +53 -0
- package/src/checkout/checkout-client.tsx +71 -13
- package/src/checkout/checkout-error-screen.tsx +113 -0
- package/src/checkout/discount-section.tsx +88 -56
- package/src/checkout/fulfillment-option.ts +30 -0
- package/src/checkout/index.ts +41 -6
- package/src/checkout/labels.ts +98 -0
- package/src/checkout/line-item-card.tsx +35 -83
- package/src/checkout/mobile-checkout-bottom-bar.tsx +132 -0
- package/src/checkout/mobile-checkout-top-bar.tsx +94 -0
- package/src/checkout/mobile-order-summary-body.tsx +172 -0
- package/src/checkout/order-summary.tsx +85 -179
- package/src/checkout/payment-button.tsx +25 -8
- package/src/checkout/payment-method-list.tsx +51 -13
- package/src/checkout/payment-wrapper.tsx +57 -13
- package/src/checkout/pickup-option.ts +35 -0
- package/src/checkout/pickup-point-selector.tsx +372 -0
- package/src/checkout/pigeon-office-selector.tsx +379 -0
- package/src/checkout/shipping-method-list.tsx +102 -10
- package/src/checkout/summary-math.ts +152 -0
- package/src/checkout/use-checkout-funnel.ts +303 -0
- package/src/checkout/use-checkout-orchestration.ts +384 -30
- package/src/lib/stripe-env.ts +25 -0
- package/src/locales/bg.ts +37 -1
- package/src/locales/es.ts +35 -1
- package/src/tracking/attribution.ts +83 -0
- package/src/tracking/consent.ts +53 -0
- package/src/tracking/events.ts +113 -0
- package/src/tracking/fbq.ts +48 -0
- package/src/tracking/gtag.ts +32 -0
- package/src/tracking/index.ts +32 -1
- package/src/tracking/once.ts +137 -0
- package/src/tracking/rybbit-events.ts +42 -0
- package/src/tracking/ttq.ts +24 -0
- package/src/tracking/types.ts +34 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* once — "fire this tracking event at most once per cart" guard.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists:
|
|
7
|
+
*
|
|
8
|
+
* The mid-funnel checkout events (add_shipping_info, add_payment_info)
|
|
9
|
+
* are driven by FORM STATE, not by a one-shot user action. A shopper
|
|
10
|
+
* can switch courier, toggle card and cash on delivery, correct a typo
|
|
11
|
+
* in their address or refresh the page, and each of those re-runs the
|
|
12
|
+
* effect that would fire the event. Without a guard one shopper emits a
|
|
13
|
+
* dozen add_payment_info events, which wrecks the funnel ratios in GA4
|
|
14
|
+
* and Rybbit and teaches Meta's optimizer that the event is worthless.
|
|
15
|
+
*
|
|
16
|
+
* A `useRef` guard is NOT enough: it lives in React memory and resets on
|
|
17
|
+
* every remount, so a refresh or a walk back into the checkout re-fires.
|
|
18
|
+
* The guard has to outlive the component.
|
|
19
|
+
*
|
|
20
|
+
* Design:
|
|
21
|
+
*
|
|
22
|
+
* - Keyed by `${event}:${cartId}` so a genuinely NEW cart (the shopper
|
|
23
|
+
* bought, then started a second order) fires its own events. The same
|
|
24
|
+
* cart never fires twice.
|
|
25
|
+
* - `sessionStorage`, not `localStorage`: the guard should expire when
|
|
26
|
+
* the browsing session does. A shopper returning tomorrow to the same
|
|
27
|
+
* abandoned cart is a new session and legitimately re-enters the
|
|
28
|
+
* funnel.
|
|
29
|
+
* - An in-memory Set mirrors the store, so repeated calls within one
|
|
30
|
+
* page never touch sessionStorage (Safari throws on quota and in
|
|
31
|
+
* private mode; we degrade to memory-only rather than fire twice).
|
|
32
|
+
*
|
|
33
|
+
* Belt and braces: callers pair this with a DETERMINISTIC `event_id` (see
|
|
34
|
+
* `checkoutStepEventId`) so that even if the guard is defeated, by two
|
|
35
|
+
* tabs on one cart or by storage cleared mid-session, Meta still collapses
|
|
36
|
+
* the duplicates server-side by event_id, exactly as it does for
|
|
37
|
+
* Purchase's `purchase_${display_id}`.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const STORAGE_KEY = "cartbase:fired-events"
|
|
41
|
+
|
|
42
|
+
/** Mirrors sessionStorage so repeat calls in one page skip storage I/O. */
|
|
43
|
+
const memory = new Set<string>()
|
|
44
|
+
|
|
45
|
+
function readStore(): Set<string> {
|
|
46
|
+
if (memory.size > 0) return memory
|
|
47
|
+
if (typeof window === "undefined") return memory
|
|
48
|
+
try {
|
|
49
|
+
const raw = window.sessionStorage.getItem(STORAGE_KEY)
|
|
50
|
+
if (raw) {
|
|
51
|
+
const parsed = JSON.parse(raw) as unknown
|
|
52
|
+
if (Array.isArray(parsed)) {
|
|
53
|
+
for (const k of parsed) if (typeof k === "string") memory.add(k)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Private mode, quota, corrupt JSON: memory-only from here.
|
|
58
|
+
}
|
|
59
|
+
return memory
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function persist(): void {
|
|
63
|
+
if (typeof window === "undefined") return
|
|
64
|
+
try {
|
|
65
|
+
window.sessionStorage.setItem(
|
|
66
|
+
STORAGE_KEY,
|
|
67
|
+
JSON.stringify(Array.from(memory))
|
|
68
|
+
)
|
|
69
|
+
} catch {
|
|
70
|
+
// Storage unavailable: the in-memory Set still dedupes this page.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Returns true the FIRST time it is called for a given (event, cartId)
|
|
76
|
+
* pair and false every time after, marking the pair as fired.
|
|
77
|
+
*
|
|
78
|
+
* Callers treat it as the gate itself:
|
|
79
|
+
*
|
|
80
|
+
* if (!markFiredOnce("add_payment_info", cart.id)) return
|
|
81
|
+
* trackCheckoutPaymentInfo(...)
|
|
82
|
+
*
|
|
83
|
+
* Returns false when `cartId` is missing: a checkout-step event with no
|
|
84
|
+
* cart to key on cannot be deduped, and firing an undedupable event is
|
|
85
|
+
* worse than dropping it.
|
|
86
|
+
*/
|
|
87
|
+
export function markFiredOnce(
|
|
88
|
+
event: string,
|
|
89
|
+
cartId: string | undefined | null
|
|
90
|
+
): boolean {
|
|
91
|
+
if (!cartId) return false
|
|
92
|
+
if (typeof window === "undefined") return false
|
|
93
|
+
|
|
94
|
+
const key = `${event}:${cartId}`
|
|
95
|
+
const store = readStore()
|
|
96
|
+
if (store.has(key)) return false
|
|
97
|
+
|
|
98
|
+
store.add(key)
|
|
99
|
+
persist()
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Read-only check, does not mark. For conditional UI and debugging. */
|
|
104
|
+
export function hasFired(
|
|
105
|
+
event: string,
|
|
106
|
+
cartId: string | undefined | null
|
|
107
|
+
): boolean {
|
|
108
|
+
if (!cartId || typeof window === "undefined") return false
|
|
109
|
+
return readStore().has(`${event}:${cartId}`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Deterministic Meta `event_id` for a checkout-step event.
|
|
114
|
+
*
|
|
115
|
+
* Unlike `generateEventId()` (random, for events with no natural key),
|
|
116
|
+
* this derives the id from the cart, so the SAME logical action always
|
|
117
|
+
* produces the SAME id. That gives Meta a server-side dedup key: two tabs,
|
|
118
|
+
* a restored session or a retried request all collapse into one event
|
|
119
|
+
* instead of inflating the count. Same principle as Purchase's
|
|
120
|
+
* `purchase_${display_id}`.
|
|
121
|
+
*/
|
|
122
|
+
export function checkoutStepEventId(event: string, cartId: string): string {
|
|
123
|
+
return `${event}_${cartId}`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Forget every guard for one cart. Called when the order is placed, so a
|
|
128
|
+
* shopper who starts a second order in the same session is not silenced by
|
|
129
|
+
* the keys of the cart they just bought.
|
|
130
|
+
*/
|
|
131
|
+
export function forgetFiredEvents(cartId: string | undefined | null): void {
|
|
132
|
+
if (!cartId) return
|
|
133
|
+
for (const key of Array.from(memory)) {
|
|
134
|
+
if (key.endsWith(`:${cartId}`)) memory.delete(key)
|
|
135
|
+
}
|
|
136
|
+
persist()
|
|
137
|
+
}
|
|
@@ -179,6 +179,22 @@ export type RybbitBeginCheckoutData = {
|
|
|
179
179
|
value: number
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
export type RybbitAddShippingInfoData = {
|
|
183
|
+
item_ids: string[]
|
|
184
|
+
num_items: number
|
|
185
|
+
currency: string
|
|
186
|
+
value: number
|
|
187
|
+
shipping_tier?: string
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export type RybbitAddPaymentInfoData = {
|
|
191
|
+
item_ids: string[]
|
|
192
|
+
num_items: number
|
|
193
|
+
currency: string
|
|
194
|
+
value: number
|
|
195
|
+
payment_type?: string
|
|
196
|
+
}
|
|
197
|
+
|
|
182
198
|
export type RybbitPurchaseData = {
|
|
183
199
|
transaction_id: string
|
|
184
200
|
item_ids: string[]
|
|
@@ -217,6 +233,32 @@ export function trackRybbitBeginCheckout(
|
|
|
217
233
|
})
|
|
218
234
|
}
|
|
219
235
|
|
|
236
|
+
export function trackRybbitAddShippingInfo(
|
|
237
|
+
data: RybbitAddShippingInfoData
|
|
238
|
+
): void {
|
|
239
|
+
fireEvent("add_shipping_info", {
|
|
240
|
+
item_ids: joinIds(data.item_ids),
|
|
241
|
+
num_items: data.num_items,
|
|
242
|
+
currency: data.currency,
|
|
243
|
+
value: data.value,
|
|
244
|
+
// Rybbit accepts string and number only, so an empty tier is omitted
|
|
245
|
+
// rather than sent as "".
|
|
246
|
+
...(data.shipping_tier ? { shipping_tier: data.shipping_tier } : {}),
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function trackRybbitAddPaymentInfo(
|
|
251
|
+
data: RybbitAddPaymentInfoData
|
|
252
|
+
): void {
|
|
253
|
+
fireEvent("add_payment_info", {
|
|
254
|
+
item_ids: joinIds(data.item_ids),
|
|
255
|
+
num_items: data.num_items,
|
|
256
|
+
currency: data.currency,
|
|
257
|
+
value: data.value,
|
|
258
|
+
...(data.payment_type ? { payment_type: data.payment_type } : {}),
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
220
262
|
/**
|
|
221
263
|
* Purchase event — fired client-side on the order-confirmed page.
|
|
222
264
|
*
|
package/src/tracking/ttq.ts
CHANGED
|
@@ -141,6 +141,30 @@ export function trackTikTokInitiateCheckout(data: {
|
|
|
141
141
|
})
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* AddPaymentInfo — TikTok's own standard event, fired when the shopper has
|
|
146
|
+
* settled how they will pay. There is no TikTok equivalent of
|
|
147
|
+
* `add_shipping_info`, so the shipping step reaches Meta, GA4 and Rybbit
|
|
148
|
+
* and stops there; inventing a custom event on this vendor would optimise
|
|
149
|
+
* nothing and report nowhere.
|
|
150
|
+
*/
|
|
151
|
+
export function trackTikTokAddPaymentInfo(data: {
|
|
152
|
+
contents: TikTokContentItem[]
|
|
153
|
+
currency: string
|
|
154
|
+
value: number
|
|
155
|
+
paymentType?: string
|
|
156
|
+
}): void {
|
|
157
|
+
const ttq = safeTtq()
|
|
158
|
+
if (!ttq) return
|
|
159
|
+
ttq.track("AddPaymentInfo", {
|
|
160
|
+
contents: data.contents,
|
|
161
|
+
content_type: "product",
|
|
162
|
+
currency: data.currency,
|
|
163
|
+
value: data.value,
|
|
164
|
+
...(data.paymentType ? { payment_type: data.paymentType } : {}),
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
144
168
|
export function trackTikTokPurchase(data: {
|
|
145
169
|
contents: TikTokContentItem[]
|
|
146
170
|
currency: string
|
package/src/tracking/types.ts
CHANGED
|
@@ -93,6 +93,40 @@ export type InitiateCheckoutData = {
|
|
|
93
93
|
contents: MetaContentItem[]
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* AddShippingInfo. Not one of Meta's standard events, so the Pixel takes
|
|
98
|
+
* it through `trackCustom`; GA4 and Rybbit both name it
|
|
99
|
+
* `add_shipping_info`. `shipping_tier` is the courier the shopper chose,
|
|
100
|
+
* omitted while they have filled the address but picked nobody yet.
|
|
101
|
+
*/
|
|
102
|
+
export type AddShippingInfoData = {
|
|
103
|
+
content_ids: string[]
|
|
104
|
+
content_type: "product" | "product_group"
|
|
105
|
+
currency: string
|
|
106
|
+
value: number
|
|
107
|
+
num_items: number
|
|
108
|
+
contents: MetaContentItem[]
|
|
109
|
+
shipping_tier?: string
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* AddPaymentInfo, a Meta standard event. Cash on delivery counts: Meta
|
|
114
|
+
* defines the event as "payment information is added in the checkout
|
|
115
|
+
* flow" and GA4 defines `payment_type` as "the chosen method of payment",
|
|
116
|
+
* so choosing to pay the courier completes the step, it just involves no
|
|
117
|
+
* card. Carrying `payment_type` is what keeps the two paths separable in
|
|
118
|
+
* reporting.
|
|
119
|
+
*/
|
|
120
|
+
export type AddPaymentInfoData = {
|
|
121
|
+
content_ids: string[]
|
|
122
|
+
content_type: "product" | "product_group"
|
|
123
|
+
currency: string
|
|
124
|
+
value: number
|
|
125
|
+
num_items: number
|
|
126
|
+
contents: MetaContentItem[]
|
|
127
|
+
payment_type?: string
|
|
128
|
+
}
|
|
129
|
+
|
|
96
130
|
export type PurchaseData = {
|
|
97
131
|
content_ids: string[]
|
|
98
132
|
content_type: "product" | "product_group"
|