@neowhale/storefront 0.2.12 → 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-T1eA5Xs-.d.cts → client-D1XVKpFt.d.cts} +78 -4
- package/dist/{client-T1eA5Xs-.d.ts → client-D1XVKpFt.d.ts} +78 -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-1C6-h9Rp.d.ts → pixel-manager-C6PAp7vQ.d.ts} +1 -1
- package/dist/{pixel-manager-BHE1nZ90.d.cts → pixel-manager-DZwpn_x2.d.cts} +1 -1
- package/dist/react/index.cjs +1290 -56
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +54 -7
- package/dist/react/index.d.ts +54 -7
- package/dist/react/index.js +1284 -55
- 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,
|
|
@@ -1499,25 +2702,26 @@ function COAViewerSection({
|
|
|
1499
2702
|
const coa = data?.coa;
|
|
1500
2703
|
const c = section.content;
|
|
1501
2704
|
if (!coa) return null;
|
|
1502
|
-
|
|
1503
|
-
"
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
}
|
|
1520
|
-
|
|
2705
|
+
const buttonStyle = {
|
|
2706
|
+
width: "100%",
|
|
2707
|
+
padding: "0.875rem",
|
|
2708
|
+
background: theme.accent,
|
|
2709
|
+
color: theme.bg,
|
|
2710
|
+
border: "none",
|
|
2711
|
+
fontSize: "0.85rem",
|
|
2712
|
+
fontWeight: 500,
|
|
2713
|
+
cursor: "pointer",
|
|
2714
|
+
letterSpacing: "0.08em",
|
|
2715
|
+
textTransform: "uppercase",
|
|
2716
|
+
textAlign: "center",
|
|
2717
|
+
textDecoration: "none",
|
|
2718
|
+
display: "block",
|
|
2719
|
+
boxSizing: "border-box"
|
|
2720
|
+
};
|
|
2721
|
+
if (coa.viewer_url) {
|
|
2722
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("a", { href: coa.viewer_url, target: "_blank", rel: "noopener noreferrer", style: buttonStyle, children: c.button_text || "View Lab Results" }) });
|
|
2723
|
+
}
|
|
2724
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onShowCOA, style: buttonStyle, children: c.button_text || "View Lab Results" }) });
|
|
1521
2725
|
}
|
|
1522
2726
|
function SocialLinksSection({ section, theme }) {
|
|
1523
2727
|
const { links } = section.content;
|
|
@@ -1676,10 +2880,10 @@ function buildDefaultSections(data) {
|
|
|
1676
2880
|
let order = 0;
|
|
1677
2881
|
const productName = qr.landing_page.title || product?.name || qr.name;
|
|
1678
2882
|
const productImage = qr.landing_page.image_url || product?.featured_image || null;
|
|
1679
|
-
const description =
|
|
1680
|
-
const ctaUrl = qr.landing_page.cta_url || qr.destination_url;
|
|
2883
|
+
const description = product?.description || "";
|
|
1681
2884
|
const categoryName = product?.category_name ?? null;
|
|
1682
2885
|
const strainType = toStr(cf?.strain_type);
|
|
2886
|
+
const tagline = toStr(cf?.tagline);
|
|
1683
2887
|
if (productImage) {
|
|
1684
2888
|
sections.push({
|
|
1685
2889
|
id: "auto-hero",
|
|
@@ -1688,9 +2892,7 @@ function buildDefaultSections(data) {
|
|
|
1688
2892
|
content: {
|
|
1689
2893
|
title: productName,
|
|
1690
2894
|
subtitle: [categoryName, strainType].filter(Boolean).join(" \xB7 "),
|
|
1691
|
-
background_image: productImage
|
|
1692
|
-
cta_text: qr.landing_page.cta_text || null,
|
|
1693
|
-
cta_url: ctaUrl
|
|
2895
|
+
background_image: productImage
|
|
1694
2896
|
}
|
|
1695
2897
|
});
|
|
1696
2898
|
} else {
|
|
@@ -1705,6 +2907,15 @@ function buildDefaultSections(data) {
|
|
|
1705
2907
|
config: { align: "center" }
|
|
1706
2908
|
});
|
|
1707
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
|
+
}
|
|
1708
2919
|
const thca = toNum(cf?.thca_percentage);
|
|
1709
2920
|
const thc = toNum(cf?.d9_percentage);
|
|
1710
2921
|
const cbd = toNum(cf?.cbd_total);
|
|
@@ -1720,23 +2931,23 @@ function buildDefaultSections(data) {
|
|
|
1720
2931
|
content: { stats }
|
|
1721
2932
|
});
|
|
1722
2933
|
}
|
|
1723
|
-
const
|
|
2934
|
+
const profileDetails = [];
|
|
1724
2935
|
const genetics = toStr(cf?.genetics);
|
|
1725
2936
|
const terpenes = toStr(cf?.terpenes);
|
|
1726
2937
|
const effects = toStr(cf?.effects);
|
|
1727
|
-
const
|
|
1728
|
-
const
|
|
1729
|
-
if (genetics)
|
|
1730
|
-
if (terpenes)
|
|
1731
|
-
if (effects)
|
|
1732
|
-
if (
|
|
1733
|
-
if (
|
|
1734
|
-
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) {
|
|
1735
2946
|
sections.push({
|
|
1736
|
-
id: "auto-
|
|
2947
|
+
id: "auto-profile",
|
|
1737
2948
|
type: "stats",
|
|
1738
2949
|
order: order++,
|
|
1739
|
-
content: { stats:
|
|
2950
|
+
content: { stats: profileDetails },
|
|
1740
2951
|
config: { layout: "list" }
|
|
1741
2952
|
});
|
|
1742
2953
|
}
|
|
@@ -1756,16 +2967,34 @@ function buildDefaultSections(data) {
|
|
|
1756
2967
|
content: { button_text: "View Lab Results" }
|
|
1757
2968
|
});
|
|
1758
2969
|
}
|
|
1759
|
-
|
|
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) {
|
|
1760
2976
|
sections.push({
|
|
1761
|
-
id: "auto-
|
|
1762
|
-
type: "
|
|
2977
|
+
id: "auto-lab-info",
|
|
2978
|
+
type: "stats",
|
|
1763
2979
|
order: order++,
|
|
1764
|
-
content: {
|
|
1765
|
-
|
|
1766
|
-
}
|
|
2980
|
+
content: { stats: labDetails },
|
|
2981
|
+
config: { layout: "list" }
|
|
1767
2982
|
});
|
|
1768
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
|
+
}
|
|
1769
2998
|
return sections;
|
|
1770
2999
|
}
|
|
1771
3000
|
function toNum(v) {
|
|
@@ -1946,11 +3175,14 @@ function DefaultError2({ message }) {
|
|
|
1946
3175
|
|
|
1947
3176
|
exports.AnalyticsTracker = AnalyticsTracker;
|
|
1948
3177
|
exports.AuthInitializer = AuthInitializer;
|
|
3178
|
+
exports.BehavioralTrackerComponent = BehavioralTrackerComponent;
|
|
1949
3179
|
exports.CartInitializer = CartInitializer;
|
|
3180
|
+
exports.FingerprintCollector = FingerprintCollector;
|
|
1950
3181
|
exports.LandingPage = LandingPage;
|
|
1951
3182
|
exports.PixelInitializer = PixelInitializer;
|
|
1952
3183
|
exports.QRLandingPage = QRLandingPage;
|
|
1953
3184
|
exports.SectionRenderer = SectionRenderer;
|
|
3185
|
+
exports.SessionRecorderComponent = SessionRecorderComponent;
|
|
1954
3186
|
exports.WhaleContext = WhaleContext;
|
|
1955
3187
|
exports.WhaleProvider = WhaleProvider;
|
|
1956
3188
|
exports.useAnalytics = useAnalytics;
|
|
@@ -1963,11 +3195,13 @@ exports.useCheckout = useCheckout;
|
|
|
1963
3195
|
exports.useCoupons = useCoupons;
|
|
1964
3196
|
exports.useCustomerAnalytics = useCustomerAnalytics;
|
|
1965
3197
|
exports.useCustomerOrders = useCustomerOrders;
|
|
3198
|
+
exports.useDeals = useDeals;
|
|
1966
3199
|
exports.useLocations = useLocations;
|
|
1967
3200
|
exports.useLoyalty = useLoyalty;
|
|
1968
3201
|
exports.useProduct = useProduct;
|
|
1969
3202
|
exports.useProducts = useProducts;
|
|
1970
3203
|
exports.useRecommendations = useRecommendations;
|
|
3204
|
+
exports.useReferral = useReferral;
|
|
1971
3205
|
exports.useReviews = useReviews;
|
|
1972
3206
|
exports.useSearch = useSearch;
|
|
1973
3207
|
exports.useShipping = useShipping;
|