@neowhale/storefront 0.2.13 → 0.2.20
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-M2MR6C55.js → chunk-3Q7CPJBA.js} +68 -18
- package/dist/chunk-3Q7CPJBA.js.map +1 -0
- package/dist/chunk-7KXJLHGA.cjs +160 -0
- package/dist/chunk-7KXJLHGA.cjs.map +1 -0
- package/dist/chunk-PXS2DPVL.js +158 -0
- package/dist/chunk-PXS2DPVL.js.map +1 -0
- package/dist/{chunk-3VKRKDPL.cjs → chunk-VAA2KKCH.cjs} +68 -18
- package/dist/chunk-VAA2KKCH.cjs.map +1 -0
- package/dist/{client-Ca8Otk-R.d.cts → client-BSO263Uv.d.cts} +91 -6
- package/dist/{client-Ca8Otk-R.d.ts → client-BSO263Uv.d.ts} +91 -6
- 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-BcL95odX.d.ts} +1 -1
- package/dist/{pixel-manager-CIR16DXY.d.cts → pixel-manager-DJ9m2FaQ.d.cts} +1 -1
- package/dist/react/index.cjs +1503 -56
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +71 -9
- package/dist/react/index.d.ts +71 -9
- package/dist/react/index.js +1497 -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 chunkVAA2KKCH_cjs = require('../chunk-VAA2KKCH.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 chunkVAA2KKCH_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 chunkVAA2KKCH_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 chunkVAA2KKCH_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 chunkVAA2KKCH_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
|
}
|
|
@@ -764,12 +1861,12 @@ function useCheckout() {
|
|
|
764
1861
|
setLoading(false);
|
|
765
1862
|
}
|
|
766
1863
|
}, [ctx.client, session]);
|
|
767
|
-
const complete = react.useCallback(async (payment) => {
|
|
1864
|
+
const complete = react.useCallback(async (payment, opts) => {
|
|
768
1865
|
if (!session) throw new Error("No active checkout session");
|
|
769
1866
|
setLoading(true);
|
|
770
1867
|
setError(null);
|
|
771
1868
|
try {
|
|
772
|
-
const order = await ctx.client.completeCheckout(session.id, payment);
|
|
1869
|
+
const order = await ctx.client.completeCheckout(session.id, payment, opts);
|
|
773
1870
|
setSession(null);
|
|
774
1871
|
return order;
|
|
775
1872
|
} catch (err) {
|
|
@@ -923,7 +2020,11 @@ function useLoyalty() {
|
|
|
923
2020
|
await refresh();
|
|
924
2021
|
return result;
|
|
925
2022
|
}, [customer?.id, ctx.client, refresh]);
|
|
926
|
-
|
|
2023
|
+
const fetchProductsByCategory = react.useCallback(async (category, locationId, tier) => {
|
|
2024
|
+
const res = await ctx.client.listLoyaltyProducts({ category, location_id: locationId, tier });
|
|
2025
|
+
return res.data;
|
|
2026
|
+
}, [ctx.client]);
|
|
2027
|
+
return { account, rewards, transactions, loading, error, refresh, redeemReward, fetchProductsByCategory };
|
|
927
2028
|
}
|
|
928
2029
|
function useReviews(productId) {
|
|
929
2030
|
const ctx = react.useContext(WhaleContext);
|
|
@@ -1151,9 +2252,9 @@ function useShipping() {
|
|
|
1151
2252
|
}, [ctx.client]);
|
|
1152
2253
|
return { methods, rates, loading, error, refreshMethods, calculateRates };
|
|
1153
2254
|
}
|
|
1154
|
-
function
|
|
2255
|
+
function useDeals() {
|
|
1155
2256
|
const ctx = react.useContext(WhaleContext);
|
|
1156
|
-
if (!ctx) throw new Error("
|
|
2257
|
+
if (!ctx) throw new Error("useDeals must be used within <WhaleProvider>");
|
|
1157
2258
|
const [validation, setValidation] = react.useState(null);
|
|
1158
2259
|
const [loading, setLoading] = react.useState(false);
|
|
1159
2260
|
const [error, setError] = react.useState(null);
|
|
@@ -1161,7 +2262,7 @@ function useCoupons() {
|
|
|
1161
2262
|
setLoading(true);
|
|
1162
2263
|
setError(null);
|
|
1163
2264
|
try {
|
|
1164
|
-
const result = await ctx.client.
|
|
2265
|
+
const result = await ctx.client.validateDeal(code, cartId ? { cart_id: cartId } : void 0);
|
|
1165
2266
|
setValidation(result);
|
|
1166
2267
|
return result;
|
|
1167
2268
|
} catch (err) {
|
|
@@ -1177,7 +2278,7 @@ function useCoupons() {
|
|
|
1177
2278
|
setLoading(true);
|
|
1178
2279
|
setError(null);
|
|
1179
2280
|
try {
|
|
1180
|
-
const cart = await ctx.client.
|
|
2281
|
+
const cart = await ctx.client.applyDeal(cartId, code);
|
|
1181
2282
|
return cart;
|
|
1182
2283
|
} catch (err) {
|
|
1183
2284
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
@@ -1191,7 +2292,7 @@ function useCoupons() {
|
|
|
1191
2292
|
setLoading(true);
|
|
1192
2293
|
setError(null);
|
|
1193
2294
|
try {
|
|
1194
|
-
const cart = await ctx.client.
|
|
2295
|
+
const cart = await ctx.client.removeDeal(cartId);
|
|
1195
2296
|
setValidation(null);
|
|
1196
2297
|
return cart;
|
|
1197
2298
|
} catch (err) {
|
|
@@ -1208,16 +2309,133 @@ function useCoupons() {
|
|
|
1208
2309
|
}, []);
|
|
1209
2310
|
return { validation, loading, error, validate, apply, remove, clear };
|
|
1210
2311
|
}
|
|
2312
|
+
var useCoupons = useDeals;
|
|
2313
|
+
function useReferral() {
|
|
2314
|
+
const ctx = react.useContext(WhaleContext);
|
|
2315
|
+
if (!ctx) throw new Error("useReferral must be used within <WhaleProvider>");
|
|
2316
|
+
const customer = zustand.useStore(ctx.authStore, (s) => s.customer);
|
|
2317
|
+
const [status, setStatus] = react.useState(null);
|
|
2318
|
+
const [loading, setLoading] = react.useState(false);
|
|
2319
|
+
const [error, setError] = react.useState(null);
|
|
2320
|
+
const refresh = react.useCallback(async () => {
|
|
2321
|
+
if (!customer?.id) {
|
|
2322
|
+
setStatus(null);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
setLoading(true);
|
|
2326
|
+
setError(null);
|
|
2327
|
+
try {
|
|
2328
|
+
const result = await ctx.client.getReferralStatus(customer.id);
|
|
2329
|
+
setStatus(result);
|
|
2330
|
+
} catch (err) {
|
|
2331
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2332
|
+
} finally {
|
|
2333
|
+
setLoading(false);
|
|
2334
|
+
}
|
|
2335
|
+
}, [customer?.id, ctx.client]);
|
|
2336
|
+
react.useEffect(() => {
|
|
2337
|
+
refresh();
|
|
2338
|
+
}, [refresh]);
|
|
2339
|
+
const enroll = react.useCallback(async () => {
|
|
2340
|
+
if (!customer?.id) throw new Error("Not authenticated");
|
|
2341
|
+
setLoading(true);
|
|
2342
|
+
setError(null);
|
|
2343
|
+
try {
|
|
2344
|
+
const result = await ctx.client.enrollReferral(customer.id);
|
|
2345
|
+
await refresh();
|
|
2346
|
+
return result;
|
|
2347
|
+
} catch (err) {
|
|
2348
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
2349
|
+
setError(e);
|
|
2350
|
+
throw e;
|
|
2351
|
+
} finally {
|
|
2352
|
+
setLoading(false);
|
|
2353
|
+
}
|
|
2354
|
+
}, [customer?.id, ctx.client, refresh]);
|
|
2355
|
+
const attributeReferral = react.useCallback(
|
|
2356
|
+
async (code) => {
|
|
2357
|
+
if (!customer?.id) throw new Error("Not authenticated");
|
|
2358
|
+
setLoading(true);
|
|
2359
|
+
setError(null);
|
|
2360
|
+
try {
|
|
2361
|
+
const result = await ctx.client.attributeReferral(customer.id, code);
|
|
2362
|
+
await refresh();
|
|
2363
|
+
return result;
|
|
2364
|
+
} catch (err) {
|
|
2365
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
2366
|
+
setError(e);
|
|
2367
|
+
throw e;
|
|
2368
|
+
} finally {
|
|
2369
|
+
setLoading(false);
|
|
2370
|
+
}
|
|
2371
|
+
},
|
|
2372
|
+
[customer?.id, ctx.client, refresh]
|
|
2373
|
+
);
|
|
2374
|
+
react.useEffect(() => {
|
|
2375
|
+
if (!customer?.id || !status || status.referred_by) return;
|
|
2376
|
+
if (typeof window === "undefined") return;
|
|
2377
|
+
const params = new URLSearchParams(window.location.search);
|
|
2378
|
+
const code = params.get("code") || localStorage.getItem("whale_ref_code");
|
|
2379
|
+
if (!code) return;
|
|
2380
|
+
ctx.client.attributeReferral(customer.id, code).then(() => {
|
|
2381
|
+
localStorage.removeItem("whale_ref_code");
|
|
2382
|
+
refresh();
|
|
2383
|
+
}).catch(() => {
|
|
2384
|
+
});
|
|
2385
|
+
}, [customer?.id, status, ctx.client, refresh]);
|
|
2386
|
+
const share = react.useCallback(async () => {
|
|
2387
|
+
if (!status?.share_url) throw new Error("Not enrolled in referral program");
|
|
2388
|
+
const shareData = {
|
|
2389
|
+
title: "Check this out!",
|
|
2390
|
+
text: `Use my referral code ${status.referral_code} for rewards!`,
|
|
2391
|
+
url: status.share_url
|
|
2392
|
+
};
|
|
2393
|
+
if (typeof navigator !== "undefined" && navigator.share) {
|
|
2394
|
+
try {
|
|
2395
|
+
await navigator.share(shareData);
|
|
2396
|
+
return;
|
|
2397
|
+
} catch {
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
if (typeof navigator !== "undefined" && navigator.clipboard) {
|
|
2401
|
+
await navigator.clipboard.writeText(status.share_url);
|
|
2402
|
+
}
|
|
2403
|
+
}, [status]);
|
|
2404
|
+
return {
|
|
2405
|
+
status,
|
|
2406
|
+
loading,
|
|
2407
|
+
error,
|
|
2408
|
+
enroll,
|
|
2409
|
+
refresh,
|
|
2410
|
+
share,
|
|
2411
|
+
attributeReferral,
|
|
2412
|
+
referralCode: status?.referral_code ?? null,
|
|
2413
|
+
shareUrl: status?.share_url ?? null,
|
|
2414
|
+
isEnrolled: status?.enrolled ?? false,
|
|
2415
|
+
referredBy: status?.referred_by ?? null
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
function trackClick(tracking, label, url, position) {
|
|
2419
|
+
if (!tracking?.gatewayUrl || !tracking?.code) return;
|
|
2420
|
+
const body = JSON.stringify({ label, url, position });
|
|
2421
|
+
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
2422
|
+
navigator.sendBeacon(
|
|
2423
|
+
`${tracking.gatewayUrl}/q/${encodeURIComponent(tracking.code)}/click`,
|
|
2424
|
+
new Blob([body], { type: "application/json" })
|
|
2425
|
+
);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
1211
2428
|
function SectionRenderer({
|
|
1212
2429
|
section,
|
|
1213
2430
|
data,
|
|
1214
|
-
theme
|
|
2431
|
+
theme,
|
|
2432
|
+
tracking
|
|
1215
2433
|
}) {
|
|
1216
2434
|
const [showCOA, setShowCOA] = react.useState(false);
|
|
1217
2435
|
const el = (() => {
|
|
1218
2436
|
switch (section.type) {
|
|
1219
2437
|
case "hero":
|
|
1220
|
-
return /* @__PURE__ */ jsxRuntime.jsx(HeroSection, { section, theme });
|
|
2438
|
+
return /* @__PURE__ */ jsxRuntime.jsx(HeroSection, { section, theme, tracking });
|
|
1221
2439
|
case "text":
|
|
1222
2440
|
return /* @__PURE__ */ jsxRuntime.jsx(TextSection, { section, theme });
|
|
1223
2441
|
case "image":
|
|
@@ -1227,15 +2445,17 @@ function SectionRenderer({
|
|
|
1227
2445
|
case "gallery":
|
|
1228
2446
|
return /* @__PURE__ */ jsxRuntime.jsx(GallerySection, { section, theme });
|
|
1229
2447
|
case "cta":
|
|
1230
|
-
return /* @__PURE__ */ jsxRuntime.jsx(CTASection, { section, theme });
|
|
2448
|
+
return /* @__PURE__ */ jsxRuntime.jsx(CTASection, { section, theme, tracking });
|
|
1231
2449
|
case "stats":
|
|
1232
2450
|
return /* @__PURE__ */ jsxRuntime.jsx(StatsSection, { section, theme });
|
|
1233
2451
|
case "product_card":
|
|
1234
|
-
return /* @__PURE__ */ jsxRuntime.jsx(ProductCardSection, { section, data, theme });
|
|
2452
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ProductCardSection, { section, data, theme, tracking });
|
|
1235
2453
|
case "coa_viewer":
|
|
1236
|
-
return /* @__PURE__ */ jsxRuntime.jsx(COAViewerSection, { section, data, theme, onShowCOA: () => setShowCOA(true) });
|
|
2454
|
+
return /* @__PURE__ */ jsxRuntime.jsx(COAViewerSection, { section, data, theme, onShowCOA: () => setShowCOA(true), tracking });
|
|
1237
2455
|
case "social_links":
|
|
1238
2456
|
return /* @__PURE__ */ jsxRuntime.jsx(SocialLinksSection, { section, theme });
|
|
2457
|
+
case "lead_capture":
|
|
2458
|
+
return /* @__PURE__ */ jsxRuntime.jsx(LeadCaptureSection, { section, data, theme });
|
|
1239
2459
|
case "divider":
|
|
1240
2460
|
return /* @__PURE__ */ jsxRuntime.jsx(DividerSection, { theme });
|
|
1241
2461
|
default:
|
|
@@ -1247,7 +2467,7 @@ function SectionRenderer({
|
|
|
1247
2467
|
showCOA && data?.coa && /* @__PURE__ */ jsxRuntime.jsx(COAModal, { coa: data.coa, theme, onClose: () => setShowCOA(false) })
|
|
1248
2468
|
] });
|
|
1249
2469
|
}
|
|
1250
|
-
function HeroSection({ section, theme }) {
|
|
2470
|
+
function HeroSection({ section, theme, tracking }) {
|
|
1251
2471
|
const { title, subtitle, background_image, cta_text, cta_url } = section.content;
|
|
1252
2472
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1253
2473
|
"div",
|
|
@@ -1289,6 +2509,7 @@ function HeroSection({ section, theme }) {
|
|
|
1289
2509
|
"a",
|
|
1290
2510
|
{
|
|
1291
2511
|
href: cta_url,
|
|
2512
|
+
onClick: () => trackClick(tracking, cta_text, cta_url),
|
|
1292
2513
|
style: {
|
|
1293
2514
|
display: "inline-block",
|
|
1294
2515
|
padding: "0.875rem 2rem",
|
|
@@ -1374,7 +2595,7 @@ function GallerySection({ section, theme }) {
|
|
|
1374
2595
|
}
|
|
1375
2596
|
) }, i)) }) });
|
|
1376
2597
|
}
|
|
1377
|
-
function CTASection({ section, theme }) {
|
|
2598
|
+
function CTASection({ section, theme, tracking }) {
|
|
1378
2599
|
const { buttons } = section.content;
|
|
1379
2600
|
if (!buttons || buttons.length === 0) return null;
|
|
1380
2601
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "2rem 1.5rem", maxWidth: 480, margin: "0 auto", display: "flex", flexDirection: "column", gap: "0.75rem" }, children: buttons.map((btn, i) => {
|
|
@@ -1383,6 +2604,7 @@ function CTASection({ section, theme }) {
|
|
|
1383
2604
|
"a",
|
|
1384
2605
|
{
|
|
1385
2606
|
href: btn.url,
|
|
2607
|
+
onClick: () => trackClick(tracking, btn.text, btn.url, i),
|
|
1386
2608
|
style: {
|
|
1387
2609
|
display: "block",
|
|
1388
2610
|
width: "100%",
|
|
@@ -1454,7 +2676,7 @@ function StatsSection({ section, theme }) {
|
|
|
1454
2676
|
}, children: stat.label })
|
|
1455
2677
|
] }, i)) }) });
|
|
1456
2678
|
}
|
|
1457
|
-
function ProductCardSection({ section, data, theme }) {
|
|
2679
|
+
function ProductCardSection({ section, data, theme, tracking }) {
|
|
1458
2680
|
const product = data?.product;
|
|
1459
2681
|
const c = section.content;
|
|
1460
2682
|
const name = c.name || product?.name || "";
|
|
@@ -1470,6 +2692,7 @@ function ProductCardSection({ section, data, theme }) {
|
|
|
1470
2692
|
"a",
|
|
1471
2693
|
{
|
|
1472
2694
|
href: url,
|
|
2695
|
+
onClick: () => trackClick(tracking, "View Product", url),
|
|
1473
2696
|
style: {
|
|
1474
2697
|
display: "block",
|
|
1475
2698
|
width: "100%",
|
|
@@ -1494,7 +2717,8 @@ function COAViewerSection({
|
|
|
1494
2717
|
section,
|
|
1495
2718
|
data,
|
|
1496
2719
|
theme,
|
|
1497
|
-
onShowCOA
|
|
2720
|
+
onShowCOA,
|
|
2721
|
+
tracking
|
|
1498
2722
|
}) {
|
|
1499
2723
|
const coa = data?.coa;
|
|
1500
2724
|
const c = section.content;
|
|
@@ -1515,10 +2739,200 @@ function COAViewerSection({
|
|
|
1515
2739
|
display: "block",
|
|
1516
2740
|
boxSizing: "border-box"
|
|
1517
2741
|
};
|
|
2742
|
+
const buttonLabel = c.button_text || "View Lab Results";
|
|
1518
2743
|
if (coa.viewer_url) {
|
|
1519
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2744
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2745
|
+
"a",
|
|
2746
|
+
{
|
|
2747
|
+
href: coa.viewer_url,
|
|
2748
|
+
target: "_blank",
|
|
2749
|
+
rel: "noopener noreferrer",
|
|
2750
|
+
onClick: () => trackClick(tracking, buttonLabel, coa.viewer_url),
|
|
2751
|
+
style: buttonStyle,
|
|
2752
|
+
children: buttonLabel
|
|
2753
|
+
}
|
|
2754
|
+
) });
|
|
1520
2755
|
}
|
|
1521
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("button", { onClick:
|
|
2756
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => {
|
|
2757
|
+
trackClick(tracking, buttonLabel, coa.url);
|
|
2758
|
+
onShowCOA();
|
|
2759
|
+
}, style: buttonStyle, children: buttonLabel }) });
|
|
2760
|
+
}
|
|
2761
|
+
function LeadCaptureSection({ section, data, theme }) {
|
|
2762
|
+
const c = section.content;
|
|
2763
|
+
const [firstName, setFirstName] = react.useState("");
|
|
2764
|
+
const [email, setEmail] = react.useState("");
|
|
2765
|
+
const [status, setStatus] = react.useState("idle");
|
|
2766
|
+
const [errorMsg, setErrorMsg] = react.useState("");
|
|
2767
|
+
const gatewayUrl = c.gateway_url || data.gatewayUrl || "https://whale-gateway.fly.dev";
|
|
2768
|
+
const storeId = c.store_id || data.store?.id;
|
|
2769
|
+
const slug = c.landing_page_slug || data.landing_page?.slug;
|
|
2770
|
+
async function handleSubmit(e) {
|
|
2771
|
+
e.preventDefault();
|
|
2772
|
+
if (!email || !storeId) return;
|
|
2773
|
+
setStatus("loading");
|
|
2774
|
+
setErrorMsg("");
|
|
2775
|
+
try {
|
|
2776
|
+
const res = await fetch(`${gatewayUrl}/v1/stores/${storeId}/storefront/leads`, {
|
|
2777
|
+
method: "POST",
|
|
2778
|
+
headers: { "Content-Type": "application/json" },
|
|
2779
|
+
body: JSON.stringify({
|
|
2780
|
+
email,
|
|
2781
|
+
first_name: firstName || void 0,
|
|
2782
|
+
source: c.source || "landing_page",
|
|
2783
|
+
landing_page_slug: slug || void 0,
|
|
2784
|
+
tags: c.tags || void 0
|
|
2785
|
+
})
|
|
2786
|
+
});
|
|
2787
|
+
if (!res.ok) {
|
|
2788
|
+
const body = await res.json().catch(() => ({}));
|
|
2789
|
+
throw new Error(body?.error?.message || "Something went wrong. Please try again.");
|
|
2790
|
+
}
|
|
2791
|
+
setStatus("success");
|
|
2792
|
+
} catch (err) {
|
|
2793
|
+
setErrorMsg(err instanceof Error ? err.message : "Something went wrong. Please try again.");
|
|
2794
|
+
setStatus("error");
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
const heading = c.heading || "get 10% off your first visit.";
|
|
2798
|
+
const subtitle = c.subtitle || "drop your email and we will send you the code.";
|
|
2799
|
+
const buttonText = c.button_text || "Claim My Discount";
|
|
2800
|
+
const successHeading = c.success_heading || "You\u2019re in!";
|
|
2801
|
+
const successMessage = c.success_message || "Check your inbox for the discount code.";
|
|
2802
|
+
const inputStyle = {
|
|
2803
|
+
flex: 1,
|
|
2804
|
+
minWidth: 0,
|
|
2805
|
+
padding: "0.875rem 1rem",
|
|
2806
|
+
background: theme.surface,
|
|
2807
|
+
border: `1px solid ${theme.fg}15`,
|
|
2808
|
+
color: theme.fg,
|
|
2809
|
+
fontSize: "0.95rem",
|
|
2810
|
+
fontWeight: 300,
|
|
2811
|
+
outline: "none",
|
|
2812
|
+
boxSizing: "border-box",
|
|
2813
|
+
fontFamily: "inherit",
|
|
2814
|
+
transition: "border-color 0.2s"
|
|
2815
|
+
};
|
|
2816
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "3.5rem 1.5rem", maxWidth: 560, margin: "0 auto" }, children: [
|
|
2817
|
+
/* @__PURE__ */ jsxRuntime.jsx("style", { children: `@keyframes lc-spin { to { transform: rotate(360deg) } }` }),
|
|
2818
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: {
|
|
2819
|
+
background: theme.surface,
|
|
2820
|
+
border: `1px solid ${theme.fg}12`,
|
|
2821
|
+
padding: "clamp(2rem, 6vw, 3rem)"
|
|
2822
|
+
}, children: status === "success" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { textAlign: "center" }, children: [
|
|
2823
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { style: {
|
|
2824
|
+
fontSize: "clamp(1.5rem, 5vw, 2rem)",
|
|
2825
|
+
fontWeight: 300,
|
|
2826
|
+
fontFamily: theme.fontDisplay || "inherit",
|
|
2827
|
+
margin: "0 0 0.75rem",
|
|
2828
|
+
lineHeight: 1.2,
|
|
2829
|
+
letterSpacing: "-0.02em",
|
|
2830
|
+
color: theme.fg
|
|
2831
|
+
}, children: successHeading }),
|
|
2832
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { style: {
|
|
2833
|
+
fontSize: "0.9rem",
|
|
2834
|
+
color: `${theme.fg}99`,
|
|
2835
|
+
margin: "0 0 1.5rem",
|
|
2836
|
+
lineHeight: 1.6,
|
|
2837
|
+
fontWeight: 300
|
|
2838
|
+
}, children: successMessage }),
|
|
2839
|
+
c.coupon_code && /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
|
|
2840
|
+
display: "inline-block",
|
|
2841
|
+
padding: "0.75rem 2rem",
|
|
2842
|
+
background: `${theme.fg}08`,
|
|
2843
|
+
border: `1px dashed ${theme.fg}30`,
|
|
2844
|
+
fontSize: "clamp(1.25rem, 4vw, 1.75rem)",
|
|
2845
|
+
fontWeight: 500,
|
|
2846
|
+
fontFamily: "monospace",
|
|
2847
|
+
letterSpacing: "0.12em",
|
|
2848
|
+
color: theme.accent
|
|
2849
|
+
}, children: c.coupon_code })
|
|
2850
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2851
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { textAlign: "center", marginBottom: "clamp(1.5rem, 4vw, 2rem)" }, children: [
|
|
2852
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { style: {
|
|
2853
|
+
fontSize: "clamp(1.5rem, 5vw, 2.25rem)",
|
|
2854
|
+
fontWeight: 300,
|
|
2855
|
+
fontFamily: theme.fontDisplay || "inherit",
|
|
2856
|
+
margin: "0 0 0.5rem",
|
|
2857
|
+
lineHeight: 1.15,
|
|
2858
|
+
letterSpacing: "-0.02em",
|
|
2859
|
+
color: theme.fg
|
|
2860
|
+
}, children: heading }),
|
|
2861
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { style: {
|
|
2862
|
+
fontSize: "0.85rem",
|
|
2863
|
+
color: theme.accent,
|
|
2864
|
+
margin: 0,
|
|
2865
|
+
lineHeight: 1.6,
|
|
2866
|
+
textTransform: "uppercase",
|
|
2867
|
+
letterSpacing: "0.15em"
|
|
2868
|
+
}, children: subtitle })
|
|
2869
|
+
] }),
|
|
2870
|
+
/* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, style: { display: "flex", flexDirection: "column", gap: "0.75rem" }, children: [
|
|
2871
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "0.75rem", flexWrap: "wrap" }, children: [
|
|
2872
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2873
|
+
"input",
|
|
2874
|
+
{
|
|
2875
|
+
type: "text",
|
|
2876
|
+
placeholder: "First name",
|
|
2877
|
+
value: firstName,
|
|
2878
|
+
onChange: (e) => setFirstName(e.target.value),
|
|
2879
|
+
style: inputStyle
|
|
2880
|
+
}
|
|
2881
|
+
),
|
|
2882
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2883
|
+
"input",
|
|
2884
|
+
{
|
|
2885
|
+
type: "email",
|
|
2886
|
+
placeholder: "Email address",
|
|
2887
|
+
value: email,
|
|
2888
|
+
onChange: (e) => setEmail(e.target.value),
|
|
2889
|
+
required: true,
|
|
2890
|
+
style: inputStyle
|
|
2891
|
+
}
|
|
2892
|
+
)
|
|
2893
|
+
] }),
|
|
2894
|
+
status === "error" && errorMsg && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "0.8rem", color: "#e55", margin: 0, fontWeight: 400 }, children: errorMsg }),
|
|
2895
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2896
|
+
"button",
|
|
2897
|
+
{
|
|
2898
|
+
type: "submit",
|
|
2899
|
+
disabled: status === "loading",
|
|
2900
|
+
style: {
|
|
2901
|
+
width: "100%",
|
|
2902
|
+
padding: "0.875rem",
|
|
2903
|
+
background: theme.fg,
|
|
2904
|
+
color: theme.bg,
|
|
2905
|
+
border: "none",
|
|
2906
|
+
fontSize: "0.85rem",
|
|
2907
|
+
fontWeight: 500,
|
|
2908
|
+
cursor: status === "loading" ? "wait" : "pointer",
|
|
2909
|
+
letterSpacing: "0.08em",
|
|
2910
|
+
textTransform: "uppercase",
|
|
2911
|
+
fontFamily: "inherit",
|
|
2912
|
+
display: "flex",
|
|
2913
|
+
alignItems: "center",
|
|
2914
|
+
justifyContent: "center",
|
|
2915
|
+
gap: "0.5rem",
|
|
2916
|
+
opacity: status === "loading" ? 0.7 : 1,
|
|
2917
|
+
transition: "opacity 0.2s"
|
|
2918
|
+
},
|
|
2919
|
+
children: [
|
|
2920
|
+
status === "loading" && /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
|
|
2921
|
+
display: "inline-block",
|
|
2922
|
+
width: 16,
|
|
2923
|
+
height: 16,
|
|
2924
|
+
border: `2px solid ${theme.bg}40`,
|
|
2925
|
+
borderTopColor: theme.bg,
|
|
2926
|
+
borderRadius: "50%",
|
|
2927
|
+
animation: "lc-spin 0.8s linear infinite"
|
|
2928
|
+
} }),
|
|
2929
|
+
buttonText
|
|
2930
|
+
]
|
|
2931
|
+
}
|
|
2932
|
+
)
|
|
2933
|
+
] })
|
|
2934
|
+
] }) })
|
|
2935
|
+
] });
|
|
1522
2936
|
}
|
|
1523
2937
|
function SocialLinksSection({ section, theme }) {
|
|
1524
2938
|
const { links } = section.content;
|
|
@@ -1641,15 +3055,16 @@ function QRLandingPage({
|
|
|
1641
3055
|
const logoUrl = data.qr_code.logo_url || data.store?.logo_url;
|
|
1642
3056
|
const storeName = data.store?.name;
|
|
1643
3057
|
const sorted = [...sections].sort((a, b) => a.order - b.order);
|
|
3058
|
+
const tracking = { gatewayUrl, code };
|
|
1644
3059
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minHeight: "100dvh", background: theme.bg, color: theme.fg, fontFamily }, children: [
|
|
1645
3060
|
lp?.custom_css && /* @__PURE__ */ jsxRuntime.jsx("style", { children: lp.custom_css }),
|
|
1646
3061
|
logoUrl && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: logoUrl, alt: storeName || "Store", style: { height: 40, objectFit: "contain" } }) }),
|
|
1647
3062
|
sorted.map((section) => {
|
|
1648
|
-
const defaultRenderer = () => /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data, theme }, section.id);
|
|
3063
|
+
const defaultRenderer = () => /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data, theme, tracking }, section.id);
|
|
1649
3064
|
if (renderSection) {
|
|
1650
3065
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { children: renderSection(section, defaultRenderer) }, section.id);
|
|
1651
3066
|
}
|
|
1652
|
-
return /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data, theme }, section.id);
|
|
3067
|
+
return /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data, theme, tracking }, section.id);
|
|
1653
3068
|
}),
|
|
1654
3069
|
storeName && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "2rem 1.5rem", borderTop: `1px solid ${theme.surface}`, textAlign: "center" }, children: /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { fontSize: "0.75rem", color: theme.muted, margin: 0 }, children: [
|
|
1655
3070
|
storeName,
|
|
@@ -1677,10 +3092,10 @@ function buildDefaultSections(data) {
|
|
|
1677
3092
|
let order = 0;
|
|
1678
3093
|
const productName = qr.landing_page.title || product?.name || qr.name;
|
|
1679
3094
|
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;
|
|
3095
|
+
const description = product?.description || "";
|
|
1682
3096
|
const categoryName = product?.category_name ?? null;
|
|
1683
3097
|
const strainType = toStr(cf?.strain_type);
|
|
3098
|
+
const tagline = toStr(cf?.tagline);
|
|
1684
3099
|
if (productImage) {
|
|
1685
3100
|
sections.push({
|
|
1686
3101
|
id: "auto-hero",
|
|
@@ -1689,9 +3104,7 @@ function buildDefaultSections(data) {
|
|
|
1689
3104
|
content: {
|
|
1690
3105
|
title: productName,
|
|
1691
3106
|
subtitle: [categoryName, strainType].filter(Boolean).join(" \xB7 "),
|
|
1692
|
-
background_image: productImage
|
|
1693
|
-
cta_text: qr.landing_page.cta_text || null,
|
|
1694
|
-
cta_url: ctaUrl
|
|
3107
|
+
background_image: productImage
|
|
1695
3108
|
}
|
|
1696
3109
|
});
|
|
1697
3110
|
} else {
|
|
@@ -1706,6 +3119,15 @@ function buildDefaultSections(data) {
|
|
|
1706
3119
|
config: { align: "center" }
|
|
1707
3120
|
});
|
|
1708
3121
|
}
|
|
3122
|
+
if (tagline) {
|
|
3123
|
+
sections.push({
|
|
3124
|
+
id: "auto-tagline",
|
|
3125
|
+
type: "text",
|
|
3126
|
+
order: order++,
|
|
3127
|
+
content: { body: tagline },
|
|
3128
|
+
config: { align: "center" }
|
|
3129
|
+
});
|
|
3130
|
+
}
|
|
1709
3131
|
const thca = toNum(cf?.thca_percentage);
|
|
1710
3132
|
const thc = toNum(cf?.d9_percentage);
|
|
1711
3133
|
const cbd = toNum(cf?.cbd_total);
|
|
@@ -1721,23 +3143,23 @@ function buildDefaultSections(data) {
|
|
|
1721
3143
|
content: { stats }
|
|
1722
3144
|
});
|
|
1723
3145
|
}
|
|
1724
|
-
const
|
|
3146
|
+
const profileDetails = [];
|
|
1725
3147
|
const genetics = toStr(cf?.genetics);
|
|
1726
3148
|
const terpenes = toStr(cf?.terpenes);
|
|
1727
3149
|
const effects = toStr(cf?.effects);
|
|
1728
|
-
const
|
|
1729
|
-
const
|
|
1730
|
-
if (genetics)
|
|
1731
|
-
if (terpenes)
|
|
1732
|
-
if (effects)
|
|
1733
|
-
if (
|
|
1734
|
-
if (
|
|
1735
|
-
if (
|
|
3150
|
+
const flavorProfile = toStr(cf?.flavor_profile);
|
|
3151
|
+
const bestFor = toStr(cf?.best_for);
|
|
3152
|
+
if (genetics) profileDetails.push({ label: "Genetics", value: genetics });
|
|
3153
|
+
if (terpenes) profileDetails.push({ label: "Terpenes", value: terpenes });
|
|
3154
|
+
if (effects) profileDetails.push({ label: "Effects", value: effects });
|
|
3155
|
+
if (flavorProfile) profileDetails.push({ label: "Flavor", value: flavorProfile });
|
|
3156
|
+
if (bestFor) profileDetails.push({ label: "Best For", value: bestFor });
|
|
3157
|
+
if (profileDetails.length > 0) {
|
|
1736
3158
|
sections.push({
|
|
1737
|
-
id: "auto-
|
|
3159
|
+
id: "auto-profile",
|
|
1738
3160
|
type: "stats",
|
|
1739
3161
|
order: order++,
|
|
1740
|
-
content: { stats:
|
|
3162
|
+
content: { stats: profileDetails },
|
|
1741
3163
|
config: { layout: "list" }
|
|
1742
3164
|
});
|
|
1743
3165
|
}
|
|
@@ -1757,16 +3179,34 @@ function buildDefaultSections(data) {
|
|
|
1757
3179
|
content: { button_text: "View Lab Results" }
|
|
1758
3180
|
});
|
|
1759
3181
|
}
|
|
1760
|
-
|
|
3182
|
+
const labDetails = [];
|
|
3183
|
+
const batchNumber = toStr(cf?.batch_number);
|
|
3184
|
+
const dateTested = toStr(cf?.date_tested);
|
|
3185
|
+
if (batchNumber) labDetails.push({ label: "Batch", value: batchNumber });
|
|
3186
|
+
if (dateTested) labDetails.push({ label: "Tested", value: formatDate(dateTested) });
|
|
3187
|
+
if (labDetails.length > 0) {
|
|
1761
3188
|
sections.push({
|
|
1762
|
-
id: "auto-
|
|
1763
|
-
type: "
|
|
3189
|
+
id: "auto-lab-info",
|
|
3190
|
+
type: "stats",
|
|
1764
3191
|
order: order++,
|
|
1765
|
-
content: {
|
|
1766
|
-
|
|
1767
|
-
}
|
|
3192
|
+
content: { stats: labDetails },
|
|
3193
|
+
config: { layout: "list" }
|
|
1768
3194
|
});
|
|
1769
3195
|
}
|
|
3196
|
+
const productSlug = product?.slug;
|
|
3197
|
+
if (productSlug) {
|
|
3198
|
+
const storeDomain = data.store?.name === "Flora Distro" ? "floradistro.com" : null;
|
|
3199
|
+
if (storeDomain) {
|
|
3200
|
+
sections.push({
|
|
3201
|
+
id: "auto-shop",
|
|
3202
|
+
type: "cta",
|
|
3203
|
+
order: order++,
|
|
3204
|
+
content: {
|
|
3205
|
+
buttons: [{ text: "Shop This Product", url: `https://${storeDomain}/shop/${productSlug}`, style: "outline" }]
|
|
3206
|
+
}
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
1770
3210
|
return sections;
|
|
1771
3211
|
}
|
|
1772
3212
|
function toNum(v) {
|
|
@@ -1874,10 +3314,11 @@ function LandingPage({
|
|
|
1874
3314
|
if (state === "expired") return /* @__PURE__ */ jsxRuntime.jsx(DefaultExpired2, {});
|
|
1875
3315
|
if (state === "error") return /* @__PURE__ */ jsxRuntime.jsx(DefaultError2, { message: errorMsg });
|
|
1876
3316
|
if (!data) return null;
|
|
1877
|
-
return /* @__PURE__ */ jsxRuntime.jsx(PageLayout, { data, renderSection });
|
|
3317
|
+
return /* @__PURE__ */ jsxRuntime.jsx(PageLayout, { data, gatewayUrl, renderSection });
|
|
1878
3318
|
}
|
|
1879
3319
|
function PageLayout({
|
|
1880
3320
|
data,
|
|
3321
|
+
gatewayUrl,
|
|
1881
3322
|
renderSection
|
|
1882
3323
|
}) {
|
|
1883
3324
|
const { landing_page: lp, store } = data;
|
|
@@ -1893,15 +3334,16 @@ function PageLayout({
|
|
|
1893
3334
|
const fontFamily = lp.font_family || theme.fontDisplay || "system-ui, -apple-system, sans-serif";
|
|
1894
3335
|
const logoUrl = store?.logo_url;
|
|
1895
3336
|
const sorted = [...lp.sections].sort((a, b) => a.order - b.order);
|
|
3337
|
+
const sectionData = { ...data, gatewayUrl, landing_page: { slug: lp.slug } };
|
|
1896
3338
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minHeight: "100dvh", background: theme.bg, color: theme.fg, fontFamily }, children: [
|
|
1897
3339
|
lp.custom_css && /* @__PURE__ */ jsxRuntime.jsx("style", { children: lp.custom_css }),
|
|
1898
3340
|
logoUrl && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: logoUrl, alt: store?.name || "Store", style: { height: 40, objectFit: "contain" } }) }),
|
|
1899
3341
|
sorted.map((section) => {
|
|
1900
|
-
const defaultRenderer = () => /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data, theme }, section.id);
|
|
3342
|
+
const defaultRenderer = () => /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data: sectionData, theme }, section.id);
|
|
1901
3343
|
if (renderSection) {
|
|
1902
3344
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { children: renderSection(section, defaultRenderer) }, section.id);
|
|
1903
3345
|
}
|
|
1904
|
-
return /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data, theme }, section.id);
|
|
3346
|
+
return /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data: sectionData, theme }, section.id);
|
|
1905
3347
|
}),
|
|
1906
3348
|
store?.name && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "2rem 1.5rem", borderTop: `1px solid ${theme.surface}`, textAlign: "center" }, children: /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { fontSize: "0.75rem", color: theme.muted, margin: 0 }, children: [
|
|
1907
3349
|
"Powered by ",
|
|
@@ -1947,11 +3389,14 @@ function DefaultError2({ message }) {
|
|
|
1947
3389
|
|
|
1948
3390
|
exports.AnalyticsTracker = AnalyticsTracker;
|
|
1949
3391
|
exports.AuthInitializer = AuthInitializer;
|
|
3392
|
+
exports.BehavioralTrackerComponent = BehavioralTrackerComponent;
|
|
1950
3393
|
exports.CartInitializer = CartInitializer;
|
|
3394
|
+
exports.FingerprintCollector = FingerprintCollector;
|
|
1951
3395
|
exports.LandingPage = LandingPage;
|
|
1952
3396
|
exports.PixelInitializer = PixelInitializer;
|
|
1953
3397
|
exports.QRLandingPage = QRLandingPage;
|
|
1954
3398
|
exports.SectionRenderer = SectionRenderer;
|
|
3399
|
+
exports.SessionRecorderComponent = SessionRecorderComponent;
|
|
1955
3400
|
exports.WhaleContext = WhaleContext;
|
|
1956
3401
|
exports.WhaleProvider = WhaleProvider;
|
|
1957
3402
|
exports.useAnalytics = useAnalytics;
|
|
@@ -1964,11 +3409,13 @@ exports.useCheckout = useCheckout;
|
|
|
1964
3409
|
exports.useCoupons = useCoupons;
|
|
1965
3410
|
exports.useCustomerAnalytics = useCustomerAnalytics;
|
|
1966
3411
|
exports.useCustomerOrders = useCustomerOrders;
|
|
3412
|
+
exports.useDeals = useDeals;
|
|
1967
3413
|
exports.useLocations = useLocations;
|
|
1968
3414
|
exports.useLoyalty = useLoyalty;
|
|
1969
3415
|
exports.useProduct = useProduct;
|
|
1970
3416
|
exports.useProducts = useProducts;
|
|
1971
3417
|
exports.useRecommendations = useRecommendations;
|
|
3418
|
+
exports.useReferral = useReferral;
|
|
1972
3419
|
exports.useReviews = useReviews;
|
|
1973
3420
|
exports.useSearch = useSearch;
|
|
1974
3421
|
exports.useShipping = useShipping;
|