@cartbase/storefront 0.5.0 → 0.7.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.
@@ -1,133 +1,176 @@
1
- /**
2
- * @cartbase/storefront/tracking — Meta Pixel + GA4 + Rybbit + Consent Mode v2.
3
- *
4
- * Ported from `@1click/ui/src/tracking` (v2.3.1) with the Cartbase seams:
5
- * - config comes from `GET /api/store/integrations` → `tracking` block
6
- * via `getTrackingConfig(client)` (never env vars)
7
- * - server-side CAPI / GA4-MP sending is Cartbase-backend-owned (the
8
- * `order.placed` forwarder) — this package fires CLIENT events only
9
- * and writes attribution for the server to inherit
10
- * - Purchase dedupe: Pixel `eventID = "purchase_" + order.display_id`
11
- * (trackPurchase builds it); GA4 `transaction_id =
12
- * String(order.display_id)`
13
- *
14
- * Public surface:
15
- * - <ConsentInit> / <ConsentBanner> / <ConsentSettingsLink> — Consent
16
- * Mode v2 (config from GET /api/store/consent; `_1c_consent` seam)
17
- * - <MetaPixel pixelId> / <GA4 measurementId> / <Rybbit siteId> — layout tags
18
- * - Meta Pixel client helpers: trackViewContent / trackAddToCart /
19
- * trackInitiateCheckout / trackPurchase / trackLead
20
- * - GA4 client helpers: trackGAViewItem / trackGAAddToCart /
21
- * trackGABeginCheckout / trackGAPurchase / setEnhancedConversions
22
- * - Rybbit client helpers: trackRybbit* (queue-buffered)
23
- * - getTrackingConfig(client) the public tag config
24
- * - getTrackingAttribution(clientHints?, opts?) server-side signal
25
- * gathering for the cart.metadata writeback (consent-gated)
26
- * - getEngagementTimeMsec() / initEngagementTime() — time-on-session
27
- *
28
- * Server vs client split (per Next.js App Router rules):
29
- * - get-tracking-attribution.ts imports `next/headers` (server-only by
30
- * construction); get-tracking-config.ts is isomorphic
31
- * - meta-pixel.tsx, ga4.tsx, rybbit.tsx, fbq.ts, gtag.ts,
32
- * rybbit-events.ts, attribution.ts, use-engagement-time.ts,
33
- * consent-banner.tsx are `"use client"`
34
- * - types.ts, consent.ts, consent-init.tsx are universal
35
- *
36
- * Importers should usually pull from the subpath that matches their
37
- * environment. The barrel re-exports everything, but tree-shaking and
38
- * Next.js's RSC boundary detection both work better with subpaths.
39
- */
40
-
41
- export { MetaPixel, updatePixelAdvancedMatching } from "./meta-pixel"
42
- export { GA4 } from "./ga4"
43
- export { Rybbit } from "./rybbit"
44
- export { ConsentInit } from "./consent-init"
45
- export { ConsentBanner, ConsentBannerCard, ConsentSettingsLink } from "./consent-banner"
46
- export {
47
- applyConsent,
48
- setConsent,
49
- openConsentSettings,
50
- readConsentCookie,
51
- writeConsentCookie,
52
- shouldRenderBanner,
53
- pickConsentCopy,
54
- CONSENT_COOKIE,
55
- CONSENT_MAX_AGE_SECONDS,
56
- CONSENT_OPEN_EVENT,
57
- CONSENT_INIT_SNIPPET,
58
- type ConsentChoices,
59
- type ConsentCopy,
60
- type ConsentLayout,
61
- type ConsentMode,
62
- type ConsentSettings,
63
- } from "./consent"
64
- export {
65
- trackViewContent,
66
- trackAddToCart,
67
- trackInitiateCheckout,
68
- trackPurchase,
69
- trackLead,
70
- generateEventId,
71
- type LeadContext,
72
- } from "./fbq"
73
- export {
74
- setTrackingDefaults,
75
- getOrCreateAnonId,
76
- getOrCreateFbp,
77
- getOrCreateFbc,
78
- getKnownVisitor,
79
- rememberKnownVisitor,
80
- captureUtmsFromUrl,
81
- getCapturedFirstTouchUtms,
82
- getCapturedLastTouchUtms,
83
- sha256Hex,
84
- normaliseEmailForHash,
85
- normalisePhoneForHash,
86
- type KnownVisitor,
87
- type CapturedUtms,
88
- } from "./attribution"
89
- export {
90
- trackGAViewItem,
91
- trackGAAddToCart,
92
- trackGABeginCheckout,
93
- trackGAPurchase,
94
- setEnhancedConversions,
95
- type EnhancedConversionsInput,
96
- } from "./gtag"
97
- export {
98
- trackRybbitViewItem,
99
- trackRybbitAddToCart,
100
- trackRybbitBeginCheckout,
101
- trackRybbitPurchase,
102
- type RybbitViewItemData,
103
- type RybbitAddToCartData,
104
- type RybbitBeginCheckoutData,
105
- type RybbitPurchaseData,
106
- } from "./rybbit-events"
107
- export { getTrackingConfig } from "./get-tracking-config"
108
- export { getTrackingAttribution } from "./get-tracking-attribution"
109
- export {
110
- getEngagementTimeMsec,
111
- initEngagementTime,
112
- } from "./use-engagement-time"
113
-
114
- export type {
115
- TrackingConfig,
116
- TrackingConfigResponse,
117
- TrackingAttribution,
118
- TrackingClientHints,
119
- MetaContentItem,
120
- ViewContentData,
121
- AddToCartData,
122
- InitiateCheckoutData,
123
- PurchaseData,
124
- LeadData,
125
- } from "./types"
126
-
127
- export type {
128
- GA4Item,
129
- GA4ViewItemData,
130
- GA4AddToCartData,
131
- GA4BeginCheckoutData,
132
- GA4PurchaseData,
133
- } from "./gtag"
1
+ /**
2
+ * @cartbase/storefront/tracking — Meta Pixel + GA4 + Rybbit + Consent Mode v2.
3
+ *
4
+ * Ported from `@1click/ui/src/tracking` (v2.3.1) with the Cartbase seams:
5
+ * - config comes from `GET /api/store/integrations` → `tracking` block
6
+ * via `getTrackingConfig(client)` (never env vars)
7
+ * - server-side CAPI / GA4-MP sending is Cartbase-backend-owned (the
8
+ * `order.placed` forwarder) — this package fires CLIENT events only
9
+ * and writes attribution for the server to inherit
10
+ * - Purchase dedupe: Pixel `eventID = "purchase_" + order.display_id`
11
+ * (trackPurchase builds it); GA4 `transaction_id =
12
+ * String(order.display_id)`
13
+ *
14
+ * Public surface:
15
+ * - <ConsentInit> / <ConsentBanner> / <ConsentSettingsLink> — Consent
16
+ * Mode v2 (config from GET /api/store/consent; `_1c_consent` seam)
17
+ * - <StorefrontTags client> EVERY tag the store configured, mounted
18
+ * from its own config. This is the one a storefront should mount;
19
+ * the individual tags below are for a layout that needs control.
20
+ * - <TrackInit /> captures UTMs and ad-click ids on the landing page
21
+ * (they exist only there) and starts the engagement clock.
22
+ * - <MetaPixel pixelId> / <GoogleTag measurementId adsConversionId> /
23
+ * <TikTokPixel pixelId> / <Gtm containerId> / <Rybbit siteId> —
24
+ * individual layout tags (<GA4> is <GoogleTag> with only the
25
+ * analytics destination)
26
+ * - ONE call per commerce moment, every vendor at once (prefer these):
27
+ * trackProductView / trackCartAdd / trackCheckoutStart /
28
+ * trackOrderPurchase
29
+ * - Meta Pixel client helpers: trackViewContent / trackAddToCart /
30
+ * trackInitiateCheckout / trackPurchase / trackLead
31
+ * - GA4 client helpers: trackGAViewItem / trackGAAddToCart /
32
+ * trackGABeginCheckout / trackGAPurchase / setEnhancedConversions
33
+ * - Rybbit client helpers: trackRybbit* (queue-buffered)
34
+ * - getTrackingConfig(client) the public tag config
35
+ * - getTrackingAttribution(clientHints?, opts?) — server-side signal
36
+ * gathering for the cart.metadata writeback (consent-gated)
37
+ * - getEngagementTimeMsec() / initEngagementTime() time-on-session
38
+ *
39
+ * Server vs client split (per Next.js App Router rules):
40
+ * - get-tracking-attribution.ts imports `next/headers` (server-only by
41
+ * construction); get-tracking-config.ts is isomorphic
42
+ * - meta-pixel.tsx, ga4.tsx, rybbit.tsx, fbq.ts, gtag.ts,
43
+ * rybbit-events.ts, attribution.ts, use-engagement-time.ts,
44
+ * consent-banner.tsx are `"use client"`
45
+ * - types.ts, consent.ts, consent-init.tsx are universal
46
+ *
47
+ * Importers should usually pull from the subpath that matches their
48
+ * environment. The barrel re-exports everything, but tree-shaking and
49
+ * Next.js's RSC boundary detection both work better with subpaths.
50
+ */
51
+
52
+ export { MetaPixel, updatePixelAdvancedMatching } from "./meta-pixel"
53
+ export { GA4, GoogleTag } from "./ga4"
54
+ export { Gtm } from "./gtm"
55
+ export { TikTokPixel } from "./tiktok-pixel"
56
+ export { StorefrontTags } from "./storefront-tags"
57
+ export { TrackInit } from "./track-init"
58
+ export { TrackOrderPurchase } from "./track-order-purchase"
59
+ export { Rybbit } from "./rybbit"
60
+ export { ConsentInit } from "./consent-init"
61
+ export { ConsentBanner, ConsentBannerCard, ConsentSettingsLink } from "./consent-banner"
62
+ export {
63
+ applyConsent,
64
+ setConsent,
65
+ openConsentSettings,
66
+ readConsentCookie,
67
+ writeConsentCookie,
68
+ shouldRenderBanner,
69
+ pickConsentCopy,
70
+ CONSENT_COOKIE,
71
+ CONSENT_MAX_AGE_SECONDS,
72
+ CONSENT_OPEN_EVENT,
73
+ CONSENT_INIT_SNIPPET,
74
+ type ConsentChoices,
75
+ type ConsentCopy,
76
+ type ConsentLayout,
77
+ type ConsentMode,
78
+ type ConsentSettings,
79
+ } from "./consent"
80
+ export {
81
+ trackViewContent,
82
+ trackAddToCart,
83
+ trackInitiateCheckout,
84
+ trackPurchase,
85
+ trackLead,
86
+ generateEventId,
87
+ type LeadContext,
88
+ } from "./fbq"
89
+ export {
90
+ setTrackingDefaults,
91
+ getOrCreateAnonId,
92
+ getOrCreateFbp,
93
+ getOrCreateFbc,
94
+ getKnownVisitor,
95
+ rememberKnownVisitor,
96
+ captureUtmsFromUrl,
97
+ captureClickIdsFromUrl,
98
+ getCapturedFirstTouchUtms,
99
+ getCapturedLastTouchUtms,
100
+ sha256Hex,
101
+ normaliseEmailForHash,
102
+ normalisePhoneForHash,
103
+ normalisePhoneForGoogleHash,
104
+ CLICK_ID_COOKIES,
105
+ CLICK_ID_METADATA_KEYS,
106
+ type KnownVisitor,
107
+ type CapturedUtms,
108
+ } from "./attribution"
109
+ export {
110
+ trackProductView,
111
+ trackCartAdd,
112
+ trackCheckoutStart,
113
+ trackOrderPurchase,
114
+ type TrackedLine,
115
+ type TrackedOrder,
116
+ } from "./events"
117
+ export {
118
+ trackTikTokViewContent,
119
+ trackTikTokAddToCart,
120
+ trackTikTokInitiateCheckout,
121
+ trackTikTokPurchase,
122
+ applyTikTokConsent,
123
+ tiktokPurchaseEventId,
124
+ type TikTokContentItem,
125
+ } from "./ttq"
126
+ export {
127
+ trackGoogleAdsPurchase,
128
+ googleAdsPurchaseSendTo,
129
+ type GoogleAdsPurchaseInput,
130
+ } from "./google-ads"
131
+ export {
132
+ trackGAViewItem,
133
+ trackGAAddToCart,
134
+ trackGABeginCheckout,
135
+ trackGAPurchase,
136
+ setEnhancedConversions,
137
+ type EnhancedConversionsInput,
138
+ } from "./gtag"
139
+ export {
140
+ trackRybbitViewItem,
141
+ trackRybbitAddToCart,
142
+ trackRybbitBeginCheckout,
143
+ trackRybbitPurchase,
144
+ type RybbitViewItemData,
145
+ type RybbitAddToCartData,
146
+ type RybbitBeginCheckoutData,
147
+ type RybbitPurchaseData,
148
+ } from "./rybbit-events"
149
+ export { getTrackingConfig } from "./get-tracking-config"
150
+ export { useTrackingConfig } from "./use-tracking-config"
151
+ export { getTrackingAttribution } from "./get-tracking-attribution"
152
+ export {
153
+ getEngagementTimeMsec,
154
+ initEngagementTime,
155
+ } from "./use-engagement-time"
156
+
157
+ export type {
158
+ TrackingConfig,
159
+ TrackingConfigResponse,
160
+ TrackingAttribution,
161
+ TrackingClientHints,
162
+ MetaContentItem,
163
+ ViewContentData,
164
+ AddToCartData,
165
+ InitiateCheckoutData,
166
+ PurchaseData,
167
+ LeadData,
168
+ } from "./types"
169
+
170
+ export type {
171
+ GA4Item,
172
+ GA4ViewItemData,
173
+ GA4AddToCartData,
174
+ GA4BeginCheckoutData,
175
+ GA4PurchaseData,
176
+ } from "./gtag"
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Safe interpolation of a configured id into a vendor's inline snippet.
3
+ *
4
+ * Every tag component here injects the vendor's own bootstrap through
5
+ * `dangerouslySetInnerHTML`, with the merchant's pixel / measurement /
6
+ * conversion id written into it. Those ids arrive from the store's
7
+ * integration settings, which means they are typed by a person in the
8
+ * admin, and a value carrying a quote would close the string literal and
9
+ * run whatever follows it on every page of that storefront.
10
+ *
11
+ * On a single-merchant site that is only self-inflicted. On a platform it
12
+ * is one tenant's admin user writing script into that tenant's public
13
+ * storefront, so the escape is not optional.
14
+ *
15
+ * Returns the value as a COMPLETE single-quoted JavaScript literal,
16
+ * quotes included, so callers cannot forget to quote it themselves:
17
+ *
18
+ * `fbq('init', ${jsStringLiteral(pixelId)});`
19
+ */
20
+ const LINE_SEPARATORS = new RegExp("[\u2028\u2029]", "g")
21
+
22
+ export function jsStringLiteral(value: string): string {
23
+ const escaped = String(value)
24
+ .replace(/\\/g, "\\\\")
25
+ .replace(/'/g, "\\'")
26
+ .replace(/\r/g, "\\r")
27
+ .replace(/\n/g, "\\n")
28
+ // `</script` inside an inline script closes the element wherever it
29
+ // appears, quoted or not.
30
+ .replace(/<\//g, "<\\/")
31
+ // U+2028 and U+2029 are line terminators to a JavaScript parser, so
32
+ // they break a string literal exactly like a newline does. The class
33
+ // is built from escapes rather than written literally, because a
34
+ // source file carrying those characters raw is its own version of
35
+ // this problem (it breaks the file that fixes it).
36
+ .replace(LINE_SEPARATORS, (ch) =>
37
+ ch.charCodeAt(0) === 0x2028 ? "\\u2028" : "\\u2029"
38
+ )
39
+ return `'${escaped}'`
40
+ }
41
+
42
+ /**
43
+ * The same value for a URL query parameter (the loader `src`), where the
44
+ * hazard is a crafted id breaking out of the parameter rather than out of
45
+ * a string literal.
46
+ */
47
+ export function urlParam(value: string): string {
48
+ return encodeURIComponent(String(value))
49
+ }
@@ -0,0 +1,67 @@
1
+ import { GoogleTag } from "./ga4"
2
+ import { Gtm } from "./gtm"
3
+ import { MetaPixel } from "./meta-pixel"
4
+ import { TikTokPixel } from "./tiktok-pixel"
5
+ import { getTrackingConfig } from "./get-tracking-config"
6
+ import type { StorefrontClient } from "../api/http"
7
+ import type { TrackingConfig } from "./types"
8
+
9
+ /**
10
+ * StorefrontTags — every marketing tag the store has configured, mounted
11
+ * from the store's own config.
12
+ *
13
+ * THE POINT: a merchant who saves their pixel ids in the admin gets those
14
+ * tags on their storefront, with nothing else to do and no code to write.
15
+ * Before this existed the package EXPORTED tags and no storefront mounted
16
+ * them, so the admin could serve a Meta pixel id, a GA4 id and a Google
17
+ * Ads conversion id that reached the browser and did nothing. Adding a
18
+ * vendor is one line HERE now, and every store built on the package gains
19
+ * it at the next deploy.
20
+ *
21
+ * Consent: nothing extra to wire. <ConsentInit> sets the Consent Mode v2
22
+ * defaults synchronously ahead of these, the Meta and TikTok snippets read
23
+ * the shared consent cookie before they are allowed to write cookies, and
24
+ * `applyConsent()` relays a live decision to all three vendors. Mount this
25
+ * AFTER <ConsentInit> and the gate holds.
26
+ *
27
+ * Google is deliberately ONE tag with two destinations rather than two
28
+ * loaders — see the comment block in ./ga4.tsx. It is also why the Ads
29
+ * conversion inherits the consent gate for free.
30
+ *
31
+ * Server component: pass a `client` and it fetches the config (cached),
32
+ * or pass an already-fetched `config` when the layout has one in hand.
33
+ * Renders nothing for a vendor the store has not configured, so a store
34
+ * running only GA4 ships exactly one tag.
35
+ */
36
+ export async function StorefrontTags({
37
+ client,
38
+ config,
39
+ }: {
40
+ /** Used to fetch the tracking config when `config` is not supplied. */
41
+ client?: StorefrontClient
42
+ /** Pre-fetched tracking block, e.g. from a layout that already has it. */
43
+ config?: TrackingConfig
44
+ }) {
45
+ let tracking: TrackingConfig | undefined = config
46
+ if (!tracking) {
47
+ if (!client) return null
48
+ try {
49
+ tracking = await getTrackingConfig(client)
50
+ } catch {
51
+ // A tracking-config failure must never take a storefront down.
52
+ return null
53
+ }
54
+ }
55
+
56
+ return (
57
+ <>
58
+ <MetaPixel pixelId={tracking.facebookPixel?.pixelId} />
59
+ <TikTokPixel pixelId={tracking.tiktok?.pixelId} />
60
+ <GoogleTag
61
+ measurementId={tracking.ga4?.measurementId}
62
+ adsConversionId={tracking.googleAds?.conversionId}
63
+ />
64
+ <Gtm containerId={tracking.gtm?.containerId} />
65
+ </>
66
+ )
67
+ }
@@ -0,0 +1,83 @@
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
+ * TikTokPixel — client-side base pixel injector, the TikTok twin of
10
+ * <MetaPixel>.
11
+ *
12
+ * Injects TikTok's own base snippet with `<Script
13
+ * strategy="afterInteractive">`, resolves consent from the `_1c_consent`
14
+ * cookie BEFORE the pixel is allowed to write cookies, then loads and
15
+ * fires the initial page view.
16
+ *
17
+ * Consent model. TikTok's base code registers `holdConsent`,
18
+ * `grantConsent` and `revokeConsent` on `ttq` alongside `page` and
19
+ * `track`, and `setAndDefer` queues every call onto the `ttq` array
20
+ * before the SDK is fetched. So the Meta pattern transfers exactly:
21
+ *
22
+ * ttq.holdConsent() // queued before load — cookies withheld
23
+ * ttq.load(pixelId)
24
+ * ttq.page()
25
+ * ttq.grantConsent() // visitor accepted marketing
26
+ * ttq.revokeConsent() // visitor declined, or withdrew later
27
+ *
28
+ * HOLDING rather than withholding the script is the point, and it is the
29
+ * same reasoning as the Meta gate above it: under load-on-consent, a
30
+ * visitor who browses three products and only then accepts gives TikTok
31
+ * nothing for those three pages. Holding queues them and the grant
32
+ * releases them.
33
+ *
34
+ * Live decisions arrive through `applyConsent()` in ./consent.ts, which
35
+ * relays to gtag, fbq and ttq together — there is no cookie watcher.
36
+ *
37
+ * One dependency outside the code: Cookie Consent Mode must be switched
38
+ * on for this pixel in TikTok Events Manager. With it off, `holdConsent`
39
+ * is an inert queued call and the pixel behaves like any unconsented
40
+ * pixel — no error, no crash, and no gate. That switch belongs to whoever
41
+ * owns the ad account.
42
+ *
43
+ * Renders nothing when `pixelId` is falsy, so a layout can mount it
44
+ * unconditionally.
45
+ */
46
+ export function TikTokPixel({ pixelId }: { pixelId?: string }) {
47
+ if (!pixelId) return null
48
+
49
+ const id = jsStringLiteral(pixelId)
50
+
51
+ const initSnippet = `
52
+ !function (w, d, t) {
53
+ w.TiktokAnalyticsObject=t;
54
+ var ttq=w[t]=w[t]||[];
55
+ ttq.methods=["page","track","identify","instances","debug","on","off","once","ready","alias","group","enableCookie","disableCookie","holdConsent","revokeConsent","grantConsent"];
56
+ ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};
57
+ for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);
58
+ ttq.instance=function(t){var e=ttq._i[t]||[];for(var n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e};
59
+ ttq.load=function(e,n){var r="https://analytics.tiktok.com/i18n/pixel/events.js";ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};var o=d.createElement("script");o.type="text/javascript",o.async=!0,o.src=r+"?sdkid="+e+"&lib="+t;var a=d.getElementsByTagName("script")[0];a.parentNode.insertBefore(o,a)};
60
+
61
+ ttq.holdConsent();
62
+ ttq.load(${id});
63
+ ttq.page();
64
+
65
+ (function(){
66
+ var ads=false;
67
+ try{
68
+ var m=d.cookie.match(/(?:^|; )${CONSENT_COOKIE}=([^;]*)/);
69
+ if(m){ads=!!JSON.parse(decodeURIComponent(m[1])).ads;}
70
+ }catch(e){}
71
+ if(ads){ttq.grantConsent();}else{ttq.revokeConsent();}
72
+ })();
73
+ }(window, document, 'ttq');
74
+ `.trim()
75
+
76
+ return (
77
+ <Script
78
+ id="tiktok-pixel-init"
79
+ strategy="afterInteractive"
80
+ dangerouslySetInnerHTML={{ __html: initSnippet }}
81
+ />
82
+ )
83
+ }
@@ -0,0 +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
+ }