@neowhale/storefront 0.2.13 → 0.2.19
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/dist/chunk-7KXJLHGA.cjs +160 -0
- package/dist/chunk-7KXJLHGA.cjs.map +1 -0
- package/dist/{chunk-3VKRKDPL.cjs → chunk-CQCCXDUS.cjs} +52 -11
- package/dist/chunk-CQCCXDUS.cjs.map +1 -0
- package/dist/chunk-PXS2DPVL.js +158 -0
- package/dist/chunk-PXS2DPVL.js.map +1 -0
- package/dist/{chunk-M2MR6C55.js → chunk-XHWAUMWS.js} +52 -11
- package/dist/chunk-XHWAUMWS.js.map +1 -0
- package/dist/{client-Ca8Otk-R.d.cts → client-D1XVKpFt.d.cts} +76 -4
- package/dist/{client-Ca8Otk-R.d.ts → client-D1XVKpFt.d.ts} +76 -4
- package/dist/index.cjs +5 -5
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/next/index.cjs +7 -6
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +5 -4
- package/dist/next/index.js.map +1 -1
- package/dist/{pixel-manager-CIZKghfx.d.ts → pixel-manager-C6PAp7vQ.d.ts} +1 -1
- package/dist/{pixel-manager-CIR16DXY.d.cts → pixel-manager-DZwpn_x2.d.cts} +1 -1
- package/dist/react/index.cjs +1270 -37
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +53 -7
- package/dist/react/index.d.ts +53 -7
- package/dist/react/index.js +1264 -36
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-3VKRKDPL.cjs.map +0 -1
- package/dist/chunk-BTGOSNMP.cjs +0 -95
- package/dist/chunk-BTGOSNMP.cjs.map +0 -1
- package/dist/chunk-M2MR6C55.js.map +0 -1
- package/dist/chunk-NLH3W6JA.js +0 -93
- package/dist/chunk-NLH3W6JA.js.map +0 -1
package/dist/react/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
3
|
+
var chunk7KXJLHGA_cjs = require('../chunk-7KXJLHGA.cjs');
|
|
4
|
+
var chunkCQCCXDUS_cjs = require('../chunk-CQCCXDUS.cjs');
|
|
5
5
|
var react = require('react');
|
|
6
6
|
var navigation = require('next/navigation');
|
|
7
7
|
var vanilla = require('zustand/vanilla');
|
|
@@ -12,7 +12,7 @@ var ui = require('@neowhale/ui');
|
|
|
12
12
|
var jsxRuntime = require('react/jsx-runtime');
|
|
13
13
|
|
|
14
14
|
var WhaleContext = react.createContext(null);
|
|
15
|
-
function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
|
|
15
|
+
function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart, onCartChange) {
|
|
16
16
|
return vanilla.createStore()(
|
|
17
17
|
middleware.persist(
|
|
18
18
|
(set, get) => ({
|
|
@@ -100,6 +100,8 @@ function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
|
|
|
100
100
|
}
|
|
101
101
|
await get().syncCart();
|
|
102
102
|
onAddToCart?.(productId, productName || "", quantity, unitPrice || 0, tier);
|
|
103
|
+
const state = get();
|
|
104
|
+
if (state.cartId) onCartChange?.(state.cartId, state.total, state.itemCount);
|
|
103
105
|
} finally {
|
|
104
106
|
set({ cartLoading: false, addItemInFlight: false });
|
|
105
107
|
}
|
|
@@ -111,6 +113,8 @@ function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
|
|
|
111
113
|
if (!cartId) return;
|
|
112
114
|
await client.updateCartItem(cartId, itemId, quantity);
|
|
113
115
|
await get().syncCart();
|
|
116
|
+
const state = get();
|
|
117
|
+
if (state.cartId) onCartChange?.(state.cartId, state.total, state.itemCount);
|
|
114
118
|
} finally {
|
|
115
119
|
set({ cartLoading: false });
|
|
116
120
|
}
|
|
@@ -126,6 +130,8 @@ function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
|
|
|
126
130
|
if (item) {
|
|
127
131
|
onRemoveFromCart?.(item.product_id, productName || item.product_name);
|
|
128
132
|
}
|
|
133
|
+
const state = get();
|
|
134
|
+
if (state.cartId) onCartChange?.(state.cartId, state.total, state.itemCount);
|
|
129
135
|
} finally {
|
|
130
136
|
set({ cartLoading: false });
|
|
131
137
|
}
|
|
@@ -239,6 +245,12 @@ function createAuthStore(client, storagePrefix) {
|
|
|
239
245
|
set({ authLoading: false });
|
|
240
246
|
}
|
|
241
247
|
},
|
|
248
|
+
updateProfile: async (data) => {
|
|
249
|
+
const customer = get().customer;
|
|
250
|
+
if (!customer?.id) throw new Error("Not authenticated");
|
|
251
|
+
const updated = await client.updateProfile(customer.id, data);
|
|
252
|
+
set({ customer: updated });
|
|
253
|
+
},
|
|
242
254
|
restoreSession: async () => {
|
|
243
255
|
const { sessionToken, sessionExpiresAt, customer } = get();
|
|
244
256
|
if (!sessionToken || !sessionExpiresAt) return;
|
|
@@ -299,6 +311,38 @@ function createAuthStore(client, storagePrefix) {
|
|
|
299
311
|
);
|
|
300
312
|
}
|
|
301
313
|
var SESSION_KEY_SUFFIX = "-analytics-session";
|
|
314
|
+
var VISITOR_KEY_SUFFIX = "-visitor-id";
|
|
315
|
+
function parseMarketingParams() {
|
|
316
|
+
if (typeof window === "undefined") return {};
|
|
317
|
+
const params = new URLSearchParams(window.location.search);
|
|
318
|
+
const result = {};
|
|
319
|
+
for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term", "gclid", "fbclid"]) {
|
|
320
|
+
const val = params.get(key);
|
|
321
|
+
if (val) result[key] = val;
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
function getVisitorId(prefix) {
|
|
326
|
+
const key = `${prefix}${VISITOR_KEY_SUFFIX}`;
|
|
327
|
+
try {
|
|
328
|
+
const existing = localStorage.getItem(key);
|
|
329
|
+
if (existing) return existing;
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
const id = `v-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
333
|
+
try {
|
|
334
|
+
localStorage.setItem(key, id);
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
337
|
+
return id;
|
|
338
|
+
}
|
|
339
|
+
function detectDevice() {
|
|
340
|
+
if (typeof navigator === "undefined") return "unknown";
|
|
341
|
+
const ua = navigator.userAgent;
|
|
342
|
+
if (/Mobi|Android/i.test(ua)) return "mobile";
|
|
343
|
+
if (/Tablet|iPad/i.test(ua)) return "tablet";
|
|
344
|
+
return "desktop";
|
|
345
|
+
}
|
|
302
346
|
function useAnalytics() {
|
|
303
347
|
const ctx = react.useContext(WhaleContext);
|
|
304
348
|
if (!ctx) throw new Error("useAnalytics must be used within <WhaleProvider>");
|
|
@@ -321,9 +365,15 @@ function useAnalytics() {
|
|
|
321
365
|
} catch {
|
|
322
366
|
}
|
|
323
367
|
try {
|
|
368
|
+
const marketing = parseMarketingParams();
|
|
369
|
+
const visitorId = getVisitorId(config.storagePrefix);
|
|
324
370
|
const session = await client.createSession({
|
|
371
|
+
visitor_id: visitorId,
|
|
325
372
|
user_agent: navigator.userAgent,
|
|
326
|
-
referrer: document.referrer || void 0
|
|
373
|
+
referrer: document.referrer || void 0,
|
|
374
|
+
page_url: window.location.href,
|
|
375
|
+
device: detectDevice(),
|
|
376
|
+
...marketing
|
|
327
377
|
});
|
|
328
378
|
if (session?.id) {
|
|
329
379
|
localStorage.setItem(sessionKey, JSON.stringify({ id: session.id, createdAt: Date.now() }));
|
|
@@ -348,10 +398,12 @@ function useAnalytics() {
|
|
|
348
398
|
pixelManager?.track(eventType, { ...data, eventID: eventId });
|
|
349
399
|
try {
|
|
350
400
|
const sessionId = await getOrCreateSession();
|
|
401
|
+
const visitorId = getVisitorId(config.storagePrefix);
|
|
351
402
|
await client.trackEvent({
|
|
352
403
|
session_id: sessionId,
|
|
353
404
|
event_type: eventType,
|
|
354
|
-
event_data: { ...data, event_id: eventId }
|
|
405
|
+
event_data: { ...data, event_id: eventId },
|
|
406
|
+
visitor_id: visitorId
|
|
355
407
|
});
|
|
356
408
|
} catch {
|
|
357
409
|
}
|
|
@@ -370,9 +422,38 @@ function useAnalytics() {
|
|
|
370
422
|
},
|
|
371
423
|
[client, getOrCreateSession, trackingEnabled]
|
|
372
424
|
);
|
|
425
|
+
const updateSessionCart = react.useCallback(
|
|
426
|
+
async (cartId, cartTotal, cartItemCount) => {
|
|
427
|
+
if (!trackingEnabled) return;
|
|
428
|
+
try {
|
|
429
|
+
const sessionId = await getOrCreateSession();
|
|
430
|
+
if (sessionId.startsWith("local-")) return;
|
|
431
|
+
await client.updateSession(sessionId, {
|
|
432
|
+
cart_id: cartId,
|
|
433
|
+
cart_total: cartTotal,
|
|
434
|
+
cart_item_count: cartItemCount,
|
|
435
|
+
status: "carting"
|
|
436
|
+
});
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
[client, getOrCreateSession, trackingEnabled]
|
|
441
|
+
);
|
|
442
|
+
const updateSessionOrder = react.useCallback(
|
|
443
|
+
async (orderId) => {
|
|
444
|
+
if (!trackingEnabled) return;
|
|
445
|
+
try {
|
|
446
|
+
const sessionId = await getOrCreateSession();
|
|
447
|
+
if (sessionId.startsWith("local-")) return;
|
|
448
|
+
await client.updateSession(sessionId, { order_id: orderId, status: "converted" });
|
|
449
|
+
} catch {
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
[client, getOrCreateSession, trackingEnabled]
|
|
453
|
+
);
|
|
373
454
|
const trackPageView = react.useCallback(
|
|
374
455
|
(url, referrer) => {
|
|
375
|
-
track("page_view", { url, referrer });
|
|
456
|
+
track("page_view", { url, referrer, page_url: url });
|
|
376
457
|
},
|
|
377
458
|
[track]
|
|
378
459
|
);
|
|
@@ -429,6 +510,8 @@ function useAnalytics() {
|
|
|
429
510
|
trackAddToCart,
|
|
430
511
|
trackRemoveFromCart,
|
|
431
512
|
linkCustomer,
|
|
513
|
+
updateSessionCart,
|
|
514
|
+
updateSessionOrder,
|
|
432
515
|
getOrCreateSession,
|
|
433
516
|
/** Whether tracking is globally enabled for this storefront */
|
|
434
517
|
trackingEnabled,
|
|
@@ -446,6 +529,7 @@ function useAuth() {
|
|
|
446
529
|
isAuthenticated: s.isSessionValid(),
|
|
447
530
|
sendCode: s.sendOTP,
|
|
448
531
|
verifyCode: s.verifyOTP,
|
|
532
|
+
updateProfile: s.updateProfile,
|
|
449
533
|
restoreSession: s.restoreSession,
|
|
450
534
|
logout: s.logout,
|
|
451
535
|
fetchCustomer: s.fetchCustomer
|
|
@@ -465,7 +549,8 @@ function AnalyticsTracker({ pathname }) {
|
|
|
465
549
|
if (pathname === prevPathname.current) return;
|
|
466
550
|
const referrer = prevPathname.current || (typeof document !== "undefined" ? document.referrer : "");
|
|
467
551
|
prevPathname.current = pathname;
|
|
468
|
-
|
|
552
|
+
const fullUrl = typeof window !== "undefined" ? window.location.href : pathname;
|
|
553
|
+
trackPageView(fullUrl, referrer || void 0);
|
|
469
554
|
}, [pathname, trackPageView, trackingEnabled]);
|
|
470
555
|
react.useEffect(() => {
|
|
471
556
|
if (!trackingEnabled) return;
|
|
@@ -550,7 +635,7 @@ function PixelInitializer({ onReady, onTheme }) {
|
|
|
550
635
|
onTheme(config.theme);
|
|
551
636
|
}
|
|
552
637
|
if (ctx.config.trackingEnabled && config.pixels && config.pixels.length > 0) {
|
|
553
|
-
const manager = new
|
|
638
|
+
const manager = new chunk7KXJLHGA_cjs.PixelManager(config.pixels);
|
|
554
639
|
await manager.initialize();
|
|
555
640
|
onReady(manager);
|
|
556
641
|
}
|
|
@@ -559,6 +644,983 @@ function PixelInitializer({ onReady, onTheme }) {
|
|
|
559
644
|
}, [ctx, onReady, onTheme]);
|
|
560
645
|
return null;
|
|
561
646
|
}
|
|
647
|
+
|
|
648
|
+
// src/behavioral/tracker.ts
|
|
649
|
+
var SCROLL_MILESTONES = [25, 50, 75, 100];
|
|
650
|
+
var TIME_MILESTONES = [30, 60, 120, 300];
|
|
651
|
+
var MOUSE_THROTTLE_MS = 200;
|
|
652
|
+
var MOUSE_BUFFER_MAX = 100;
|
|
653
|
+
var RAGE_CLICK_COUNT = 3;
|
|
654
|
+
var RAGE_CLICK_RADIUS = 50;
|
|
655
|
+
var RAGE_CLICK_WINDOW_MS = 2e3;
|
|
656
|
+
var MAX_CLICK_HISTORY = 10;
|
|
657
|
+
var BehavioralTracker = class {
|
|
658
|
+
constructor(config) {
|
|
659
|
+
this.buffer = [];
|
|
660
|
+
this.pageUrl = "";
|
|
661
|
+
this.pagePath = "";
|
|
662
|
+
this.flushTimer = null;
|
|
663
|
+
this.scrollMilestones = /* @__PURE__ */ new Set();
|
|
664
|
+
this.timeMilestones = /* @__PURE__ */ new Set();
|
|
665
|
+
this.timeTimers = [];
|
|
666
|
+
this.exitIntentFired = false;
|
|
667
|
+
this.startTime = 0;
|
|
668
|
+
this.clickHistory = [];
|
|
669
|
+
this.mouseBuffer = [];
|
|
670
|
+
this.lastMouseTime = 0;
|
|
671
|
+
this.listeners = [];
|
|
672
|
+
this.observer = null;
|
|
673
|
+
this.sentinels = [];
|
|
674
|
+
// ---------------------------------------------------------------------------
|
|
675
|
+
// Event handlers (arrow functions for stable `this`)
|
|
676
|
+
// ---------------------------------------------------------------------------
|
|
677
|
+
this.handleClick = (e) => {
|
|
678
|
+
const me = e;
|
|
679
|
+
const target = me.target;
|
|
680
|
+
if (!target) return;
|
|
681
|
+
const now2 = Date.now();
|
|
682
|
+
const x = me.clientX;
|
|
683
|
+
const y = me.clientY;
|
|
684
|
+
this.clickHistory.push({ x, y, t: now2 });
|
|
685
|
+
if (this.clickHistory.length > MAX_CLICK_HISTORY) {
|
|
686
|
+
this.clickHistory.shift();
|
|
687
|
+
}
|
|
688
|
+
const tag = target.tagName?.toLowerCase() ?? "";
|
|
689
|
+
const rawText = target.textContent ?? "";
|
|
690
|
+
const text = rawText.trim().slice(0, 50);
|
|
691
|
+
this.push({
|
|
692
|
+
data_type: "click",
|
|
693
|
+
data: {
|
|
694
|
+
tag,
|
|
695
|
+
text,
|
|
696
|
+
selector: this.getSelector(target),
|
|
697
|
+
x,
|
|
698
|
+
y,
|
|
699
|
+
timestamp: now2
|
|
700
|
+
},
|
|
701
|
+
page_url: this.pageUrl,
|
|
702
|
+
page_path: this.pagePath
|
|
703
|
+
});
|
|
704
|
+
this.detectRageClick(x, y, now2);
|
|
705
|
+
};
|
|
706
|
+
this.handleMouseMove = (e) => {
|
|
707
|
+
const me = e;
|
|
708
|
+
const now2 = Date.now();
|
|
709
|
+
if (now2 - this.lastMouseTime < MOUSE_THROTTLE_MS) return;
|
|
710
|
+
this.lastMouseTime = now2;
|
|
711
|
+
this.mouseBuffer.push({ x: me.clientX, y: me.clientY, t: now2 });
|
|
712
|
+
if (this.mouseBuffer.length > MOUSE_BUFFER_MAX) {
|
|
713
|
+
this.mouseBuffer.shift();
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
this.handleMouseOut = (e) => {
|
|
717
|
+
const me = e;
|
|
718
|
+
if (this.exitIntentFired) return;
|
|
719
|
+
if (me.clientY > 0) return;
|
|
720
|
+
if (me.relatedTarget !== null) return;
|
|
721
|
+
this.exitIntentFired = true;
|
|
722
|
+
this.push({
|
|
723
|
+
data_type: "exit_intent",
|
|
724
|
+
data: {
|
|
725
|
+
time_on_page_ms: Date.now() - this.startTime,
|
|
726
|
+
timestamp: Date.now()
|
|
727
|
+
},
|
|
728
|
+
page_url: this.pageUrl,
|
|
729
|
+
page_path: this.pagePath
|
|
730
|
+
});
|
|
731
|
+
};
|
|
732
|
+
this.handleCopy = () => {
|
|
733
|
+
const selection = window.getSelection();
|
|
734
|
+
const length = selection?.toString().length ?? 0;
|
|
735
|
+
this.push({
|
|
736
|
+
data_type: "copy",
|
|
737
|
+
data: {
|
|
738
|
+
text_length: length,
|
|
739
|
+
timestamp: Date.now()
|
|
740
|
+
},
|
|
741
|
+
page_url: this.pageUrl,
|
|
742
|
+
page_path: this.pagePath
|
|
743
|
+
});
|
|
744
|
+
};
|
|
745
|
+
this.handleVisibilityChange = () => {
|
|
746
|
+
if (document.visibilityState !== "hidden") return;
|
|
747
|
+
const timeSpent = Date.now() - this.startTime;
|
|
748
|
+
this.push({
|
|
749
|
+
data_type: "page_exit",
|
|
750
|
+
data: {
|
|
751
|
+
time_spent_ms: timeSpent,
|
|
752
|
+
timestamp: Date.now()
|
|
753
|
+
},
|
|
754
|
+
page_url: this.pageUrl,
|
|
755
|
+
page_path: this.pagePath
|
|
756
|
+
});
|
|
757
|
+
this.flushMouseBuffer();
|
|
758
|
+
this.flush();
|
|
759
|
+
};
|
|
760
|
+
this.config = {
|
|
761
|
+
sendBatch: config.sendBatch,
|
|
762
|
+
sessionId: config.sessionId,
|
|
763
|
+
visitorId: config.visitorId,
|
|
764
|
+
flushIntervalMs: config.flushIntervalMs ?? 1e4,
|
|
765
|
+
maxBufferSize: config.maxBufferSize ?? 500
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
start() {
|
|
769
|
+
this.startTime = Date.now();
|
|
770
|
+
this.addListener(document, "click", this.handleClick);
|
|
771
|
+
this.addListener(document, "mousemove", this.handleMouseMove);
|
|
772
|
+
this.addListener(document, "mouseout", this.handleMouseOut);
|
|
773
|
+
this.addListener(document, "copy", this.handleCopy);
|
|
774
|
+
this.addListener(document, "visibilitychange", this.handleVisibilityChange);
|
|
775
|
+
this.setupScrollTracking();
|
|
776
|
+
this.setupTimeMilestones();
|
|
777
|
+
this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
|
|
778
|
+
}
|
|
779
|
+
stop() {
|
|
780
|
+
for (const [target, event, handler] of this.listeners) {
|
|
781
|
+
target.removeEventListener(event, handler, { capture: true });
|
|
782
|
+
}
|
|
783
|
+
this.listeners = [];
|
|
784
|
+
if (this.flushTimer !== null) {
|
|
785
|
+
clearInterval(this.flushTimer);
|
|
786
|
+
this.flushTimer = null;
|
|
787
|
+
}
|
|
788
|
+
this.clearTimeMilestones();
|
|
789
|
+
this.cleanupScrollTracking();
|
|
790
|
+
this.flushMouseBuffer();
|
|
791
|
+
this.flush();
|
|
792
|
+
}
|
|
793
|
+
setPageContext(url, path) {
|
|
794
|
+
this.flushMouseBuffer();
|
|
795
|
+
this.flush();
|
|
796
|
+
this.pageUrl = url;
|
|
797
|
+
this.pagePath = path;
|
|
798
|
+
this.scrollMilestones.clear();
|
|
799
|
+
this.timeMilestones.clear();
|
|
800
|
+
this.exitIntentFired = false;
|
|
801
|
+
this.startTime = Date.now();
|
|
802
|
+
this.clickHistory = [];
|
|
803
|
+
this.clearTimeMilestones();
|
|
804
|
+
this.cleanupScrollTracking();
|
|
805
|
+
this.setupTimeMilestones();
|
|
806
|
+
requestAnimationFrame(() => this.setupScrollTracking());
|
|
807
|
+
}
|
|
808
|
+
// ---------------------------------------------------------------------------
|
|
809
|
+
// Buffer management
|
|
810
|
+
// ---------------------------------------------------------------------------
|
|
811
|
+
push(event) {
|
|
812
|
+
this.buffer.push(event);
|
|
813
|
+
if (this.buffer.length >= this.config.maxBufferSize) {
|
|
814
|
+
this.flush();
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
flush() {
|
|
818
|
+
if (this.buffer.length === 0) return;
|
|
819
|
+
const batch = {
|
|
820
|
+
session_id: this.config.sessionId,
|
|
821
|
+
visitor_id: this.config.visitorId,
|
|
822
|
+
events: this.buffer
|
|
823
|
+
};
|
|
824
|
+
this.buffer = [];
|
|
825
|
+
this.config.sendBatch(batch).catch(() => {
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
addListener(target, event, handler) {
|
|
829
|
+
target.addEventListener(event, handler, { passive: true, capture: true });
|
|
830
|
+
this.listeners.push([target, event, handler]);
|
|
831
|
+
}
|
|
832
|
+
// ---------------------------------------------------------------------------
|
|
833
|
+
// Scroll tracking with IntersectionObserver
|
|
834
|
+
// ---------------------------------------------------------------------------
|
|
835
|
+
setupScrollTracking() {
|
|
836
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
837
|
+
this.observer = new IntersectionObserver(
|
|
838
|
+
(entries) => {
|
|
839
|
+
for (const entry of entries) {
|
|
840
|
+
if (!entry.isIntersecting) continue;
|
|
841
|
+
const milestone = Number(entry.target.getAttribute("data-scroll-milestone"));
|
|
842
|
+
if (isNaN(milestone) || this.scrollMilestones.has(milestone)) continue;
|
|
843
|
+
this.scrollMilestones.add(milestone);
|
|
844
|
+
this.push({
|
|
845
|
+
data_type: "scroll_depth",
|
|
846
|
+
data: {
|
|
847
|
+
depth_percent: milestone,
|
|
848
|
+
timestamp: Date.now()
|
|
849
|
+
},
|
|
850
|
+
page_url: this.pageUrl,
|
|
851
|
+
page_path: this.pagePath
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
},
|
|
855
|
+
{ threshold: 0 }
|
|
856
|
+
);
|
|
857
|
+
const docHeight = document.documentElement.scrollHeight;
|
|
858
|
+
for (const pct of SCROLL_MILESTONES) {
|
|
859
|
+
const sentinel = document.createElement("div");
|
|
860
|
+
sentinel.setAttribute("data-scroll-milestone", String(pct));
|
|
861
|
+
sentinel.style.position = "absolute";
|
|
862
|
+
sentinel.style.left = "0";
|
|
863
|
+
sentinel.style.width = "1px";
|
|
864
|
+
sentinel.style.height = "1px";
|
|
865
|
+
sentinel.style.pointerEvents = "none";
|
|
866
|
+
sentinel.style.opacity = "0";
|
|
867
|
+
sentinel.style.top = `${docHeight * pct / 100 - 1}px`;
|
|
868
|
+
document.body.appendChild(sentinel);
|
|
869
|
+
this.sentinels.push(sentinel);
|
|
870
|
+
this.observer.observe(sentinel);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
cleanupScrollTracking() {
|
|
874
|
+
if (this.observer) {
|
|
875
|
+
this.observer.disconnect();
|
|
876
|
+
this.observer = null;
|
|
877
|
+
}
|
|
878
|
+
for (const sentinel of this.sentinels) {
|
|
879
|
+
sentinel.remove();
|
|
880
|
+
}
|
|
881
|
+
this.sentinels = [];
|
|
882
|
+
}
|
|
883
|
+
// ---------------------------------------------------------------------------
|
|
884
|
+
// Time milestones
|
|
885
|
+
// ---------------------------------------------------------------------------
|
|
886
|
+
setupTimeMilestones() {
|
|
887
|
+
for (const seconds of TIME_MILESTONES) {
|
|
888
|
+
const timer = setTimeout(() => {
|
|
889
|
+
if (this.timeMilestones.has(seconds)) return;
|
|
890
|
+
this.timeMilestones.add(seconds);
|
|
891
|
+
this.push({
|
|
892
|
+
data_type: "time_on_page",
|
|
893
|
+
data: {
|
|
894
|
+
milestone_seconds: seconds,
|
|
895
|
+
timestamp: Date.now()
|
|
896
|
+
},
|
|
897
|
+
page_url: this.pageUrl,
|
|
898
|
+
page_path: this.pagePath
|
|
899
|
+
});
|
|
900
|
+
}, seconds * 1e3);
|
|
901
|
+
this.timeTimers.push(timer);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
clearTimeMilestones() {
|
|
905
|
+
for (const timer of this.timeTimers) {
|
|
906
|
+
clearTimeout(timer);
|
|
907
|
+
}
|
|
908
|
+
this.timeTimers = [];
|
|
909
|
+
}
|
|
910
|
+
// ---------------------------------------------------------------------------
|
|
911
|
+
// Rage click detection
|
|
912
|
+
// ---------------------------------------------------------------------------
|
|
913
|
+
detectRageClick(x, y, now2) {
|
|
914
|
+
const windowStart = now2 - RAGE_CLICK_WINDOW_MS;
|
|
915
|
+
const nearby = this.clickHistory.filter((c) => {
|
|
916
|
+
if (c.t < windowStart) return false;
|
|
917
|
+
const dx = c.x - x;
|
|
918
|
+
const dy = c.y - y;
|
|
919
|
+
return Math.sqrt(dx * dx + dy * dy) <= RAGE_CLICK_RADIUS;
|
|
920
|
+
});
|
|
921
|
+
if (nearby.length >= RAGE_CLICK_COUNT) {
|
|
922
|
+
this.push({
|
|
923
|
+
data_type: "rage_click",
|
|
924
|
+
data: {
|
|
925
|
+
x,
|
|
926
|
+
y,
|
|
927
|
+
click_count: nearby.length,
|
|
928
|
+
timestamp: now2
|
|
929
|
+
},
|
|
930
|
+
page_url: this.pageUrl,
|
|
931
|
+
page_path: this.pagePath
|
|
932
|
+
});
|
|
933
|
+
this.clickHistory = [];
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
// ---------------------------------------------------------------------------
|
|
937
|
+
// Mouse buffer flush
|
|
938
|
+
// ---------------------------------------------------------------------------
|
|
939
|
+
flushMouseBuffer() {
|
|
940
|
+
if (this.mouseBuffer.length === 0) return;
|
|
941
|
+
this.push({
|
|
942
|
+
data_type: "mouse_movement",
|
|
943
|
+
data: {
|
|
944
|
+
points: [...this.mouseBuffer],
|
|
945
|
+
timestamp: Date.now()
|
|
946
|
+
},
|
|
947
|
+
page_url: this.pageUrl,
|
|
948
|
+
page_path: this.pagePath
|
|
949
|
+
});
|
|
950
|
+
this.mouseBuffer = [];
|
|
951
|
+
}
|
|
952
|
+
// ---------------------------------------------------------------------------
|
|
953
|
+
// CSS selector helper
|
|
954
|
+
// ---------------------------------------------------------------------------
|
|
955
|
+
getSelector(el) {
|
|
956
|
+
const parts = [];
|
|
957
|
+
let current = el;
|
|
958
|
+
let depth = 0;
|
|
959
|
+
while (current && depth < 3) {
|
|
960
|
+
let segment = current.tagName.toLowerCase();
|
|
961
|
+
if (current.id) {
|
|
962
|
+
segment += `#${current.id}`;
|
|
963
|
+
} else if (current.classList.length > 0) {
|
|
964
|
+
segment += `.${Array.from(current.classList).join(".")}`;
|
|
965
|
+
}
|
|
966
|
+
parts.unshift(segment);
|
|
967
|
+
current = current.parentElement;
|
|
968
|
+
depth++;
|
|
969
|
+
}
|
|
970
|
+
return parts.join(" > ");
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
// src/react/components/behavioral-tracker.tsx
|
|
975
|
+
var SESSION_KEY_SUFFIX2 = "-analytics-session";
|
|
976
|
+
var VISITOR_KEY_SUFFIX2 = "-visitor-id";
|
|
977
|
+
var MAX_SESSION_WAIT_MS = 1e4;
|
|
978
|
+
var SESSION_POLL_MS = 500;
|
|
979
|
+
function BehavioralTrackerComponent({ pathname }) {
|
|
980
|
+
const ctx = react.useContext(WhaleContext);
|
|
981
|
+
const trackerRef = react.useRef(null);
|
|
982
|
+
const initRef = react.useRef(false);
|
|
983
|
+
react.useEffect(() => {
|
|
984
|
+
if (!ctx || !ctx.config.trackingEnabled) return;
|
|
985
|
+
if (typeof window === "undefined") return;
|
|
986
|
+
const { config } = ctx;
|
|
987
|
+
let cancelled = false;
|
|
988
|
+
let pollTimer = null;
|
|
989
|
+
const startTime = Date.now();
|
|
990
|
+
const readSessionId = () => {
|
|
991
|
+
const key = `${config.storagePrefix}${SESSION_KEY_SUFFIX2}`;
|
|
992
|
+
try {
|
|
993
|
+
const raw = localStorage.getItem(key);
|
|
994
|
+
if (raw) {
|
|
995
|
+
const stored = JSON.parse(raw);
|
|
996
|
+
return stored.id ?? null;
|
|
997
|
+
}
|
|
998
|
+
} catch {
|
|
999
|
+
}
|
|
1000
|
+
return null;
|
|
1001
|
+
};
|
|
1002
|
+
const readVisitorId = () => {
|
|
1003
|
+
const key = `${config.storagePrefix}${VISITOR_KEY_SUFFIX2}`;
|
|
1004
|
+
try {
|
|
1005
|
+
const existing = localStorage.getItem(key);
|
|
1006
|
+
if (existing) return existing;
|
|
1007
|
+
} catch {
|
|
1008
|
+
}
|
|
1009
|
+
const id = `v-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1010
|
+
try {
|
|
1011
|
+
localStorage.setItem(key, id);
|
|
1012
|
+
} catch {
|
|
1013
|
+
}
|
|
1014
|
+
return id;
|
|
1015
|
+
};
|
|
1016
|
+
const tryInit = () => {
|
|
1017
|
+
if (cancelled) return;
|
|
1018
|
+
const sessionId = readSessionId();
|
|
1019
|
+
if (!sessionId) {
|
|
1020
|
+
if (Date.now() - startTime < MAX_SESSION_WAIT_MS) {
|
|
1021
|
+
pollTimer = setTimeout(tryInit, SESSION_POLL_MS);
|
|
1022
|
+
}
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
initRef.current = true;
|
|
1026
|
+
const visitorId = readVisitorId();
|
|
1027
|
+
const baseUrl = config.proxyPath;
|
|
1028
|
+
const endpoint = `${baseUrl}/v1/stores/${config.storeId}/storefront/behavioral`;
|
|
1029
|
+
const sendBatch = async (batch) => {
|
|
1030
|
+
await chunkCQCCXDUS_cjs.resilientSend(endpoint, batch, {
|
|
1031
|
+
"Content-Type": "application/json",
|
|
1032
|
+
"x-api-key": config.apiKey
|
|
1033
|
+
});
|
|
1034
|
+
};
|
|
1035
|
+
const tracker = new BehavioralTracker({ sendBatch, sessionId, visitorId });
|
|
1036
|
+
tracker.start();
|
|
1037
|
+
trackerRef.current = tracker;
|
|
1038
|
+
};
|
|
1039
|
+
tryInit();
|
|
1040
|
+
return () => {
|
|
1041
|
+
cancelled = true;
|
|
1042
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
1043
|
+
if (trackerRef.current) {
|
|
1044
|
+
trackerRef.current.stop();
|
|
1045
|
+
trackerRef.current = null;
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
}, []);
|
|
1049
|
+
react.useEffect(() => {
|
|
1050
|
+
if (!trackerRef.current || !pathname) return;
|
|
1051
|
+
const url = typeof window !== "undefined" ? window.location.href : pathname;
|
|
1052
|
+
trackerRef.current.setPageContext(url, pathname);
|
|
1053
|
+
}, [pathname]);
|
|
1054
|
+
return null;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// src/fingerprint/collector.ts
|
|
1058
|
+
async function sha256(input) {
|
|
1059
|
+
const data = new TextEncoder().encode(input);
|
|
1060
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
1061
|
+
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1062
|
+
}
|
|
1063
|
+
async function getCanvasFingerprint() {
|
|
1064
|
+
try {
|
|
1065
|
+
const canvas = document.createElement("canvas");
|
|
1066
|
+
canvas.width = 256;
|
|
1067
|
+
canvas.height = 256;
|
|
1068
|
+
const ctx = canvas.getContext("2d");
|
|
1069
|
+
if (!ctx) return "";
|
|
1070
|
+
const gradient = ctx.createLinearGradient(0, 0, 256, 256);
|
|
1071
|
+
gradient.addColorStop(0, "#ff6b35");
|
|
1072
|
+
gradient.addColorStop(0.5, "#1a73e8");
|
|
1073
|
+
gradient.addColorStop(1, "#34a853");
|
|
1074
|
+
ctx.fillStyle = gradient;
|
|
1075
|
+
ctx.fillRect(0, 0, 256, 256);
|
|
1076
|
+
ctx.fillStyle = "#ffffff";
|
|
1077
|
+
ctx.font = "18px Arial";
|
|
1078
|
+
ctx.textBaseline = "top";
|
|
1079
|
+
ctx.fillText("WhaleTools", 10, 10);
|
|
1080
|
+
ctx.beginPath();
|
|
1081
|
+
ctx.arc(128, 128, 60, 0, Math.PI * 2);
|
|
1082
|
+
ctx.strokeStyle = "#fbbc04";
|
|
1083
|
+
ctx.lineWidth = 3;
|
|
1084
|
+
ctx.stroke();
|
|
1085
|
+
ctx.beginPath();
|
|
1086
|
+
ctx.moveTo(0, 0);
|
|
1087
|
+
ctx.lineTo(256, 256);
|
|
1088
|
+
ctx.strokeStyle = "#ea4335";
|
|
1089
|
+
ctx.lineWidth = 2;
|
|
1090
|
+
ctx.stroke();
|
|
1091
|
+
const dataUrl = canvas.toDataURL();
|
|
1092
|
+
return sha256(dataUrl);
|
|
1093
|
+
} catch {
|
|
1094
|
+
return "";
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
async function getWebGLFingerprint() {
|
|
1098
|
+
try {
|
|
1099
|
+
const canvas = document.createElement("canvas");
|
|
1100
|
+
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
1101
|
+
if (!gl || !(gl instanceof WebGLRenderingContext)) return "";
|
|
1102
|
+
const ext = gl.getExtension("WEBGL_debug_renderer_info");
|
|
1103
|
+
const renderer = ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : "unknown";
|
|
1104
|
+
const vendor = ext ? gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) : "unknown";
|
|
1105
|
+
const version = gl.getParameter(gl.VERSION);
|
|
1106
|
+
const combined = `${renderer}|${vendor}|${version}`;
|
|
1107
|
+
return sha256(combined);
|
|
1108
|
+
} catch {
|
|
1109
|
+
return "";
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
async function getAudioFingerprint() {
|
|
1113
|
+
try {
|
|
1114
|
+
const AudioCtx = window.OfflineAudioContext || window.webkitOfflineAudioContext;
|
|
1115
|
+
if (!AudioCtx) return "";
|
|
1116
|
+
const context = new AudioCtx(1, 44100, 44100);
|
|
1117
|
+
const oscillator = context.createOscillator();
|
|
1118
|
+
oscillator.type = "triangle";
|
|
1119
|
+
oscillator.frequency.setValueAtTime(1e4, context.currentTime);
|
|
1120
|
+
const compressor = context.createDynamicsCompressor();
|
|
1121
|
+
compressor.threshold.setValueAtTime(-50, context.currentTime);
|
|
1122
|
+
compressor.knee.setValueAtTime(40, context.currentTime);
|
|
1123
|
+
compressor.ratio.setValueAtTime(12, context.currentTime);
|
|
1124
|
+
compressor.attack.setValueAtTime(0, context.currentTime);
|
|
1125
|
+
compressor.release.setValueAtTime(0.25, context.currentTime);
|
|
1126
|
+
oscillator.connect(compressor);
|
|
1127
|
+
compressor.connect(context.destination);
|
|
1128
|
+
oscillator.start(0);
|
|
1129
|
+
const buffer = await context.startRendering();
|
|
1130
|
+
const samples = buffer.getChannelData(0).slice(0, 100);
|
|
1131
|
+
const sampleStr = Array.from(samples).map((s) => s.toString()).join(",");
|
|
1132
|
+
return sha256(sampleStr);
|
|
1133
|
+
} catch {
|
|
1134
|
+
return "";
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
async function collectFingerprint() {
|
|
1138
|
+
const [canvas_fingerprint, webgl_fingerprint, audio_fingerprint] = await Promise.all([
|
|
1139
|
+
getCanvasFingerprint(),
|
|
1140
|
+
getWebGLFingerprint(),
|
|
1141
|
+
getAudioFingerprint()
|
|
1142
|
+
]);
|
|
1143
|
+
const screen_resolution = `${window.screen.width}x${window.screen.height}`;
|
|
1144
|
+
const platform = navigator.platform || "";
|
|
1145
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "";
|
|
1146
|
+
const language = navigator.language || "";
|
|
1147
|
+
const hardware_concurrency = navigator.hardwareConcurrency || 0;
|
|
1148
|
+
const device_memory = navigator.deviceMemory ?? null;
|
|
1149
|
+
const color_depth = window.screen.colorDepth;
|
|
1150
|
+
const pixel_ratio = window.devicePixelRatio || 1;
|
|
1151
|
+
const touch_support = "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
|
1152
|
+
const cookie_enabled = navigator.cookieEnabled;
|
|
1153
|
+
const do_not_track = navigator.doNotTrack ?? null;
|
|
1154
|
+
const fingerprintSource = [
|
|
1155
|
+
canvas_fingerprint,
|
|
1156
|
+
webgl_fingerprint,
|
|
1157
|
+
audio_fingerprint,
|
|
1158
|
+
screen_resolution,
|
|
1159
|
+
platform,
|
|
1160
|
+
timezone,
|
|
1161
|
+
language,
|
|
1162
|
+
String(hardware_concurrency)
|
|
1163
|
+
].join("|");
|
|
1164
|
+
const fingerprint_id = await sha256(fingerprintSource);
|
|
1165
|
+
return {
|
|
1166
|
+
fingerprint_id,
|
|
1167
|
+
canvas_fingerprint,
|
|
1168
|
+
webgl_fingerprint,
|
|
1169
|
+
audio_fingerprint,
|
|
1170
|
+
screen_resolution,
|
|
1171
|
+
platform,
|
|
1172
|
+
timezone,
|
|
1173
|
+
language,
|
|
1174
|
+
hardware_concurrency,
|
|
1175
|
+
device_memory,
|
|
1176
|
+
color_depth,
|
|
1177
|
+
pixel_ratio,
|
|
1178
|
+
touch_support,
|
|
1179
|
+
cookie_enabled,
|
|
1180
|
+
do_not_track
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// src/react/components/fingerprint-collector.tsx
|
|
1185
|
+
var SESSION_KEY_SUFFIX3 = "-analytics-session";
|
|
1186
|
+
function FingerprintCollector() {
|
|
1187
|
+
const ctx = react.useContext(WhaleContext);
|
|
1188
|
+
const sent = react.useRef(false);
|
|
1189
|
+
react.useEffect(() => {
|
|
1190
|
+
if (!ctx || sent.current) return;
|
|
1191
|
+
if (!ctx.config.trackingEnabled) return;
|
|
1192
|
+
if (typeof window === "undefined") return;
|
|
1193
|
+
sent.current = true;
|
|
1194
|
+
const { config, client } = ctx;
|
|
1195
|
+
const prefix = config.storagePrefix;
|
|
1196
|
+
const fpKey = `${prefix}-fingerprint-sent`;
|
|
1197
|
+
const linkFingerprintToSession = (fingerprintId) => {
|
|
1198
|
+
try {
|
|
1199
|
+
const sessionRaw = localStorage.getItem(`${prefix}${SESSION_KEY_SUFFIX3}`);
|
|
1200
|
+
if (sessionRaw) {
|
|
1201
|
+
const session = JSON.parse(sessionRaw);
|
|
1202
|
+
client.updateSession(session.id, { fingerprint_id: fingerprintId }).catch(() => {
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
} catch {
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
const existing = localStorage.getItem(fpKey);
|
|
1209
|
+
if (existing) {
|
|
1210
|
+
linkFingerprintToSession(existing);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
collectFingerprint().then(async (fp) => {
|
|
1214
|
+
const baseUrl = config.proxyPath;
|
|
1215
|
+
const url = `${baseUrl}/v1/stores/${config.storeId}/storefront/fingerprints`;
|
|
1216
|
+
await chunkCQCCXDUS_cjs.resilientSend(url, fp, {
|
|
1217
|
+
"Content-Type": "application/json",
|
|
1218
|
+
"x-api-key": config.apiKey
|
|
1219
|
+
}).catch(() => {
|
|
1220
|
+
});
|
|
1221
|
+
localStorage.setItem(fpKey, fp.fingerprint_id);
|
|
1222
|
+
linkFingerprintToSession(fp.fingerprint_id);
|
|
1223
|
+
}).catch(() => {
|
|
1224
|
+
});
|
|
1225
|
+
}, [ctx]);
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// src/recording/recorder.ts
|
|
1230
|
+
function now() {
|
|
1231
|
+
return Date.now();
|
|
1232
|
+
}
|
|
1233
|
+
function throttle(fn, ms) {
|
|
1234
|
+
let last = 0;
|
|
1235
|
+
let timer = null;
|
|
1236
|
+
const throttled = (...args) => {
|
|
1237
|
+
const elapsed = now() - last;
|
|
1238
|
+
if (elapsed >= ms) {
|
|
1239
|
+
last = now();
|
|
1240
|
+
fn(...args);
|
|
1241
|
+
} else if (!timer) {
|
|
1242
|
+
timer = setTimeout(() => {
|
|
1243
|
+
last = now();
|
|
1244
|
+
timer = null;
|
|
1245
|
+
fn(...args);
|
|
1246
|
+
}, ms - elapsed);
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
return throttled;
|
|
1250
|
+
}
|
|
1251
|
+
function shortSelector(el) {
|
|
1252
|
+
if (el.id) return `#${el.id}`;
|
|
1253
|
+
let sel = el.tagName.toLowerCase();
|
|
1254
|
+
if (el.className && typeof el.className === "string") {
|
|
1255
|
+
const cls = el.className.trim().split(/\s+/).slice(0, 2).join(".");
|
|
1256
|
+
if (cls) sel += `.${cls}`;
|
|
1257
|
+
}
|
|
1258
|
+
return sel;
|
|
1259
|
+
}
|
|
1260
|
+
var SessionRecorder = class {
|
|
1261
|
+
constructor(config) {
|
|
1262
|
+
this.events = [];
|
|
1263
|
+
this.sequence = 0;
|
|
1264
|
+
this.observer = null;
|
|
1265
|
+
this.flushTimer = null;
|
|
1266
|
+
this.listeners = [];
|
|
1267
|
+
this.started = false;
|
|
1268
|
+
this.flushing = false;
|
|
1269
|
+
/** Pending mutations collected within the current animation frame. */
|
|
1270
|
+
this.pendingMutations = [];
|
|
1271
|
+
this.mutationRafId = null;
|
|
1272
|
+
this.config = {
|
|
1273
|
+
sendChunk: config.sendChunk,
|
|
1274
|
+
flushIntervalMs: config.flushIntervalMs ?? 5e3,
|
|
1275
|
+
maxChunkSize: config.maxChunkSize ?? 200
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
// -----------------------------------------------------------------------
|
|
1279
|
+
// Public API
|
|
1280
|
+
// -----------------------------------------------------------------------
|
|
1281
|
+
start() {
|
|
1282
|
+
if (this.started) return;
|
|
1283
|
+
this.started = true;
|
|
1284
|
+
this.captureFullSnapshot();
|
|
1285
|
+
this.setupMutationObserver();
|
|
1286
|
+
this.setupEventListeners();
|
|
1287
|
+
this.flushTimer = setInterval(() => {
|
|
1288
|
+
void this.flush();
|
|
1289
|
+
}, this.config.flushIntervalMs);
|
|
1290
|
+
}
|
|
1291
|
+
stop() {
|
|
1292
|
+
if (!this.started) return;
|
|
1293
|
+
this.started = false;
|
|
1294
|
+
if (this.observer) {
|
|
1295
|
+
this.observer.disconnect();
|
|
1296
|
+
this.observer = null;
|
|
1297
|
+
}
|
|
1298
|
+
if (this.mutationRafId !== null) {
|
|
1299
|
+
cancelAnimationFrame(this.mutationRafId);
|
|
1300
|
+
this.mutationRafId = null;
|
|
1301
|
+
}
|
|
1302
|
+
for (const [target, event, handler, options] of this.listeners) {
|
|
1303
|
+
target.removeEventListener(event, handler, options);
|
|
1304
|
+
}
|
|
1305
|
+
this.listeners = [];
|
|
1306
|
+
if (this.flushTimer !== null) {
|
|
1307
|
+
clearInterval(this.flushTimer);
|
|
1308
|
+
this.flushTimer = null;
|
|
1309
|
+
}
|
|
1310
|
+
void this.flush();
|
|
1311
|
+
}
|
|
1312
|
+
// -----------------------------------------------------------------------
|
|
1313
|
+
// Full Snapshot (type 0)
|
|
1314
|
+
// -----------------------------------------------------------------------
|
|
1315
|
+
captureFullSnapshot() {
|
|
1316
|
+
const tree = this.serializeNode(document.documentElement);
|
|
1317
|
+
this.push({
|
|
1318
|
+
type: 0,
|
|
1319
|
+
timestamp: now(),
|
|
1320
|
+
data: {
|
|
1321
|
+
href: location.href,
|
|
1322
|
+
width: window.innerWidth,
|
|
1323
|
+
height: window.innerHeight,
|
|
1324
|
+
tree: tree ?? {}
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
serializeNode(node) {
|
|
1329
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1330
|
+
const el = node;
|
|
1331
|
+
const tag = el.tagName.toLowerCase();
|
|
1332
|
+
if (tag === "script" || tag === "noscript") return null;
|
|
1333
|
+
const attrs = {};
|
|
1334
|
+
for (const attr of Array.from(el.attributes)) {
|
|
1335
|
+
if (attr.name === "value" && el instanceof HTMLInputElement && el.type === "password") {
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
attrs[attr.name] = attr.value;
|
|
1339
|
+
}
|
|
1340
|
+
const children = [];
|
|
1341
|
+
for (const child of Array.from(el.childNodes)) {
|
|
1342
|
+
const serialized = this.serializeNode(child);
|
|
1343
|
+
if (serialized) children.push(serialized);
|
|
1344
|
+
}
|
|
1345
|
+
return { tag, attrs, children };
|
|
1346
|
+
}
|
|
1347
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1348
|
+
const text = node.textContent || "";
|
|
1349
|
+
if (!text.trim()) return null;
|
|
1350
|
+
return { text };
|
|
1351
|
+
}
|
|
1352
|
+
return null;
|
|
1353
|
+
}
|
|
1354
|
+
// -----------------------------------------------------------------------
|
|
1355
|
+
// Mutation Observer (type 1)
|
|
1356
|
+
// -----------------------------------------------------------------------
|
|
1357
|
+
setupMutationObserver() {
|
|
1358
|
+
this.observer = new MutationObserver((mutations) => {
|
|
1359
|
+
this.pendingMutations.push(...mutations);
|
|
1360
|
+
if (this.mutationRafId === null) {
|
|
1361
|
+
this.mutationRafId = requestAnimationFrame(() => {
|
|
1362
|
+
this.processMutations(this.pendingMutations);
|
|
1363
|
+
this.pendingMutations = [];
|
|
1364
|
+
this.mutationRafId = null;
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
this.observer.observe(document.documentElement, {
|
|
1369
|
+
childList: true,
|
|
1370
|
+
attributes: true,
|
|
1371
|
+
characterData: true,
|
|
1372
|
+
subtree: true,
|
|
1373
|
+
attributeOldValue: false
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
processMutations(mutations) {
|
|
1377
|
+
const ts = now();
|
|
1378
|
+
const adds = [];
|
|
1379
|
+
const removes = [];
|
|
1380
|
+
const attrs = [];
|
|
1381
|
+
const texts = [];
|
|
1382
|
+
for (const m of mutations) {
|
|
1383
|
+
if (m.type === "childList") {
|
|
1384
|
+
for (const node of Array.from(m.addedNodes)) {
|
|
1385
|
+
if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE) {
|
|
1386
|
+
const serialized = this.serializeNode(node);
|
|
1387
|
+
if (serialized) {
|
|
1388
|
+
adds.push({
|
|
1389
|
+
parentSelector: m.target.nodeType === Node.ELEMENT_NODE ? shortSelector(m.target) : null,
|
|
1390
|
+
node: serialized
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
for (const node of Array.from(m.removedNodes)) {
|
|
1396
|
+
removes.push({
|
|
1397
|
+
parentSelector: m.target.nodeType === Node.ELEMENT_NODE ? shortSelector(m.target) : null,
|
|
1398
|
+
tag: node.nodeType === Node.ELEMENT_NODE ? node.tagName.toLowerCase() : "#text"
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
} else if (m.type === "attributes" && m.target.nodeType === Node.ELEMENT_NODE) {
|
|
1402
|
+
const el = m.target;
|
|
1403
|
+
const name = m.attributeName || "";
|
|
1404
|
+
attrs.push({
|
|
1405
|
+
selector: shortSelector(el),
|
|
1406
|
+
name,
|
|
1407
|
+
value: el.getAttribute(name)
|
|
1408
|
+
});
|
|
1409
|
+
} else if (m.type === "characterData") {
|
|
1410
|
+
texts.push({
|
|
1411
|
+
parentSelector: m.target.parentElement ? shortSelector(m.target.parentElement) : null,
|
|
1412
|
+
value: (m.target.textContent || "").slice(0, 200)
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
if (adds.length || removes.length || attrs.length || texts.length) {
|
|
1417
|
+
this.push({
|
|
1418
|
+
type: 1,
|
|
1419
|
+
timestamp: ts,
|
|
1420
|
+
data: {
|
|
1421
|
+
adds: adds.length ? adds : void 0,
|
|
1422
|
+
removes: removes.length ? removes : void 0,
|
|
1423
|
+
attrs: attrs.length ? attrs : void 0,
|
|
1424
|
+
texts: texts.length ? texts : void 0
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
// -----------------------------------------------------------------------
|
|
1430
|
+
// Event Listeners (types 2–5)
|
|
1431
|
+
// -----------------------------------------------------------------------
|
|
1432
|
+
setupEventListeners() {
|
|
1433
|
+
const onMouseMove = throttle(((e) => {
|
|
1434
|
+
this.push({
|
|
1435
|
+
type: 2,
|
|
1436
|
+
timestamp: now(),
|
|
1437
|
+
data: { source: "move", x: e.clientX, y: e.clientY }
|
|
1438
|
+
});
|
|
1439
|
+
}), 100);
|
|
1440
|
+
const onClick = ((e) => {
|
|
1441
|
+
const target = e.target;
|
|
1442
|
+
this.push({
|
|
1443
|
+
type: 2,
|
|
1444
|
+
timestamp: now(),
|
|
1445
|
+
data: {
|
|
1446
|
+
source: "click",
|
|
1447
|
+
x: e.clientX,
|
|
1448
|
+
y: e.clientY,
|
|
1449
|
+
target: target ? shortSelector(target) : null
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
});
|
|
1453
|
+
this.addListener(document, "mousemove", onMouseMove, { passive: true, capture: true });
|
|
1454
|
+
this.addListener(document, "click", onClick, { passive: true, capture: true });
|
|
1455
|
+
const onScroll = throttle(((e) => {
|
|
1456
|
+
const target = e.target;
|
|
1457
|
+
if (target === document || target === document.documentElement || target === window) {
|
|
1458
|
+
this.push({
|
|
1459
|
+
type: 3,
|
|
1460
|
+
timestamp: now(),
|
|
1461
|
+
data: { target: "window", x: window.scrollX, y: window.scrollY }
|
|
1462
|
+
});
|
|
1463
|
+
} else if (target instanceof Element) {
|
|
1464
|
+
this.push({
|
|
1465
|
+
type: 3,
|
|
1466
|
+
timestamp: now(),
|
|
1467
|
+
data: {
|
|
1468
|
+
target: shortSelector(target),
|
|
1469
|
+
x: target.scrollLeft,
|
|
1470
|
+
y: target.scrollTop
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
}), 200);
|
|
1475
|
+
this.addListener(document, "scroll", onScroll, { passive: true, capture: true });
|
|
1476
|
+
const onInput = ((e) => {
|
|
1477
|
+
const target = e.target;
|
|
1478
|
+
if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement)) {
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
const isPassword = target instanceof HTMLInputElement && target.type === "password";
|
|
1482
|
+
this.push({
|
|
1483
|
+
type: 4,
|
|
1484
|
+
timestamp: now(),
|
|
1485
|
+
data: {
|
|
1486
|
+
selector: shortSelector(target),
|
|
1487
|
+
tag: target.tagName.toLowerCase(),
|
|
1488
|
+
inputType: target instanceof HTMLInputElement ? target.type : void 0,
|
|
1489
|
+
value: isPassword ? "***" : (target.value || "").slice(0, 100)
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
});
|
|
1493
|
+
this.addListener(document, "input", onInput, { passive: true, capture: true });
|
|
1494
|
+
this.addListener(document, "change", onInput, { passive: true, capture: true });
|
|
1495
|
+
const onResize = throttle((() => {
|
|
1496
|
+
this.push({
|
|
1497
|
+
type: 5,
|
|
1498
|
+
timestamp: now(),
|
|
1499
|
+
data: { width: window.innerWidth, height: window.innerHeight }
|
|
1500
|
+
});
|
|
1501
|
+
}), 500);
|
|
1502
|
+
this.addListener(window, "resize", onResize, { passive: true });
|
|
1503
|
+
const onVisibility = (() => {
|
|
1504
|
+
if (document.visibilityState === "hidden") {
|
|
1505
|
+
void this.flush();
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
this.addListener(document, "visibilitychange", onVisibility);
|
|
1509
|
+
}
|
|
1510
|
+
// -----------------------------------------------------------------------
|
|
1511
|
+
// Event buffer & flushing
|
|
1512
|
+
// -----------------------------------------------------------------------
|
|
1513
|
+
push(event) {
|
|
1514
|
+
if (!this.started) return;
|
|
1515
|
+
this.events.push(event);
|
|
1516
|
+
if (this.events.length >= this.config.maxChunkSize) {
|
|
1517
|
+
void this.flush();
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
async flush() {
|
|
1521
|
+
if (this.events.length === 0) return;
|
|
1522
|
+
if (this.flushing) return;
|
|
1523
|
+
this.flushing = true;
|
|
1524
|
+
const chunk = this.events;
|
|
1525
|
+
this.events = [];
|
|
1526
|
+
const seq = this.sequence++;
|
|
1527
|
+
try {
|
|
1528
|
+
await this.config.sendChunk(chunk, seq);
|
|
1529
|
+
} catch {
|
|
1530
|
+
} finally {
|
|
1531
|
+
this.flushing = false;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
// -----------------------------------------------------------------------
|
|
1535
|
+
// Listener bookkeeping
|
|
1536
|
+
// -----------------------------------------------------------------------
|
|
1537
|
+
addListener(target, event, handler, options) {
|
|
1538
|
+
target.addEventListener(event, handler, options);
|
|
1539
|
+
this.listeners.push([target, event, handler, options]);
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
|
|
1543
|
+
// src/react/components/session-recorder.tsx
|
|
1544
|
+
var SESSION_KEY_SUFFIX4 = "-analytics-session";
|
|
1545
|
+
var VISITOR_KEY_SUFFIX3 = "-visitor-id";
|
|
1546
|
+
var MAX_SESSION_WAIT_MS2 = 1e4;
|
|
1547
|
+
var SESSION_POLL_MS2 = 500;
|
|
1548
|
+
function SessionRecorderComponent() {
|
|
1549
|
+
const ctx = react.useContext(WhaleContext);
|
|
1550
|
+
const recorderRef = react.useRef(null);
|
|
1551
|
+
const sampledRef = react.useRef(null);
|
|
1552
|
+
const initRef = react.useRef(false);
|
|
1553
|
+
react.useEffect(() => {
|
|
1554
|
+
if (!ctx || initRef.current) return;
|
|
1555
|
+
if (!ctx.config.trackingEnabled) return;
|
|
1556
|
+
if (typeof window === "undefined") return;
|
|
1557
|
+
if (sampledRef.current === null) {
|
|
1558
|
+
sampledRef.current = Math.random() < ctx.config.recordingRate;
|
|
1559
|
+
}
|
|
1560
|
+
if (!sampledRef.current) return;
|
|
1561
|
+
const { config } = ctx;
|
|
1562
|
+
const prefix = config.storagePrefix;
|
|
1563
|
+
let cancelled = false;
|
|
1564
|
+
let pollTimer = null;
|
|
1565
|
+
const startTime = Date.now();
|
|
1566
|
+
const tryInit = () => {
|
|
1567
|
+
if (cancelled) return;
|
|
1568
|
+
let sessionId = null;
|
|
1569
|
+
try {
|
|
1570
|
+
const sessionRaw = localStorage.getItem(`${prefix}${SESSION_KEY_SUFFIX4}`);
|
|
1571
|
+
if (sessionRaw) {
|
|
1572
|
+
const session = JSON.parse(sessionRaw);
|
|
1573
|
+
sessionId = session.id || null;
|
|
1574
|
+
}
|
|
1575
|
+
} catch {
|
|
1576
|
+
}
|
|
1577
|
+
if (!sessionId) {
|
|
1578
|
+
if (Date.now() - startTime < MAX_SESSION_WAIT_MS2) {
|
|
1579
|
+
pollTimer = setTimeout(tryInit, SESSION_POLL_MS2);
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
initRef.current = true;
|
|
1585
|
+
const visitorId = (() => {
|
|
1586
|
+
try {
|
|
1587
|
+
return localStorage.getItem(`${prefix}${VISITOR_KEY_SUFFIX3}`) || "unknown";
|
|
1588
|
+
} catch {
|
|
1589
|
+
return "unknown";
|
|
1590
|
+
}
|
|
1591
|
+
})();
|
|
1592
|
+
const baseUrl = config.proxyPath;
|
|
1593
|
+
const url = `${baseUrl}/v1/stores/${config.storeId}/storefront/recordings`;
|
|
1594
|
+
const sid = sessionId;
|
|
1595
|
+
const recorder = new SessionRecorder({
|
|
1596
|
+
sendChunk: async (events, sequence) => {
|
|
1597
|
+
await chunkCQCCXDUS_cjs.resilientSend(url, {
|
|
1598
|
+
session_id: sid,
|
|
1599
|
+
visitor_id: visitorId,
|
|
1600
|
+
events,
|
|
1601
|
+
sequence,
|
|
1602
|
+
started_at: sequence === 0 ? (/* @__PURE__ */ new Date()).toISOString() : void 0
|
|
1603
|
+
}, {
|
|
1604
|
+
"Content-Type": "application/json",
|
|
1605
|
+
"x-api-key": config.apiKey
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
recorder.start();
|
|
1610
|
+
recorderRef.current = recorder;
|
|
1611
|
+
};
|
|
1612
|
+
tryInit();
|
|
1613
|
+
return () => {
|
|
1614
|
+
cancelled = true;
|
|
1615
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
1616
|
+
if (recorderRef.current) {
|
|
1617
|
+
recorderRef.current.stop();
|
|
1618
|
+
recorderRef.current = null;
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
}, []);
|
|
1622
|
+
return null;
|
|
1623
|
+
}
|
|
562
1624
|
function envBool(name) {
|
|
563
1625
|
if (typeof process === "undefined") return void 0;
|
|
564
1626
|
const raw = process.env[name];
|
|
@@ -611,13 +1673,45 @@ function WhaleProvider({
|
|
|
611
1673
|
trackingEnabled: trackingEnabled ?? envBool("NEXT_PUBLIC_TRACKING_ENABLED") ?? true,
|
|
612
1674
|
recordingRate: recordingRate ?? envNumber("NEXT_PUBLIC_RECORDING_RATE") ?? 0.1
|
|
613
1675
|
};
|
|
614
|
-
const client = new
|
|
1676
|
+
const client = new chunkCQCCXDUS_cjs.WhaleClient({
|
|
615
1677
|
storeId,
|
|
616
1678
|
apiKey,
|
|
617
1679
|
gatewayUrl: resolvedConfig.gatewayUrl,
|
|
618
1680
|
proxyPath: resolvedConfig.proxyPath
|
|
619
1681
|
});
|
|
620
|
-
const
|
|
1682
|
+
const readSessionId = () => {
|
|
1683
|
+
try {
|
|
1684
|
+
const raw = localStorage.getItem(`${resolvedConfig.storagePrefix}-analytics-session`);
|
|
1685
|
+
if (!raw) return void 0;
|
|
1686
|
+
const stored = JSON.parse(raw);
|
|
1687
|
+
if (Date.now() - stored.createdAt < resolvedConfig.sessionTtl) return stored.id;
|
|
1688
|
+
} catch {
|
|
1689
|
+
}
|
|
1690
|
+
return void 0;
|
|
1691
|
+
};
|
|
1692
|
+
const syncCartToSession = (cartId, total, itemCount) => {
|
|
1693
|
+
const sid = readSessionId();
|
|
1694
|
+
if (sid && !sid.startsWith("local-")) {
|
|
1695
|
+
client.updateSession(sid, {
|
|
1696
|
+
cart_id: cartId,
|
|
1697
|
+
cart_total: total,
|
|
1698
|
+
cart_item_count: itemCount,
|
|
1699
|
+
status: "carting"
|
|
1700
|
+
}).catch(() => {
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
const onAddToCart = resolvedConfig.trackingEnabled ? (productId, productName, quantity, price, tier) => {
|
|
1705
|
+
const sid = readSessionId();
|
|
1706
|
+
if (sid) client.trackEvent({ session_id: sid, event_type: "add_to_cart", event_data: { product_id: productId, product_name: productName, quantity, price, tier } }).catch(() => {
|
|
1707
|
+
});
|
|
1708
|
+
} : void 0;
|
|
1709
|
+
const onRemoveFromCart = resolvedConfig.trackingEnabled ? (productId, productName) => {
|
|
1710
|
+
const sid = readSessionId();
|
|
1711
|
+
if (sid) client.trackEvent({ session_id: sid, event_type: "remove_from_cart", event_data: { product_id: productId, product_name: productName } }).catch(() => {
|
|
1712
|
+
});
|
|
1713
|
+
} : void 0;
|
|
1714
|
+
const cartStore = createCartStore(client, resolvedConfig.storagePrefix, onAddToCart, onRemoveFromCart, syncCartToSession);
|
|
621
1715
|
const authStore = createAuthStore(client, resolvedConfig.storagePrefix);
|
|
622
1716
|
return {
|
|
623
1717
|
client,
|
|
@@ -641,6 +1735,9 @@ function WhaleProvider({
|
|
|
641
1735
|
/* @__PURE__ */ jsxRuntime.jsx(CartInitializer, {}),
|
|
642
1736
|
/* @__PURE__ */ jsxRuntime.jsx(AnalyticsTracker, { pathname }),
|
|
643
1737
|
/* @__PURE__ */ jsxRuntime.jsx(PixelInitializer, { onReady: handlePixelReady, onTheme: handleTheme }),
|
|
1738
|
+
/* @__PURE__ */ jsxRuntime.jsx(BehavioralTrackerComponent, { pathname }),
|
|
1739
|
+
/* @__PURE__ */ jsxRuntime.jsx(FingerprintCollector, {}),
|
|
1740
|
+
/* @__PURE__ */ jsxRuntime.jsx(SessionRecorderComponent, {}),
|
|
644
1741
|
children
|
|
645
1742
|
] }) });
|
|
646
1743
|
}
|
|
@@ -1151,9 +2248,9 @@ function useShipping() {
|
|
|
1151
2248
|
}, [ctx.client]);
|
|
1152
2249
|
return { methods, rates, loading, error, refreshMethods, calculateRates };
|
|
1153
2250
|
}
|
|
1154
|
-
function
|
|
2251
|
+
function useDeals() {
|
|
1155
2252
|
const ctx = react.useContext(WhaleContext);
|
|
1156
|
-
if (!ctx) throw new Error("
|
|
2253
|
+
if (!ctx) throw new Error("useDeals must be used within <WhaleProvider>");
|
|
1157
2254
|
const [validation, setValidation] = react.useState(null);
|
|
1158
2255
|
const [loading, setLoading] = react.useState(false);
|
|
1159
2256
|
const [error, setError] = react.useState(null);
|
|
@@ -1161,7 +2258,7 @@ function useCoupons() {
|
|
|
1161
2258
|
setLoading(true);
|
|
1162
2259
|
setError(null);
|
|
1163
2260
|
try {
|
|
1164
|
-
const result = await ctx.client.
|
|
2261
|
+
const result = await ctx.client.validateDeal(code, cartId ? { cart_id: cartId } : void 0);
|
|
1165
2262
|
setValidation(result);
|
|
1166
2263
|
return result;
|
|
1167
2264
|
} catch (err) {
|
|
@@ -1177,7 +2274,7 @@ function useCoupons() {
|
|
|
1177
2274
|
setLoading(true);
|
|
1178
2275
|
setError(null);
|
|
1179
2276
|
try {
|
|
1180
|
-
const cart = await ctx.client.
|
|
2277
|
+
const cart = await ctx.client.applyDeal(cartId, code);
|
|
1181
2278
|
return cart;
|
|
1182
2279
|
} catch (err) {
|
|
1183
2280
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
@@ -1191,7 +2288,7 @@ function useCoupons() {
|
|
|
1191
2288
|
setLoading(true);
|
|
1192
2289
|
setError(null);
|
|
1193
2290
|
try {
|
|
1194
|
-
const cart = await ctx.client.
|
|
2291
|
+
const cart = await ctx.client.removeDeal(cartId);
|
|
1195
2292
|
setValidation(null);
|
|
1196
2293
|
return cart;
|
|
1197
2294
|
} catch (err) {
|
|
@@ -1208,6 +2305,112 @@ function useCoupons() {
|
|
|
1208
2305
|
}, []);
|
|
1209
2306
|
return { validation, loading, error, validate, apply, remove, clear };
|
|
1210
2307
|
}
|
|
2308
|
+
var useCoupons = useDeals;
|
|
2309
|
+
function useReferral() {
|
|
2310
|
+
const ctx = react.useContext(WhaleContext);
|
|
2311
|
+
if (!ctx) throw new Error("useReferral must be used within <WhaleProvider>");
|
|
2312
|
+
const customer = zustand.useStore(ctx.authStore, (s) => s.customer);
|
|
2313
|
+
const [status, setStatus] = react.useState(null);
|
|
2314
|
+
const [loading, setLoading] = react.useState(false);
|
|
2315
|
+
const [error, setError] = react.useState(null);
|
|
2316
|
+
const refresh = react.useCallback(async () => {
|
|
2317
|
+
if (!customer?.id) {
|
|
2318
|
+
setStatus(null);
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
setLoading(true);
|
|
2322
|
+
setError(null);
|
|
2323
|
+
try {
|
|
2324
|
+
const result = await ctx.client.getReferralStatus(customer.id);
|
|
2325
|
+
setStatus(result);
|
|
2326
|
+
} catch (err) {
|
|
2327
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2328
|
+
} finally {
|
|
2329
|
+
setLoading(false);
|
|
2330
|
+
}
|
|
2331
|
+
}, [customer?.id, ctx.client]);
|
|
2332
|
+
react.useEffect(() => {
|
|
2333
|
+
refresh();
|
|
2334
|
+
}, [refresh]);
|
|
2335
|
+
const enroll = react.useCallback(async () => {
|
|
2336
|
+
if (!customer?.id) throw new Error("Not authenticated");
|
|
2337
|
+
setLoading(true);
|
|
2338
|
+
setError(null);
|
|
2339
|
+
try {
|
|
2340
|
+
const result = await ctx.client.enrollReferral(customer.id);
|
|
2341
|
+
await refresh();
|
|
2342
|
+
return result;
|
|
2343
|
+
} catch (err) {
|
|
2344
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
2345
|
+
setError(e);
|
|
2346
|
+
throw e;
|
|
2347
|
+
} finally {
|
|
2348
|
+
setLoading(false);
|
|
2349
|
+
}
|
|
2350
|
+
}, [customer?.id, ctx.client, refresh]);
|
|
2351
|
+
const attributeReferral = react.useCallback(
|
|
2352
|
+
async (code) => {
|
|
2353
|
+
if (!customer?.id) throw new Error("Not authenticated");
|
|
2354
|
+
setLoading(true);
|
|
2355
|
+
setError(null);
|
|
2356
|
+
try {
|
|
2357
|
+
const result = await ctx.client.attributeReferral(customer.id, code);
|
|
2358
|
+
await refresh();
|
|
2359
|
+
return result;
|
|
2360
|
+
} catch (err) {
|
|
2361
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
2362
|
+
setError(e);
|
|
2363
|
+
throw e;
|
|
2364
|
+
} finally {
|
|
2365
|
+
setLoading(false);
|
|
2366
|
+
}
|
|
2367
|
+
},
|
|
2368
|
+
[customer?.id, ctx.client, refresh]
|
|
2369
|
+
);
|
|
2370
|
+
react.useEffect(() => {
|
|
2371
|
+
if (!customer?.id || !status || status.referred_by) return;
|
|
2372
|
+
if (typeof window === "undefined") return;
|
|
2373
|
+
const params = new URLSearchParams(window.location.search);
|
|
2374
|
+
const code = params.get("code") || localStorage.getItem("whale_ref_code");
|
|
2375
|
+
if (!code) return;
|
|
2376
|
+
ctx.client.attributeReferral(customer.id, code).then(() => {
|
|
2377
|
+
localStorage.removeItem("whale_ref_code");
|
|
2378
|
+
refresh();
|
|
2379
|
+
}).catch(() => {
|
|
2380
|
+
});
|
|
2381
|
+
}, [customer?.id, status, ctx.client, refresh]);
|
|
2382
|
+
const share = react.useCallback(async () => {
|
|
2383
|
+
if (!status?.share_url) throw new Error("Not enrolled in referral program");
|
|
2384
|
+
const shareData = {
|
|
2385
|
+
title: "Check this out!",
|
|
2386
|
+
text: `Use my referral code ${status.referral_code} for rewards!`,
|
|
2387
|
+
url: status.share_url
|
|
2388
|
+
};
|
|
2389
|
+
if (typeof navigator !== "undefined" && navigator.share) {
|
|
2390
|
+
try {
|
|
2391
|
+
await navigator.share(shareData);
|
|
2392
|
+
return;
|
|
2393
|
+
} catch {
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
if (typeof navigator !== "undefined" && navigator.clipboard) {
|
|
2397
|
+
await navigator.clipboard.writeText(status.share_url);
|
|
2398
|
+
}
|
|
2399
|
+
}, [status]);
|
|
2400
|
+
return {
|
|
2401
|
+
status,
|
|
2402
|
+
loading,
|
|
2403
|
+
error,
|
|
2404
|
+
enroll,
|
|
2405
|
+
refresh,
|
|
2406
|
+
share,
|
|
2407
|
+
attributeReferral,
|
|
2408
|
+
referralCode: status?.referral_code ?? null,
|
|
2409
|
+
shareUrl: status?.share_url ?? null,
|
|
2410
|
+
isEnrolled: status?.enrolled ?? false,
|
|
2411
|
+
referredBy: status?.referred_by ?? null
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
1211
2414
|
function SectionRenderer({
|
|
1212
2415
|
section,
|
|
1213
2416
|
data,
|
|
@@ -1677,10 +2880,10 @@ function buildDefaultSections(data) {
|
|
|
1677
2880
|
let order = 0;
|
|
1678
2881
|
const productName = qr.landing_page.title || product?.name || qr.name;
|
|
1679
2882
|
const productImage = qr.landing_page.image_url || product?.featured_image || null;
|
|
1680
|
-
const description =
|
|
1681
|
-
const ctaUrl = qr.landing_page.cta_url || qr.destination_url;
|
|
2883
|
+
const description = product?.description || "";
|
|
1682
2884
|
const categoryName = product?.category_name ?? null;
|
|
1683
2885
|
const strainType = toStr(cf?.strain_type);
|
|
2886
|
+
const tagline = toStr(cf?.tagline);
|
|
1684
2887
|
if (productImage) {
|
|
1685
2888
|
sections.push({
|
|
1686
2889
|
id: "auto-hero",
|
|
@@ -1689,9 +2892,7 @@ function buildDefaultSections(data) {
|
|
|
1689
2892
|
content: {
|
|
1690
2893
|
title: productName,
|
|
1691
2894
|
subtitle: [categoryName, strainType].filter(Boolean).join(" \xB7 "),
|
|
1692
|
-
background_image: productImage
|
|
1693
|
-
cta_text: qr.landing_page.cta_text || null,
|
|
1694
|
-
cta_url: ctaUrl
|
|
2895
|
+
background_image: productImage
|
|
1695
2896
|
}
|
|
1696
2897
|
});
|
|
1697
2898
|
} else {
|
|
@@ -1706,6 +2907,15 @@ function buildDefaultSections(data) {
|
|
|
1706
2907
|
config: { align: "center" }
|
|
1707
2908
|
});
|
|
1708
2909
|
}
|
|
2910
|
+
if (tagline) {
|
|
2911
|
+
sections.push({
|
|
2912
|
+
id: "auto-tagline",
|
|
2913
|
+
type: "text",
|
|
2914
|
+
order: order++,
|
|
2915
|
+
content: { body: tagline },
|
|
2916
|
+
config: { align: "center" }
|
|
2917
|
+
});
|
|
2918
|
+
}
|
|
1709
2919
|
const thca = toNum(cf?.thca_percentage);
|
|
1710
2920
|
const thc = toNum(cf?.d9_percentage);
|
|
1711
2921
|
const cbd = toNum(cf?.cbd_total);
|
|
@@ -1721,23 +2931,23 @@ function buildDefaultSections(data) {
|
|
|
1721
2931
|
content: { stats }
|
|
1722
2932
|
});
|
|
1723
2933
|
}
|
|
1724
|
-
const
|
|
2934
|
+
const profileDetails = [];
|
|
1725
2935
|
const genetics = toStr(cf?.genetics);
|
|
1726
2936
|
const terpenes = toStr(cf?.terpenes);
|
|
1727
2937
|
const effects = toStr(cf?.effects);
|
|
1728
|
-
const
|
|
1729
|
-
const
|
|
1730
|
-
if (genetics)
|
|
1731
|
-
if (terpenes)
|
|
1732
|
-
if (effects)
|
|
1733
|
-
if (
|
|
1734
|
-
if (
|
|
1735
|
-
if (
|
|
2938
|
+
const flavorProfile = toStr(cf?.flavor_profile);
|
|
2939
|
+
const bestFor = toStr(cf?.best_for);
|
|
2940
|
+
if (genetics) profileDetails.push({ label: "Genetics", value: genetics });
|
|
2941
|
+
if (terpenes) profileDetails.push({ label: "Terpenes", value: terpenes });
|
|
2942
|
+
if (effects) profileDetails.push({ label: "Effects", value: effects });
|
|
2943
|
+
if (flavorProfile) profileDetails.push({ label: "Flavor", value: flavorProfile });
|
|
2944
|
+
if (bestFor) profileDetails.push({ label: "Best For", value: bestFor });
|
|
2945
|
+
if (profileDetails.length > 0) {
|
|
1736
2946
|
sections.push({
|
|
1737
|
-
id: "auto-
|
|
2947
|
+
id: "auto-profile",
|
|
1738
2948
|
type: "stats",
|
|
1739
2949
|
order: order++,
|
|
1740
|
-
content: { stats:
|
|
2950
|
+
content: { stats: profileDetails },
|
|
1741
2951
|
config: { layout: "list" }
|
|
1742
2952
|
});
|
|
1743
2953
|
}
|
|
@@ -1757,16 +2967,34 @@ function buildDefaultSections(data) {
|
|
|
1757
2967
|
content: { button_text: "View Lab Results" }
|
|
1758
2968
|
});
|
|
1759
2969
|
}
|
|
1760
|
-
|
|
2970
|
+
const labDetails = [];
|
|
2971
|
+
const batchNumber = toStr(cf?.batch_number);
|
|
2972
|
+
const dateTested = toStr(cf?.date_tested);
|
|
2973
|
+
if (batchNumber) labDetails.push({ label: "Batch", value: batchNumber });
|
|
2974
|
+
if (dateTested) labDetails.push({ label: "Tested", value: formatDate(dateTested) });
|
|
2975
|
+
if (labDetails.length > 0) {
|
|
1761
2976
|
sections.push({
|
|
1762
|
-
id: "auto-
|
|
1763
|
-
type: "
|
|
2977
|
+
id: "auto-lab-info",
|
|
2978
|
+
type: "stats",
|
|
1764
2979
|
order: order++,
|
|
1765
|
-
content: {
|
|
1766
|
-
|
|
1767
|
-
}
|
|
2980
|
+
content: { stats: labDetails },
|
|
2981
|
+
config: { layout: "list" }
|
|
1768
2982
|
});
|
|
1769
2983
|
}
|
|
2984
|
+
const productSlug = product?.slug;
|
|
2985
|
+
if (productSlug) {
|
|
2986
|
+
const storeDomain = data.store?.name === "Flora Distro" ? "floradistro.com" : null;
|
|
2987
|
+
if (storeDomain) {
|
|
2988
|
+
sections.push({
|
|
2989
|
+
id: "auto-shop",
|
|
2990
|
+
type: "cta",
|
|
2991
|
+
order: order++,
|
|
2992
|
+
content: {
|
|
2993
|
+
buttons: [{ text: "Shop This Product", url: `https://${storeDomain}/shop/${productSlug}`, style: "outline" }]
|
|
2994
|
+
}
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
1770
2998
|
return sections;
|
|
1771
2999
|
}
|
|
1772
3000
|
function toNum(v) {
|
|
@@ -1947,11 +3175,14 @@ function DefaultError2({ message }) {
|
|
|
1947
3175
|
|
|
1948
3176
|
exports.AnalyticsTracker = AnalyticsTracker;
|
|
1949
3177
|
exports.AuthInitializer = AuthInitializer;
|
|
3178
|
+
exports.BehavioralTrackerComponent = BehavioralTrackerComponent;
|
|
1950
3179
|
exports.CartInitializer = CartInitializer;
|
|
3180
|
+
exports.FingerprintCollector = FingerprintCollector;
|
|
1951
3181
|
exports.LandingPage = LandingPage;
|
|
1952
3182
|
exports.PixelInitializer = PixelInitializer;
|
|
1953
3183
|
exports.QRLandingPage = QRLandingPage;
|
|
1954
3184
|
exports.SectionRenderer = SectionRenderer;
|
|
3185
|
+
exports.SessionRecorderComponent = SessionRecorderComponent;
|
|
1955
3186
|
exports.WhaleContext = WhaleContext;
|
|
1956
3187
|
exports.WhaleProvider = WhaleProvider;
|
|
1957
3188
|
exports.useAnalytics = useAnalytics;
|
|
@@ -1964,11 +3195,13 @@ exports.useCheckout = useCheckout;
|
|
|
1964
3195
|
exports.useCoupons = useCoupons;
|
|
1965
3196
|
exports.useCustomerAnalytics = useCustomerAnalytics;
|
|
1966
3197
|
exports.useCustomerOrders = useCustomerOrders;
|
|
3198
|
+
exports.useDeals = useDeals;
|
|
1967
3199
|
exports.useLocations = useLocations;
|
|
1968
3200
|
exports.useLoyalty = useLoyalty;
|
|
1969
3201
|
exports.useProduct = useProduct;
|
|
1970
3202
|
exports.useProducts = useProducts;
|
|
1971
3203
|
exports.useRecommendations = useRecommendations;
|
|
3204
|
+
exports.useReferral = useReferral;
|
|
1972
3205
|
exports.useReviews = useReviews;
|
|
1973
3206
|
exports.useSearch = useSearch;
|
|
1974
3207
|
exports.useShipping = useShipping;
|