@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.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { PixelManager } from '../chunk-
|
|
2
|
-
import { WhaleClient } from '../chunk-
|
|
1
|
+
import { PixelManager } from '../chunk-PXS2DPVL.js';
|
|
2
|
+
import { resilientSend, WhaleClient } from '../chunk-3Q7CPJBA.js';
|
|
3
3
|
import { createContext, useContext, useRef, useCallback, useEffect, useState, useMemo } from 'react';
|
|
4
4
|
import { usePathname } from 'next/navigation';
|
|
5
5
|
import { createStore } from 'zustand/vanilla';
|
|
@@ -10,7 +10,7 @@ import { ThemeProvider } from '@neowhale/ui';
|
|
|
10
10
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
11
11
|
|
|
12
12
|
var WhaleContext = createContext(null);
|
|
13
|
-
function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
|
|
13
|
+
function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart, onCartChange) {
|
|
14
14
|
return createStore()(
|
|
15
15
|
persist(
|
|
16
16
|
(set, get) => ({
|
|
@@ -98,6 +98,8 @@ function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
|
|
|
98
98
|
}
|
|
99
99
|
await get().syncCart();
|
|
100
100
|
onAddToCart?.(productId, productName || "", quantity, unitPrice || 0, tier);
|
|
101
|
+
const state = get();
|
|
102
|
+
if (state.cartId) onCartChange?.(state.cartId, state.total, state.itemCount);
|
|
101
103
|
} finally {
|
|
102
104
|
set({ cartLoading: false, addItemInFlight: false });
|
|
103
105
|
}
|
|
@@ -109,6 +111,8 @@ function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
|
|
|
109
111
|
if (!cartId) return;
|
|
110
112
|
await client.updateCartItem(cartId, itemId, quantity);
|
|
111
113
|
await get().syncCart();
|
|
114
|
+
const state = get();
|
|
115
|
+
if (state.cartId) onCartChange?.(state.cartId, state.total, state.itemCount);
|
|
112
116
|
} finally {
|
|
113
117
|
set({ cartLoading: false });
|
|
114
118
|
}
|
|
@@ -124,6 +128,8 @@ function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
|
|
|
124
128
|
if (item) {
|
|
125
129
|
onRemoveFromCart?.(item.product_id, productName || item.product_name);
|
|
126
130
|
}
|
|
131
|
+
const state = get();
|
|
132
|
+
if (state.cartId) onCartChange?.(state.cartId, state.total, state.itemCount);
|
|
127
133
|
} finally {
|
|
128
134
|
set({ cartLoading: false });
|
|
129
135
|
}
|
|
@@ -237,6 +243,12 @@ function createAuthStore(client, storagePrefix) {
|
|
|
237
243
|
set({ authLoading: false });
|
|
238
244
|
}
|
|
239
245
|
},
|
|
246
|
+
updateProfile: async (data) => {
|
|
247
|
+
const customer = get().customer;
|
|
248
|
+
if (!customer?.id) throw new Error("Not authenticated");
|
|
249
|
+
const updated = await client.updateProfile(customer.id, data);
|
|
250
|
+
set({ customer: updated });
|
|
251
|
+
},
|
|
240
252
|
restoreSession: async () => {
|
|
241
253
|
const { sessionToken, sessionExpiresAt, customer } = get();
|
|
242
254
|
if (!sessionToken || !sessionExpiresAt) return;
|
|
@@ -297,6 +309,38 @@ function createAuthStore(client, storagePrefix) {
|
|
|
297
309
|
);
|
|
298
310
|
}
|
|
299
311
|
var SESSION_KEY_SUFFIX = "-analytics-session";
|
|
312
|
+
var VISITOR_KEY_SUFFIX = "-visitor-id";
|
|
313
|
+
function parseMarketingParams() {
|
|
314
|
+
if (typeof window === "undefined") return {};
|
|
315
|
+
const params = new URLSearchParams(window.location.search);
|
|
316
|
+
const result = {};
|
|
317
|
+
for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term", "gclid", "fbclid"]) {
|
|
318
|
+
const val = params.get(key);
|
|
319
|
+
if (val) result[key] = val;
|
|
320
|
+
}
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
function getVisitorId(prefix) {
|
|
324
|
+
const key = `${prefix}${VISITOR_KEY_SUFFIX}`;
|
|
325
|
+
try {
|
|
326
|
+
const existing = localStorage.getItem(key);
|
|
327
|
+
if (existing) return existing;
|
|
328
|
+
} catch {
|
|
329
|
+
}
|
|
330
|
+
const id = `v-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
331
|
+
try {
|
|
332
|
+
localStorage.setItem(key, id);
|
|
333
|
+
} catch {
|
|
334
|
+
}
|
|
335
|
+
return id;
|
|
336
|
+
}
|
|
337
|
+
function detectDevice() {
|
|
338
|
+
if (typeof navigator === "undefined") return "unknown";
|
|
339
|
+
const ua = navigator.userAgent;
|
|
340
|
+
if (/Mobi|Android/i.test(ua)) return "mobile";
|
|
341
|
+
if (/Tablet|iPad/i.test(ua)) return "tablet";
|
|
342
|
+
return "desktop";
|
|
343
|
+
}
|
|
300
344
|
function useAnalytics() {
|
|
301
345
|
const ctx = useContext(WhaleContext);
|
|
302
346
|
if (!ctx) throw new Error("useAnalytics must be used within <WhaleProvider>");
|
|
@@ -319,9 +363,15 @@ function useAnalytics() {
|
|
|
319
363
|
} catch {
|
|
320
364
|
}
|
|
321
365
|
try {
|
|
366
|
+
const marketing = parseMarketingParams();
|
|
367
|
+
const visitorId = getVisitorId(config.storagePrefix);
|
|
322
368
|
const session = await client.createSession({
|
|
369
|
+
visitor_id: visitorId,
|
|
323
370
|
user_agent: navigator.userAgent,
|
|
324
|
-
referrer: document.referrer || void 0
|
|
371
|
+
referrer: document.referrer || void 0,
|
|
372
|
+
page_url: window.location.href,
|
|
373
|
+
device: detectDevice(),
|
|
374
|
+
...marketing
|
|
325
375
|
});
|
|
326
376
|
if (session?.id) {
|
|
327
377
|
localStorage.setItem(sessionKey, JSON.stringify({ id: session.id, createdAt: Date.now() }));
|
|
@@ -346,10 +396,12 @@ function useAnalytics() {
|
|
|
346
396
|
pixelManager?.track(eventType, { ...data, eventID: eventId });
|
|
347
397
|
try {
|
|
348
398
|
const sessionId = await getOrCreateSession();
|
|
399
|
+
const visitorId = getVisitorId(config.storagePrefix);
|
|
349
400
|
await client.trackEvent({
|
|
350
401
|
session_id: sessionId,
|
|
351
402
|
event_type: eventType,
|
|
352
|
-
event_data: { ...data, event_id: eventId }
|
|
403
|
+
event_data: { ...data, event_id: eventId },
|
|
404
|
+
visitor_id: visitorId
|
|
353
405
|
});
|
|
354
406
|
} catch {
|
|
355
407
|
}
|
|
@@ -368,9 +420,38 @@ function useAnalytics() {
|
|
|
368
420
|
},
|
|
369
421
|
[client, getOrCreateSession, trackingEnabled]
|
|
370
422
|
);
|
|
423
|
+
const updateSessionCart = useCallback(
|
|
424
|
+
async (cartId, cartTotal, cartItemCount) => {
|
|
425
|
+
if (!trackingEnabled) return;
|
|
426
|
+
try {
|
|
427
|
+
const sessionId = await getOrCreateSession();
|
|
428
|
+
if (sessionId.startsWith("local-")) return;
|
|
429
|
+
await client.updateSession(sessionId, {
|
|
430
|
+
cart_id: cartId,
|
|
431
|
+
cart_total: cartTotal,
|
|
432
|
+
cart_item_count: cartItemCount,
|
|
433
|
+
status: "carting"
|
|
434
|
+
});
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
[client, getOrCreateSession, trackingEnabled]
|
|
439
|
+
);
|
|
440
|
+
const updateSessionOrder = useCallback(
|
|
441
|
+
async (orderId) => {
|
|
442
|
+
if (!trackingEnabled) return;
|
|
443
|
+
try {
|
|
444
|
+
const sessionId = await getOrCreateSession();
|
|
445
|
+
if (sessionId.startsWith("local-")) return;
|
|
446
|
+
await client.updateSession(sessionId, { order_id: orderId, status: "converted" });
|
|
447
|
+
} catch {
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
[client, getOrCreateSession, trackingEnabled]
|
|
451
|
+
);
|
|
371
452
|
const trackPageView = useCallback(
|
|
372
453
|
(url, referrer) => {
|
|
373
|
-
track("page_view", { url, referrer });
|
|
454
|
+
track("page_view", { url, referrer, page_url: url });
|
|
374
455
|
},
|
|
375
456
|
[track]
|
|
376
457
|
);
|
|
@@ -427,6 +508,8 @@ function useAnalytics() {
|
|
|
427
508
|
trackAddToCart,
|
|
428
509
|
trackRemoveFromCart,
|
|
429
510
|
linkCustomer,
|
|
511
|
+
updateSessionCart,
|
|
512
|
+
updateSessionOrder,
|
|
430
513
|
getOrCreateSession,
|
|
431
514
|
/** Whether tracking is globally enabled for this storefront */
|
|
432
515
|
trackingEnabled,
|
|
@@ -444,6 +527,7 @@ function useAuth() {
|
|
|
444
527
|
isAuthenticated: s.isSessionValid(),
|
|
445
528
|
sendCode: s.sendOTP,
|
|
446
529
|
verifyCode: s.verifyOTP,
|
|
530
|
+
updateProfile: s.updateProfile,
|
|
447
531
|
restoreSession: s.restoreSession,
|
|
448
532
|
logout: s.logout,
|
|
449
533
|
fetchCustomer: s.fetchCustomer
|
|
@@ -463,7 +547,8 @@ function AnalyticsTracker({ pathname }) {
|
|
|
463
547
|
if (pathname === prevPathname.current) return;
|
|
464
548
|
const referrer = prevPathname.current || (typeof document !== "undefined" ? document.referrer : "");
|
|
465
549
|
prevPathname.current = pathname;
|
|
466
|
-
|
|
550
|
+
const fullUrl = typeof window !== "undefined" ? window.location.href : pathname;
|
|
551
|
+
trackPageView(fullUrl, referrer || void 0);
|
|
467
552
|
}, [pathname, trackPageView, trackingEnabled]);
|
|
468
553
|
useEffect(() => {
|
|
469
554
|
if (!trackingEnabled) return;
|
|
@@ -557,6 +642,983 @@ function PixelInitializer({ onReady, onTheme }) {
|
|
|
557
642
|
}, [ctx, onReady, onTheme]);
|
|
558
643
|
return null;
|
|
559
644
|
}
|
|
645
|
+
|
|
646
|
+
// src/behavioral/tracker.ts
|
|
647
|
+
var SCROLL_MILESTONES = [25, 50, 75, 100];
|
|
648
|
+
var TIME_MILESTONES = [30, 60, 120, 300];
|
|
649
|
+
var MOUSE_THROTTLE_MS = 200;
|
|
650
|
+
var MOUSE_BUFFER_MAX = 100;
|
|
651
|
+
var RAGE_CLICK_COUNT = 3;
|
|
652
|
+
var RAGE_CLICK_RADIUS = 50;
|
|
653
|
+
var RAGE_CLICK_WINDOW_MS = 2e3;
|
|
654
|
+
var MAX_CLICK_HISTORY = 10;
|
|
655
|
+
var BehavioralTracker = class {
|
|
656
|
+
constructor(config) {
|
|
657
|
+
this.buffer = [];
|
|
658
|
+
this.pageUrl = "";
|
|
659
|
+
this.pagePath = "";
|
|
660
|
+
this.flushTimer = null;
|
|
661
|
+
this.scrollMilestones = /* @__PURE__ */ new Set();
|
|
662
|
+
this.timeMilestones = /* @__PURE__ */ new Set();
|
|
663
|
+
this.timeTimers = [];
|
|
664
|
+
this.exitIntentFired = false;
|
|
665
|
+
this.startTime = 0;
|
|
666
|
+
this.clickHistory = [];
|
|
667
|
+
this.mouseBuffer = [];
|
|
668
|
+
this.lastMouseTime = 0;
|
|
669
|
+
this.listeners = [];
|
|
670
|
+
this.observer = null;
|
|
671
|
+
this.sentinels = [];
|
|
672
|
+
// ---------------------------------------------------------------------------
|
|
673
|
+
// Event handlers (arrow functions for stable `this`)
|
|
674
|
+
// ---------------------------------------------------------------------------
|
|
675
|
+
this.handleClick = (e) => {
|
|
676
|
+
const me = e;
|
|
677
|
+
const target = me.target;
|
|
678
|
+
if (!target) return;
|
|
679
|
+
const now2 = Date.now();
|
|
680
|
+
const x = me.clientX;
|
|
681
|
+
const y = me.clientY;
|
|
682
|
+
this.clickHistory.push({ x, y, t: now2 });
|
|
683
|
+
if (this.clickHistory.length > MAX_CLICK_HISTORY) {
|
|
684
|
+
this.clickHistory.shift();
|
|
685
|
+
}
|
|
686
|
+
const tag = target.tagName?.toLowerCase() ?? "";
|
|
687
|
+
const rawText = target.textContent ?? "";
|
|
688
|
+
const text = rawText.trim().slice(0, 50);
|
|
689
|
+
this.push({
|
|
690
|
+
data_type: "click",
|
|
691
|
+
data: {
|
|
692
|
+
tag,
|
|
693
|
+
text,
|
|
694
|
+
selector: this.getSelector(target),
|
|
695
|
+
x,
|
|
696
|
+
y,
|
|
697
|
+
timestamp: now2
|
|
698
|
+
},
|
|
699
|
+
page_url: this.pageUrl,
|
|
700
|
+
page_path: this.pagePath
|
|
701
|
+
});
|
|
702
|
+
this.detectRageClick(x, y, now2);
|
|
703
|
+
};
|
|
704
|
+
this.handleMouseMove = (e) => {
|
|
705
|
+
const me = e;
|
|
706
|
+
const now2 = Date.now();
|
|
707
|
+
if (now2 - this.lastMouseTime < MOUSE_THROTTLE_MS) return;
|
|
708
|
+
this.lastMouseTime = now2;
|
|
709
|
+
this.mouseBuffer.push({ x: me.clientX, y: me.clientY, t: now2 });
|
|
710
|
+
if (this.mouseBuffer.length > MOUSE_BUFFER_MAX) {
|
|
711
|
+
this.mouseBuffer.shift();
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
this.handleMouseOut = (e) => {
|
|
715
|
+
const me = e;
|
|
716
|
+
if (this.exitIntentFired) return;
|
|
717
|
+
if (me.clientY > 0) return;
|
|
718
|
+
if (me.relatedTarget !== null) return;
|
|
719
|
+
this.exitIntentFired = true;
|
|
720
|
+
this.push({
|
|
721
|
+
data_type: "exit_intent",
|
|
722
|
+
data: {
|
|
723
|
+
time_on_page_ms: Date.now() - this.startTime,
|
|
724
|
+
timestamp: Date.now()
|
|
725
|
+
},
|
|
726
|
+
page_url: this.pageUrl,
|
|
727
|
+
page_path: this.pagePath
|
|
728
|
+
});
|
|
729
|
+
};
|
|
730
|
+
this.handleCopy = () => {
|
|
731
|
+
const selection = window.getSelection();
|
|
732
|
+
const length = selection?.toString().length ?? 0;
|
|
733
|
+
this.push({
|
|
734
|
+
data_type: "copy",
|
|
735
|
+
data: {
|
|
736
|
+
text_length: length,
|
|
737
|
+
timestamp: Date.now()
|
|
738
|
+
},
|
|
739
|
+
page_url: this.pageUrl,
|
|
740
|
+
page_path: this.pagePath
|
|
741
|
+
});
|
|
742
|
+
};
|
|
743
|
+
this.handleVisibilityChange = () => {
|
|
744
|
+
if (document.visibilityState !== "hidden") return;
|
|
745
|
+
const timeSpent = Date.now() - this.startTime;
|
|
746
|
+
this.push({
|
|
747
|
+
data_type: "page_exit",
|
|
748
|
+
data: {
|
|
749
|
+
time_spent_ms: timeSpent,
|
|
750
|
+
timestamp: Date.now()
|
|
751
|
+
},
|
|
752
|
+
page_url: this.pageUrl,
|
|
753
|
+
page_path: this.pagePath
|
|
754
|
+
});
|
|
755
|
+
this.flushMouseBuffer();
|
|
756
|
+
this.flush();
|
|
757
|
+
};
|
|
758
|
+
this.config = {
|
|
759
|
+
sendBatch: config.sendBatch,
|
|
760
|
+
sessionId: config.sessionId,
|
|
761
|
+
visitorId: config.visitorId,
|
|
762
|
+
flushIntervalMs: config.flushIntervalMs ?? 1e4,
|
|
763
|
+
maxBufferSize: config.maxBufferSize ?? 500
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
start() {
|
|
767
|
+
this.startTime = Date.now();
|
|
768
|
+
this.addListener(document, "click", this.handleClick);
|
|
769
|
+
this.addListener(document, "mousemove", this.handleMouseMove);
|
|
770
|
+
this.addListener(document, "mouseout", this.handleMouseOut);
|
|
771
|
+
this.addListener(document, "copy", this.handleCopy);
|
|
772
|
+
this.addListener(document, "visibilitychange", this.handleVisibilityChange);
|
|
773
|
+
this.setupScrollTracking();
|
|
774
|
+
this.setupTimeMilestones();
|
|
775
|
+
this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
|
|
776
|
+
}
|
|
777
|
+
stop() {
|
|
778
|
+
for (const [target, event, handler] of this.listeners) {
|
|
779
|
+
target.removeEventListener(event, handler, { capture: true });
|
|
780
|
+
}
|
|
781
|
+
this.listeners = [];
|
|
782
|
+
if (this.flushTimer !== null) {
|
|
783
|
+
clearInterval(this.flushTimer);
|
|
784
|
+
this.flushTimer = null;
|
|
785
|
+
}
|
|
786
|
+
this.clearTimeMilestones();
|
|
787
|
+
this.cleanupScrollTracking();
|
|
788
|
+
this.flushMouseBuffer();
|
|
789
|
+
this.flush();
|
|
790
|
+
}
|
|
791
|
+
setPageContext(url, path) {
|
|
792
|
+
this.flushMouseBuffer();
|
|
793
|
+
this.flush();
|
|
794
|
+
this.pageUrl = url;
|
|
795
|
+
this.pagePath = path;
|
|
796
|
+
this.scrollMilestones.clear();
|
|
797
|
+
this.timeMilestones.clear();
|
|
798
|
+
this.exitIntentFired = false;
|
|
799
|
+
this.startTime = Date.now();
|
|
800
|
+
this.clickHistory = [];
|
|
801
|
+
this.clearTimeMilestones();
|
|
802
|
+
this.cleanupScrollTracking();
|
|
803
|
+
this.setupTimeMilestones();
|
|
804
|
+
requestAnimationFrame(() => this.setupScrollTracking());
|
|
805
|
+
}
|
|
806
|
+
// ---------------------------------------------------------------------------
|
|
807
|
+
// Buffer management
|
|
808
|
+
// ---------------------------------------------------------------------------
|
|
809
|
+
push(event) {
|
|
810
|
+
this.buffer.push(event);
|
|
811
|
+
if (this.buffer.length >= this.config.maxBufferSize) {
|
|
812
|
+
this.flush();
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
flush() {
|
|
816
|
+
if (this.buffer.length === 0) return;
|
|
817
|
+
const batch = {
|
|
818
|
+
session_id: this.config.sessionId,
|
|
819
|
+
visitor_id: this.config.visitorId,
|
|
820
|
+
events: this.buffer
|
|
821
|
+
};
|
|
822
|
+
this.buffer = [];
|
|
823
|
+
this.config.sendBatch(batch).catch(() => {
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
addListener(target, event, handler) {
|
|
827
|
+
target.addEventListener(event, handler, { passive: true, capture: true });
|
|
828
|
+
this.listeners.push([target, event, handler]);
|
|
829
|
+
}
|
|
830
|
+
// ---------------------------------------------------------------------------
|
|
831
|
+
// Scroll tracking with IntersectionObserver
|
|
832
|
+
// ---------------------------------------------------------------------------
|
|
833
|
+
setupScrollTracking() {
|
|
834
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
835
|
+
this.observer = new IntersectionObserver(
|
|
836
|
+
(entries) => {
|
|
837
|
+
for (const entry of entries) {
|
|
838
|
+
if (!entry.isIntersecting) continue;
|
|
839
|
+
const milestone = Number(entry.target.getAttribute("data-scroll-milestone"));
|
|
840
|
+
if (isNaN(milestone) || this.scrollMilestones.has(milestone)) continue;
|
|
841
|
+
this.scrollMilestones.add(milestone);
|
|
842
|
+
this.push({
|
|
843
|
+
data_type: "scroll_depth",
|
|
844
|
+
data: {
|
|
845
|
+
depth_percent: milestone,
|
|
846
|
+
timestamp: Date.now()
|
|
847
|
+
},
|
|
848
|
+
page_url: this.pageUrl,
|
|
849
|
+
page_path: this.pagePath
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
{ threshold: 0 }
|
|
854
|
+
);
|
|
855
|
+
const docHeight = document.documentElement.scrollHeight;
|
|
856
|
+
for (const pct of SCROLL_MILESTONES) {
|
|
857
|
+
const sentinel = document.createElement("div");
|
|
858
|
+
sentinel.setAttribute("data-scroll-milestone", String(pct));
|
|
859
|
+
sentinel.style.position = "absolute";
|
|
860
|
+
sentinel.style.left = "0";
|
|
861
|
+
sentinel.style.width = "1px";
|
|
862
|
+
sentinel.style.height = "1px";
|
|
863
|
+
sentinel.style.pointerEvents = "none";
|
|
864
|
+
sentinel.style.opacity = "0";
|
|
865
|
+
sentinel.style.top = `${docHeight * pct / 100 - 1}px`;
|
|
866
|
+
document.body.appendChild(sentinel);
|
|
867
|
+
this.sentinels.push(sentinel);
|
|
868
|
+
this.observer.observe(sentinel);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
cleanupScrollTracking() {
|
|
872
|
+
if (this.observer) {
|
|
873
|
+
this.observer.disconnect();
|
|
874
|
+
this.observer = null;
|
|
875
|
+
}
|
|
876
|
+
for (const sentinel of this.sentinels) {
|
|
877
|
+
sentinel.remove();
|
|
878
|
+
}
|
|
879
|
+
this.sentinels = [];
|
|
880
|
+
}
|
|
881
|
+
// ---------------------------------------------------------------------------
|
|
882
|
+
// Time milestones
|
|
883
|
+
// ---------------------------------------------------------------------------
|
|
884
|
+
setupTimeMilestones() {
|
|
885
|
+
for (const seconds of TIME_MILESTONES) {
|
|
886
|
+
const timer = setTimeout(() => {
|
|
887
|
+
if (this.timeMilestones.has(seconds)) return;
|
|
888
|
+
this.timeMilestones.add(seconds);
|
|
889
|
+
this.push({
|
|
890
|
+
data_type: "time_on_page",
|
|
891
|
+
data: {
|
|
892
|
+
milestone_seconds: seconds,
|
|
893
|
+
timestamp: Date.now()
|
|
894
|
+
},
|
|
895
|
+
page_url: this.pageUrl,
|
|
896
|
+
page_path: this.pagePath
|
|
897
|
+
});
|
|
898
|
+
}, seconds * 1e3);
|
|
899
|
+
this.timeTimers.push(timer);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
clearTimeMilestones() {
|
|
903
|
+
for (const timer of this.timeTimers) {
|
|
904
|
+
clearTimeout(timer);
|
|
905
|
+
}
|
|
906
|
+
this.timeTimers = [];
|
|
907
|
+
}
|
|
908
|
+
// ---------------------------------------------------------------------------
|
|
909
|
+
// Rage click detection
|
|
910
|
+
// ---------------------------------------------------------------------------
|
|
911
|
+
detectRageClick(x, y, now2) {
|
|
912
|
+
const windowStart = now2 - RAGE_CLICK_WINDOW_MS;
|
|
913
|
+
const nearby = this.clickHistory.filter((c) => {
|
|
914
|
+
if (c.t < windowStart) return false;
|
|
915
|
+
const dx = c.x - x;
|
|
916
|
+
const dy = c.y - y;
|
|
917
|
+
return Math.sqrt(dx * dx + dy * dy) <= RAGE_CLICK_RADIUS;
|
|
918
|
+
});
|
|
919
|
+
if (nearby.length >= RAGE_CLICK_COUNT) {
|
|
920
|
+
this.push({
|
|
921
|
+
data_type: "rage_click",
|
|
922
|
+
data: {
|
|
923
|
+
x,
|
|
924
|
+
y,
|
|
925
|
+
click_count: nearby.length,
|
|
926
|
+
timestamp: now2
|
|
927
|
+
},
|
|
928
|
+
page_url: this.pageUrl,
|
|
929
|
+
page_path: this.pagePath
|
|
930
|
+
});
|
|
931
|
+
this.clickHistory = [];
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
// ---------------------------------------------------------------------------
|
|
935
|
+
// Mouse buffer flush
|
|
936
|
+
// ---------------------------------------------------------------------------
|
|
937
|
+
flushMouseBuffer() {
|
|
938
|
+
if (this.mouseBuffer.length === 0) return;
|
|
939
|
+
this.push({
|
|
940
|
+
data_type: "mouse_movement",
|
|
941
|
+
data: {
|
|
942
|
+
points: [...this.mouseBuffer],
|
|
943
|
+
timestamp: Date.now()
|
|
944
|
+
},
|
|
945
|
+
page_url: this.pageUrl,
|
|
946
|
+
page_path: this.pagePath
|
|
947
|
+
});
|
|
948
|
+
this.mouseBuffer = [];
|
|
949
|
+
}
|
|
950
|
+
// ---------------------------------------------------------------------------
|
|
951
|
+
// CSS selector helper
|
|
952
|
+
// ---------------------------------------------------------------------------
|
|
953
|
+
getSelector(el) {
|
|
954
|
+
const parts = [];
|
|
955
|
+
let current = el;
|
|
956
|
+
let depth = 0;
|
|
957
|
+
while (current && depth < 3) {
|
|
958
|
+
let segment = current.tagName.toLowerCase();
|
|
959
|
+
if (current.id) {
|
|
960
|
+
segment += `#${current.id}`;
|
|
961
|
+
} else if (current.classList.length > 0) {
|
|
962
|
+
segment += `.${Array.from(current.classList).join(".")}`;
|
|
963
|
+
}
|
|
964
|
+
parts.unshift(segment);
|
|
965
|
+
current = current.parentElement;
|
|
966
|
+
depth++;
|
|
967
|
+
}
|
|
968
|
+
return parts.join(" > ");
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
// src/react/components/behavioral-tracker.tsx
|
|
973
|
+
var SESSION_KEY_SUFFIX2 = "-analytics-session";
|
|
974
|
+
var VISITOR_KEY_SUFFIX2 = "-visitor-id";
|
|
975
|
+
var MAX_SESSION_WAIT_MS = 1e4;
|
|
976
|
+
var SESSION_POLL_MS = 500;
|
|
977
|
+
function BehavioralTrackerComponent({ pathname }) {
|
|
978
|
+
const ctx = useContext(WhaleContext);
|
|
979
|
+
const trackerRef = useRef(null);
|
|
980
|
+
const initRef = useRef(false);
|
|
981
|
+
useEffect(() => {
|
|
982
|
+
if (!ctx || !ctx.config.trackingEnabled) return;
|
|
983
|
+
if (typeof window === "undefined") return;
|
|
984
|
+
const { config } = ctx;
|
|
985
|
+
let cancelled = false;
|
|
986
|
+
let pollTimer = null;
|
|
987
|
+
const startTime = Date.now();
|
|
988
|
+
const readSessionId = () => {
|
|
989
|
+
const key = `${config.storagePrefix}${SESSION_KEY_SUFFIX2}`;
|
|
990
|
+
try {
|
|
991
|
+
const raw = localStorage.getItem(key);
|
|
992
|
+
if (raw) {
|
|
993
|
+
const stored = JSON.parse(raw);
|
|
994
|
+
return stored.id ?? null;
|
|
995
|
+
}
|
|
996
|
+
} catch {
|
|
997
|
+
}
|
|
998
|
+
return null;
|
|
999
|
+
};
|
|
1000
|
+
const readVisitorId = () => {
|
|
1001
|
+
const key = `${config.storagePrefix}${VISITOR_KEY_SUFFIX2}`;
|
|
1002
|
+
try {
|
|
1003
|
+
const existing = localStorage.getItem(key);
|
|
1004
|
+
if (existing) return existing;
|
|
1005
|
+
} catch {
|
|
1006
|
+
}
|
|
1007
|
+
const id = `v-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1008
|
+
try {
|
|
1009
|
+
localStorage.setItem(key, id);
|
|
1010
|
+
} catch {
|
|
1011
|
+
}
|
|
1012
|
+
return id;
|
|
1013
|
+
};
|
|
1014
|
+
const tryInit = () => {
|
|
1015
|
+
if (cancelled) return;
|
|
1016
|
+
const sessionId = readSessionId();
|
|
1017
|
+
if (!sessionId) {
|
|
1018
|
+
if (Date.now() - startTime < MAX_SESSION_WAIT_MS) {
|
|
1019
|
+
pollTimer = setTimeout(tryInit, SESSION_POLL_MS);
|
|
1020
|
+
}
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
initRef.current = true;
|
|
1024
|
+
const visitorId = readVisitorId();
|
|
1025
|
+
const baseUrl = config.proxyPath;
|
|
1026
|
+
const endpoint = `${baseUrl}/v1/stores/${config.storeId}/storefront/behavioral`;
|
|
1027
|
+
const sendBatch = async (batch) => {
|
|
1028
|
+
await resilientSend(endpoint, batch, {
|
|
1029
|
+
"Content-Type": "application/json",
|
|
1030
|
+
"x-api-key": config.apiKey
|
|
1031
|
+
});
|
|
1032
|
+
};
|
|
1033
|
+
const tracker = new BehavioralTracker({ sendBatch, sessionId, visitorId });
|
|
1034
|
+
tracker.start();
|
|
1035
|
+
trackerRef.current = tracker;
|
|
1036
|
+
};
|
|
1037
|
+
tryInit();
|
|
1038
|
+
return () => {
|
|
1039
|
+
cancelled = true;
|
|
1040
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
1041
|
+
if (trackerRef.current) {
|
|
1042
|
+
trackerRef.current.stop();
|
|
1043
|
+
trackerRef.current = null;
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
}, []);
|
|
1047
|
+
useEffect(() => {
|
|
1048
|
+
if (!trackerRef.current || !pathname) return;
|
|
1049
|
+
const url = typeof window !== "undefined" ? window.location.href : pathname;
|
|
1050
|
+
trackerRef.current.setPageContext(url, pathname);
|
|
1051
|
+
}, [pathname]);
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// src/fingerprint/collector.ts
|
|
1056
|
+
async function sha256(input) {
|
|
1057
|
+
const data = new TextEncoder().encode(input);
|
|
1058
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
1059
|
+
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1060
|
+
}
|
|
1061
|
+
async function getCanvasFingerprint() {
|
|
1062
|
+
try {
|
|
1063
|
+
const canvas = document.createElement("canvas");
|
|
1064
|
+
canvas.width = 256;
|
|
1065
|
+
canvas.height = 256;
|
|
1066
|
+
const ctx = canvas.getContext("2d");
|
|
1067
|
+
if (!ctx) return "";
|
|
1068
|
+
const gradient = ctx.createLinearGradient(0, 0, 256, 256);
|
|
1069
|
+
gradient.addColorStop(0, "#ff6b35");
|
|
1070
|
+
gradient.addColorStop(0.5, "#1a73e8");
|
|
1071
|
+
gradient.addColorStop(1, "#34a853");
|
|
1072
|
+
ctx.fillStyle = gradient;
|
|
1073
|
+
ctx.fillRect(0, 0, 256, 256);
|
|
1074
|
+
ctx.fillStyle = "#ffffff";
|
|
1075
|
+
ctx.font = "18px Arial";
|
|
1076
|
+
ctx.textBaseline = "top";
|
|
1077
|
+
ctx.fillText("WhaleTools", 10, 10);
|
|
1078
|
+
ctx.beginPath();
|
|
1079
|
+
ctx.arc(128, 128, 60, 0, Math.PI * 2);
|
|
1080
|
+
ctx.strokeStyle = "#fbbc04";
|
|
1081
|
+
ctx.lineWidth = 3;
|
|
1082
|
+
ctx.stroke();
|
|
1083
|
+
ctx.beginPath();
|
|
1084
|
+
ctx.moveTo(0, 0);
|
|
1085
|
+
ctx.lineTo(256, 256);
|
|
1086
|
+
ctx.strokeStyle = "#ea4335";
|
|
1087
|
+
ctx.lineWidth = 2;
|
|
1088
|
+
ctx.stroke();
|
|
1089
|
+
const dataUrl = canvas.toDataURL();
|
|
1090
|
+
return sha256(dataUrl);
|
|
1091
|
+
} catch {
|
|
1092
|
+
return "";
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
async function getWebGLFingerprint() {
|
|
1096
|
+
try {
|
|
1097
|
+
const canvas = document.createElement("canvas");
|
|
1098
|
+
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
1099
|
+
if (!gl || !(gl instanceof WebGLRenderingContext)) return "";
|
|
1100
|
+
const ext = gl.getExtension("WEBGL_debug_renderer_info");
|
|
1101
|
+
const renderer = ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : "unknown";
|
|
1102
|
+
const vendor = ext ? gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) : "unknown";
|
|
1103
|
+
const version = gl.getParameter(gl.VERSION);
|
|
1104
|
+
const combined = `${renderer}|${vendor}|${version}`;
|
|
1105
|
+
return sha256(combined);
|
|
1106
|
+
} catch {
|
|
1107
|
+
return "";
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
async function getAudioFingerprint() {
|
|
1111
|
+
try {
|
|
1112
|
+
const AudioCtx = window.OfflineAudioContext || window.webkitOfflineAudioContext;
|
|
1113
|
+
if (!AudioCtx) return "";
|
|
1114
|
+
const context = new AudioCtx(1, 44100, 44100);
|
|
1115
|
+
const oscillator = context.createOscillator();
|
|
1116
|
+
oscillator.type = "triangle";
|
|
1117
|
+
oscillator.frequency.setValueAtTime(1e4, context.currentTime);
|
|
1118
|
+
const compressor = context.createDynamicsCompressor();
|
|
1119
|
+
compressor.threshold.setValueAtTime(-50, context.currentTime);
|
|
1120
|
+
compressor.knee.setValueAtTime(40, context.currentTime);
|
|
1121
|
+
compressor.ratio.setValueAtTime(12, context.currentTime);
|
|
1122
|
+
compressor.attack.setValueAtTime(0, context.currentTime);
|
|
1123
|
+
compressor.release.setValueAtTime(0.25, context.currentTime);
|
|
1124
|
+
oscillator.connect(compressor);
|
|
1125
|
+
compressor.connect(context.destination);
|
|
1126
|
+
oscillator.start(0);
|
|
1127
|
+
const buffer = await context.startRendering();
|
|
1128
|
+
const samples = buffer.getChannelData(0).slice(0, 100);
|
|
1129
|
+
const sampleStr = Array.from(samples).map((s) => s.toString()).join(",");
|
|
1130
|
+
return sha256(sampleStr);
|
|
1131
|
+
} catch {
|
|
1132
|
+
return "";
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
async function collectFingerprint() {
|
|
1136
|
+
const [canvas_fingerprint, webgl_fingerprint, audio_fingerprint] = await Promise.all([
|
|
1137
|
+
getCanvasFingerprint(),
|
|
1138
|
+
getWebGLFingerprint(),
|
|
1139
|
+
getAudioFingerprint()
|
|
1140
|
+
]);
|
|
1141
|
+
const screen_resolution = `${window.screen.width}x${window.screen.height}`;
|
|
1142
|
+
const platform = navigator.platform || "";
|
|
1143
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "";
|
|
1144
|
+
const language = navigator.language || "";
|
|
1145
|
+
const hardware_concurrency = navigator.hardwareConcurrency || 0;
|
|
1146
|
+
const device_memory = navigator.deviceMemory ?? null;
|
|
1147
|
+
const color_depth = window.screen.colorDepth;
|
|
1148
|
+
const pixel_ratio = window.devicePixelRatio || 1;
|
|
1149
|
+
const touch_support = "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
|
1150
|
+
const cookie_enabled = navigator.cookieEnabled;
|
|
1151
|
+
const do_not_track = navigator.doNotTrack ?? null;
|
|
1152
|
+
const fingerprintSource = [
|
|
1153
|
+
canvas_fingerprint,
|
|
1154
|
+
webgl_fingerprint,
|
|
1155
|
+
audio_fingerprint,
|
|
1156
|
+
screen_resolution,
|
|
1157
|
+
platform,
|
|
1158
|
+
timezone,
|
|
1159
|
+
language,
|
|
1160
|
+
String(hardware_concurrency)
|
|
1161
|
+
].join("|");
|
|
1162
|
+
const fingerprint_id = await sha256(fingerprintSource);
|
|
1163
|
+
return {
|
|
1164
|
+
fingerprint_id,
|
|
1165
|
+
canvas_fingerprint,
|
|
1166
|
+
webgl_fingerprint,
|
|
1167
|
+
audio_fingerprint,
|
|
1168
|
+
screen_resolution,
|
|
1169
|
+
platform,
|
|
1170
|
+
timezone,
|
|
1171
|
+
language,
|
|
1172
|
+
hardware_concurrency,
|
|
1173
|
+
device_memory,
|
|
1174
|
+
color_depth,
|
|
1175
|
+
pixel_ratio,
|
|
1176
|
+
touch_support,
|
|
1177
|
+
cookie_enabled,
|
|
1178
|
+
do_not_track
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// src/react/components/fingerprint-collector.tsx
|
|
1183
|
+
var SESSION_KEY_SUFFIX3 = "-analytics-session";
|
|
1184
|
+
function FingerprintCollector() {
|
|
1185
|
+
const ctx = useContext(WhaleContext);
|
|
1186
|
+
const sent = useRef(false);
|
|
1187
|
+
useEffect(() => {
|
|
1188
|
+
if (!ctx || sent.current) return;
|
|
1189
|
+
if (!ctx.config.trackingEnabled) return;
|
|
1190
|
+
if (typeof window === "undefined") return;
|
|
1191
|
+
sent.current = true;
|
|
1192
|
+
const { config, client } = ctx;
|
|
1193
|
+
const prefix = config.storagePrefix;
|
|
1194
|
+
const fpKey = `${prefix}-fingerprint-sent`;
|
|
1195
|
+
const linkFingerprintToSession = (fingerprintId) => {
|
|
1196
|
+
try {
|
|
1197
|
+
const sessionRaw = localStorage.getItem(`${prefix}${SESSION_KEY_SUFFIX3}`);
|
|
1198
|
+
if (sessionRaw) {
|
|
1199
|
+
const session = JSON.parse(sessionRaw);
|
|
1200
|
+
client.updateSession(session.id, { fingerprint_id: fingerprintId }).catch(() => {
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
} catch {
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
const existing = localStorage.getItem(fpKey);
|
|
1207
|
+
if (existing) {
|
|
1208
|
+
linkFingerprintToSession(existing);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
collectFingerprint().then(async (fp) => {
|
|
1212
|
+
const baseUrl = config.proxyPath;
|
|
1213
|
+
const url = `${baseUrl}/v1/stores/${config.storeId}/storefront/fingerprints`;
|
|
1214
|
+
await resilientSend(url, fp, {
|
|
1215
|
+
"Content-Type": "application/json",
|
|
1216
|
+
"x-api-key": config.apiKey
|
|
1217
|
+
}).catch(() => {
|
|
1218
|
+
});
|
|
1219
|
+
localStorage.setItem(fpKey, fp.fingerprint_id);
|
|
1220
|
+
linkFingerprintToSession(fp.fingerprint_id);
|
|
1221
|
+
}).catch(() => {
|
|
1222
|
+
});
|
|
1223
|
+
}, [ctx]);
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// src/recording/recorder.ts
|
|
1228
|
+
function now() {
|
|
1229
|
+
return Date.now();
|
|
1230
|
+
}
|
|
1231
|
+
function throttle(fn, ms) {
|
|
1232
|
+
let last = 0;
|
|
1233
|
+
let timer = null;
|
|
1234
|
+
const throttled = (...args) => {
|
|
1235
|
+
const elapsed = now() - last;
|
|
1236
|
+
if (elapsed >= ms) {
|
|
1237
|
+
last = now();
|
|
1238
|
+
fn(...args);
|
|
1239
|
+
} else if (!timer) {
|
|
1240
|
+
timer = setTimeout(() => {
|
|
1241
|
+
last = now();
|
|
1242
|
+
timer = null;
|
|
1243
|
+
fn(...args);
|
|
1244
|
+
}, ms - elapsed);
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
return throttled;
|
|
1248
|
+
}
|
|
1249
|
+
function shortSelector(el) {
|
|
1250
|
+
if (el.id) return `#${el.id}`;
|
|
1251
|
+
let sel = el.tagName.toLowerCase();
|
|
1252
|
+
if (el.className && typeof el.className === "string") {
|
|
1253
|
+
const cls = el.className.trim().split(/\s+/).slice(0, 2).join(".");
|
|
1254
|
+
if (cls) sel += `.${cls}`;
|
|
1255
|
+
}
|
|
1256
|
+
return sel;
|
|
1257
|
+
}
|
|
1258
|
+
var SessionRecorder = class {
|
|
1259
|
+
constructor(config) {
|
|
1260
|
+
this.events = [];
|
|
1261
|
+
this.sequence = 0;
|
|
1262
|
+
this.observer = null;
|
|
1263
|
+
this.flushTimer = null;
|
|
1264
|
+
this.listeners = [];
|
|
1265
|
+
this.started = false;
|
|
1266
|
+
this.flushing = false;
|
|
1267
|
+
/** Pending mutations collected within the current animation frame. */
|
|
1268
|
+
this.pendingMutations = [];
|
|
1269
|
+
this.mutationRafId = null;
|
|
1270
|
+
this.config = {
|
|
1271
|
+
sendChunk: config.sendChunk,
|
|
1272
|
+
flushIntervalMs: config.flushIntervalMs ?? 5e3,
|
|
1273
|
+
maxChunkSize: config.maxChunkSize ?? 200
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
// -----------------------------------------------------------------------
|
|
1277
|
+
// Public API
|
|
1278
|
+
// -----------------------------------------------------------------------
|
|
1279
|
+
start() {
|
|
1280
|
+
if (this.started) return;
|
|
1281
|
+
this.started = true;
|
|
1282
|
+
this.captureFullSnapshot();
|
|
1283
|
+
this.setupMutationObserver();
|
|
1284
|
+
this.setupEventListeners();
|
|
1285
|
+
this.flushTimer = setInterval(() => {
|
|
1286
|
+
void this.flush();
|
|
1287
|
+
}, this.config.flushIntervalMs);
|
|
1288
|
+
}
|
|
1289
|
+
stop() {
|
|
1290
|
+
if (!this.started) return;
|
|
1291
|
+
this.started = false;
|
|
1292
|
+
if (this.observer) {
|
|
1293
|
+
this.observer.disconnect();
|
|
1294
|
+
this.observer = null;
|
|
1295
|
+
}
|
|
1296
|
+
if (this.mutationRafId !== null) {
|
|
1297
|
+
cancelAnimationFrame(this.mutationRafId);
|
|
1298
|
+
this.mutationRafId = null;
|
|
1299
|
+
}
|
|
1300
|
+
for (const [target, event, handler, options] of this.listeners) {
|
|
1301
|
+
target.removeEventListener(event, handler, options);
|
|
1302
|
+
}
|
|
1303
|
+
this.listeners = [];
|
|
1304
|
+
if (this.flushTimer !== null) {
|
|
1305
|
+
clearInterval(this.flushTimer);
|
|
1306
|
+
this.flushTimer = null;
|
|
1307
|
+
}
|
|
1308
|
+
void this.flush();
|
|
1309
|
+
}
|
|
1310
|
+
// -----------------------------------------------------------------------
|
|
1311
|
+
// Full Snapshot (type 0)
|
|
1312
|
+
// -----------------------------------------------------------------------
|
|
1313
|
+
captureFullSnapshot() {
|
|
1314
|
+
const tree = this.serializeNode(document.documentElement);
|
|
1315
|
+
this.push({
|
|
1316
|
+
type: 0,
|
|
1317
|
+
timestamp: now(),
|
|
1318
|
+
data: {
|
|
1319
|
+
href: location.href,
|
|
1320
|
+
width: window.innerWidth,
|
|
1321
|
+
height: window.innerHeight,
|
|
1322
|
+
tree: tree ?? {}
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
serializeNode(node) {
|
|
1327
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1328
|
+
const el = node;
|
|
1329
|
+
const tag = el.tagName.toLowerCase();
|
|
1330
|
+
if (tag === "script" || tag === "noscript") return null;
|
|
1331
|
+
const attrs = {};
|
|
1332
|
+
for (const attr of Array.from(el.attributes)) {
|
|
1333
|
+
if (attr.name === "value" && el instanceof HTMLInputElement && el.type === "password") {
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
attrs[attr.name] = attr.value;
|
|
1337
|
+
}
|
|
1338
|
+
const children = [];
|
|
1339
|
+
for (const child of Array.from(el.childNodes)) {
|
|
1340
|
+
const serialized = this.serializeNode(child);
|
|
1341
|
+
if (serialized) children.push(serialized);
|
|
1342
|
+
}
|
|
1343
|
+
return { tag, attrs, children };
|
|
1344
|
+
}
|
|
1345
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1346
|
+
const text = node.textContent || "";
|
|
1347
|
+
if (!text.trim()) return null;
|
|
1348
|
+
return { text };
|
|
1349
|
+
}
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
// -----------------------------------------------------------------------
|
|
1353
|
+
// Mutation Observer (type 1)
|
|
1354
|
+
// -----------------------------------------------------------------------
|
|
1355
|
+
setupMutationObserver() {
|
|
1356
|
+
this.observer = new MutationObserver((mutations) => {
|
|
1357
|
+
this.pendingMutations.push(...mutations);
|
|
1358
|
+
if (this.mutationRafId === null) {
|
|
1359
|
+
this.mutationRafId = requestAnimationFrame(() => {
|
|
1360
|
+
this.processMutations(this.pendingMutations);
|
|
1361
|
+
this.pendingMutations = [];
|
|
1362
|
+
this.mutationRafId = null;
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
this.observer.observe(document.documentElement, {
|
|
1367
|
+
childList: true,
|
|
1368
|
+
attributes: true,
|
|
1369
|
+
characterData: true,
|
|
1370
|
+
subtree: true,
|
|
1371
|
+
attributeOldValue: false
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
processMutations(mutations) {
|
|
1375
|
+
const ts = now();
|
|
1376
|
+
const adds = [];
|
|
1377
|
+
const removes = [];
|
|
1378
|
+
const attrs = [];
|
|
1379
|
+
const texts = [];
|
|
1380
|
+
for (const m of mutations) {
|
|
1381
|
+
if (m.type === "childList") {
|
|
1382
|
+
for (const node of Array.from(m.addedNodes)) {
|
|
1383
|
+
if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE) {
|
|
1384
|
+
const serialized = this.serializeNode(node);
|
|
1385
|
+
if (serialized) {
|
|
1386
|
+
adds.push({
|
|
1387
|
+
parentSelector: m.target.nodeType === Node.ELEMENT_NODE ? shortSelector(m.target) : null,
|
|
1388
|
+
node: serialized
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
for (const node of Array.from(m.removedNodes)) {
|
|
1394
|
+
removes.push({
|
|
1395
|
+
parentSelector: m.target.nodeType === Node.ELEMENT_NODE ? shortSelector(m.target) : null,
|
|
1396
|
+
tag: node.nodeType === Node.ELEMENT_NODE ? node.tagName.toLowerCase() : "#text"
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
} else if (m.type === "attributes" && m.target.nodeType === Node.ELEMENT_NODE) {
|
|
1400
|
+
const el = m.target;
|
|
1401
|
+
const name = m.attributeName || "";
|
|
1402
|
+
attrs.push({
|
|
1403
|
+
selector: shortSelector(el),
|
|
1404
|
+
name,
|
|
1405
|
+
value: el.getAttribute(name)
|
|
1406
|
+
});
|
|
1407
|
+
} else if (m.type === "characterData") {
|
|
1408
|
+
texts.push({
|
|
1409
|
+
parentSelector: m.target.parentElement ? shortSelector(m.target.parentElement) : null,
|
|
1410
|
+
value: (m.target.textContent || "").slice(0, 200)
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
if (adds.length || removes.length || attrs.length || texts.length) {
|
|
1415
|
+
this.push({
|
|
1416
|
+
type: 1,
|
|
1417
|
+
timestamp: ts,
|
|
1418
|
+
data: {
|
|
1419
|
+
adds: adds.length ? adds : void 0,
|
|
1420
|
+
removes: removes.length ? removes : void 0,
|
|
1421
|
+
attrs: attrs.length ? attrs : void 0,
|
|
1422
|
+
texts: texts.length ? texts : void 0
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
// -----------------------------------------------------------------------
|
|
1428
|
+
// Event Listeners (types 2–5)
|
|
1429
|
+
// -----------------------------------------------------------------------
|
|
1430
|
+
setupEventListeners() {
|
|
1431
|
+
const onMouseMove = throttle(((e) => {
|
|
1432
|
+
this.push({
|
|
1433
|
+
type: 2,
|
|
1434
|
+
timestamp: now(),
|
|
1435
|
+
data: { source: "move", x: e.clientX, y: e.clientY }
|
|
1436
|
+
});
|
|
1437
|
+
}), 100);
|
|
1438
|
+
const onClick = ((e) => {
|
|
1439
|
+
const target = e.target;
|
|
1440
|
+
this.push({
|
|
1441
|
+
type: 2,
|
|
1442
|
+
timestamp: now(),
|
|
1443
|
+
data: {
|
|
1444
|
+
source: "click",
|
|
1445
|
+
x: e.clientX,
|
|
1446
|
+
y: e.clientY,
|
|
1447
|
+
target: target ? shortSelector(target) : null
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
});
|
|
1451
|
+
this.addListener(document, "mousemove", onMouseMove, { passive: true, capture: true });
|
|
1452
|
+
this.addListener(document, "click", onClick, { passive: true, capture: true });
|
|
1453
|
+
const onScroll = throttle(((e) => {
|
|
1454
|
+
const target = e.target;
|
|
1455
|
+
if (target === document || target === document.documentElement || target === window) {
|
|
1456
|
+
this.push({
|
|
1457
|
+
type: 3,
|
|
1458
|
+
timestamp: now(),
|
|
1459
|
+
data: { target: "window", x: window.scrollX, y: window.scrollY }
|
|
1460
|
+
});
|
|
1461
|
+
} else if (target instanceof Element) {
|
|
1462
|
+
this.push({
|
|
1463
|
+
type: 3,
|
|
1464
|
+
timestamp: now(),
|
|
1465
|
+
data: {
|
|
1466
|
+
target: shortSelector(target),
|
|
1467
|
+
x: target.scrollLeft,
|
|
1468
|
+
y: target.scrollTop
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
}), 200);
|
|
1473
|
+
this.addListener(document, "scroll", onScroll, { passive: true, capture: true });
|
|
1474
|
+
const onInput = ((e) => {
|
|
1475
|
+
const target = e.target;
|
|
1476
|
+
if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement)) {
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
const isPassword = target instanceof HTMLInputElement && target.type === "password";
|
|
1480
|
+
this.push({
|
|
1481
|
+
type: 4,
|
|
1482
|
+
timestamp: now(),
|
|
1483
|
+
data: {
|
|
1484
|
+
selector: shortSelector(target),
|
|
1485
|
+
tag: target.tagName.toLowerCase(),
|
|
1486
|
+
inputType: target instanceof HTMLInputElement ? target.type : void 0,
|
|
1487
|
+
value: isPassword ? "***" : (target.value || "").slice(0, 100)
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
});
|
|
1491
|
+
this.addListener(document, "input", onInput, { passive: true, capture: true });
|
|
1492
|
+
this.addListener(document, "change", onInput, { passive: true, capture: true });
|
|
1493
|
+
const onResize = throttle((() => {
|
|
1494
|
+
this.push({
|
|
1495
|
+
type: 5,
|
|
1496
|
+
timestamp: now(),
|
|
1497
|
+
data: { width: window.innerWidth, height: window.innerHeight }
|
|
1498
|
+
});
|
|
1499
|
+
}), 500);
|
|
1500
|
+
this.addListener(window, "resize", onResize, { passive: true });
|
|
1501
|
+
const onVisibility = (() => {
|
|
1502
|
+
if (document.visibilityState === "hidden") {
|
|
1503
|
+
void this.flush();
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
this.addListener(document, "visibilitychange", onVisibility);
|
|
1507
|
+
}
|
|
1508
|
+
// -----------------------------------------------------------------------
|
|
1509
|
+
// Event buffer & flushing
|
|
1510
|
+
// -----------------------------------------------------------------------
|
|
1511
|
+
push(event) {
|
|
1512
|
+
if (!this.started) return;
|
|
1513
|
+
this.events.push(event);
|
|
1514
|
+
if (this.events.length >= this.config.maxChunkSize) {
|
|
1515
|
+
void this.flush();
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
async flush() {
|
|
1519
|
+
if (this.events.length === 0) return;
|
|
1520
|
+
if (this.flushing) return;
|
|
1521
|
+
this.flushing = true;
|
|
1522
|
+
const chunk = this.events;
|
|
1523
|
+
this.events = [];
|
|
1524
|
+
const seq = this.sequence++;
|
|
1525
|
+
try {
|
|
1526
|
+
await this.config.sendChunk(chunk, seq);
|
|
1527
|
+
} catch {
|
|
1528
|
+
} finally {
|
|
1529
|
+
this.flushing = false;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
// -----------------------------------------------------------------------
|
|
1533
|
+
// Listener bookkeeping
|
|
1534
|
+
// -----------------------------------------------------------------------
|
|
1535
|
+
addListener(target, event, handler, options) {
|
|
1536
|
+
target.addEventListener(event, handler, options);
|
|
1537
|
+
this.listeners.push([target, event, handler, options]);
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1541
|
+
// src/react/components/session-recorder.tsx
|
|
1542
|
+
var SESSION_KEY_SUFFIX4 = "-analytics-session";
|
|
1543
|
+
var VISITOR_KEY_SUFFIX3 = "-visitor-id";
|
|
1544
|
+
var MAX_SESSION_WAIT_MS2 = 1e4;
|
|
1545
|
+
var SESSION_POLL_MS2 = 500;
|
|
1546
|
+
function SessionRecorderComponent() {
|
|
1547
|
+
const ctx = useContext(WhaleContext);
|
|
1548
|
+
const recorderRef = useRef(null);
|
|
1549
|
+
const sampledRef = useRef(null);
|
|
1550
|
+
const initRef = useRef(false);
|
|
1551
|
+
useEffect(() => {
|
|
1552
|
+
if (!ctx || initRef.current) return;
|
|
1553
|
+
if (!ctx.config.trackingEnabled) return;
|
|
1554
|
+
if (typeof window === "undefined") return;
|
|
1555
|
+
if (sampledRef.current === null) {
|
|
1556
|
+
sampledRef.current = Math.random() < ctx.config.recordingRate;
|
|
1557
|
+
}
|
|
1558
|
+
if (!sampledRef.current) return;
|
|
1559
|
+
const { config } = ctx;
|
|
1560
|
+
const prefix = config.storagePrefix;
|
|
1561
|
+
let cancelled = false;
|
|
1562
|
+
let pollTimer = null;
|
|
1563
|
+
const startTime = Date.now();
|
|
1564
|
+
const tryInit = () => {
|
|
1565
|
+
if (cancelled) return;
|
|
1566
|
+
let sessionId = null;
|
|
1567
|
+
try {
|
|
1568
|
+
const sessionRaw = localStorage.getItem(`${prefix}${SESSION_KEY_SUFFIX4}`);
|
|
1569
|
+
if (sessionRaw) {
|
|
1570
|
+
const session = JSON.parse(sessionRaw);
|
|
1571
|
+
sessionId = session.id || null;
|
|
1572
|
+
}
|
|
1573
|
+
} catch {
|
|
1574
|
+
}
|
|
1575
|
+
if (!sessionId) {
|
|
1576
|
+
if (Date.now() - startTime < MAX_SESSION_WAIT_MS2) {
|
|
1577
|
+
pollTimer = setTimeout(tryInit, SESSION_POLL_MS2);
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
initRef.current = true;
|
|
1583
|
+
const visitorId = (() => {
|
|
1584
|
+
try {
|
|
1585
|
+
return localStorage.getItem(`${prefix}${VISITOR_KEY_SUFFIX3}`) || "unknown";
|
|
1586
|
+
} catch {
|
|
1587
|
+
return "unknown";
|
|
1588
|
+
}
|
|
1589
|
+
})();
|
|
1590
|
+
const baseUrl = config.proxyPath;
|
|
1591
|
+
const url = `${baseUrl}/v1/stores/${config.storeId}/storefront/recordings`;
|
|
1592
|
+
const sid = sessionId;
|
|
1593
|
+
const recorder = new SessionRecorder({
|
|
1594
|
+
sendChunk: async (events, sequence) => {
|
|
1595
|
+
await resilientSend(url, {
|
|
1596
|
+
session_id: sid,
|
|
1597
|
+
visitor_id: visitorId,
|
|
1598
|
+
events,
|
|
1599
|
+
sequence,
|
|
1600
|
+
started_at: sequence === 0 ? (/* @__PURE__ */ new Date()).toISOString() : void 0
|
|
1601
|
+
}, {
|
|
1602
|
+
"Content-Type": "application/json",
|
|
1603
|
+
"x-api-key": config.apiKey
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
recorder.start();
|
|
1608
|
+
recorderRef.current = recorder;
|
|
1609
|
+
};
|
|
1610
|
+
tryInit();
|
|
1611
|
+
return () => {
|
|
1612
|
+
cancelled = true;
|
|
1613
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
1614
|
+
if (recorderRef.current) {
|
|
1615
|
+
recorderRef.current.stop();
|
|
1616
|
+
recorderRef.current = null;
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
}, []);
|
|
1620
|
+
return null;
|
|
1621
|
+
}
|
|
560
1622
|
function envBool(name) {
|
|
561
1623
|
if (typeof process === "undefined") return void 0;
|
|
562
1624
|
const raw = process.env[name];
|
|
@@ -615,7 +1677,39 @@ function WhaleProvider({
|
|
|
615
1677
|
gatewayUrl: resolvedConfig.gatewayUrl,
|
|
616
1678
|
proxyPath: resolvedConfig.proxyPath
|
|
617
1679
|
});
|
|
618
|
-
const
|
|
1680
|
+
const readSessionId = () => {
|
|
1681
|
+
try {
|
|
1682
|
+
const raw = localStorage.getItem(`${resolvedConfig.storagePrefix}-analytics-session`);
|
|
1683
|
+
if (!raw) return void 0;
|
|
1684
|
+
const stored = JSON.parse(raw);
|
|
1685
|
+
if (Date.now() - stored.createdAt < resolvedConfig.sessionTtl) return stored.id;
|
|
1686
|
+
} catch {
|
|
1687
|
+
}
|
|
1688
|
+
return void 0;
|
|
1689
|
+
};
|
|
1690
|
+
const syncCartToSession = (cartId, total, itemCount) => {
|
|
1691
|
+
const sid = readSessionId();
|
|
1692
|
+
if (sid && !sid.startsWith("local-")) {
|
|
1693
|
+
client.updateSession(sid, {
|
|
1694
|
+
cart_id: cartId,
|
|
1695
|
+
cart_total: total,
|
|
1696
|
+
cart_item_count: itemCount,
|
|
1697
|
+
status: "carting"
|
|
1698
|
+
}).catch(() => {
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
const onAddToCart = resolvedConfig.trackingEnabled ? (productId, productName, quantity, price, tier) => {
|
|
1703
|
+
const sid = readSessionId();
|
|
1704
|
+
if (sid) client.trackEvent({ session_id: sid, event_type: "add_to_cart", event_data: { product_id: productId, product_name: productName, quantity, price, tier } }).catch(() => {
|
|
1705
|
+
});
|
|
1706
|
+
} : void 0;
|
|
1707
|
+
const onRemoveFromCart = resolvedConfig.trackingEnabled ? (productId, productName) => {
|
|
1708
|
+
const sid = readSessionId();
|
|
1709
|
+
if (sid) client.trackEvent({ session_id: sid, event_type: "remove_from_cart", event_data: { product_id: productId, product_name: productName } }).catch(() => {
|
|
1710
|
+
});
|
|
1711
|
+
} : void 0;
|
|
1712
|
+
const cartStore = createCartStore(client, resolvedConfig.storagePrefix, onAddToCart, onRemoveFromCart, syncCartToSession);
|
|
619
1713
|
const authStore = createAuthStore(client, resolvedConfig.storagePrefix);
|
|
620
1714
|
return {
|
|
621
1715
|
client,
|
|
@@ -639,6 +1733,9 @@ function WhaleProvider({
|
|
|
639
1733
|
/* @__PURE__ */ jsx(CartInitializer, {}),
|
|
640
1734
|
/* @__PURE__ */ jsx(AnalyticsTracker, { pathname }),
|
|
641
1735
|
/* @__PURE__ */ jsx(PixelInitializer, { onReady: handlePixelReady, onTheme: handleTheme }),
|
|
1736
|
+
/* @__PURE__ */ jsx(BehavioralTrackerComponent, { pathname }),
|
|
1737
|
+
/* @__PURE__ */ jsx(FingerprintCollector, {}),
|
|
1738
|
+
/* @__PURE__ */ jsx(SessionRecorderComponent, {}),
|
|
642
1739
|
children
|
|
643
1740
|
] }) });
|
|
644
1741
|
}
|
|
@@ -762,12 +1859,12 @@ function useCheckout() {
|
|
|
762
1859
|
setLoading(false);
|
|
763
1860
|
}
|
|
764
1861
|
}, [ctx.client, session]);
|
|
765
|
-
const complete = useCallback(async (payment) => {
|
|
1862
|
+
const complete = useCallback(async (payment, opts) => {
|
|
766
1863
|
if (!session) throw new Error("No active checkout session");
|
|
767
1864
|
setLoading(true);
|
|
768
1865
|
setError(null);
|
|
769
1866
|
try {
|
|
770
|
-
const order = await ctx.client.completeCheckout(session.id, payment);
|
|
1867
|
+
const order = await ctx.client.completeCheckout(session.id, payment, opts);
|
|
771
1868
|
setSession(null);
|
|
772
1869
|
return order;
|
|
773
1870
|
} catch (err) {
|
|
@@ -921,7 +2018,11 @@ function useLoyalty() {
|
|
|
921
2018
|
await refresh();
|
|
922
2019
|
return result;
|
|
923
2020
|
}, [customer?.id, ctx.client, refresh]);
|
|
924
|
-
|
|
2021
|
+
const fetchProductsByCategory = useCallback(async (category, locationId, tier) => {
|
|
2022
|
+
const res = await ctx.client.listLoyaltyProducts({ category, location_id: locationId, tier });
|
|
2023
|
+
return res.data;
|
|
2024
|
+
}, [ctx.client]);
|
|
2025
|
+
return { account, rewards, transactions, loading, error, refresh, redeemReward, fetchProductsByCategory };
|
|
925
2026
|
}
|
|
926
2027
|
function useReviews(productId) {
|
|
927
2028
|
const ctx = useContext(WhaleContext);
|
|
@@ -1149,9 +2250,9 @@ function useShipping() {
|
|
|
1149
2250
|
}, [ctx.client]);
|
|
1150
2251
|
return { methods, rates, loading, error, refreshMethods, calculateRates };
|
|
1151
2252
|
}
|
|
1152
|
-
function
|
|
2253
|
+
function useDeals() {
|
|
1153
2254
|
const ctx = useContext(WhaleContext);
|
|
1154
|
-
if (!ctx) throw new Error("
|
|
2255
|
+
if (!ctx) throw new Error("useDeals must be used within <WhaleProvider>");
|
|
1155
2256
|
const [validation, setValidation] = useState(null);
|
|
1156
2257
|
const [loading, setLoading] = useState(false);
|
|
1157
2258
|
const [error, setError] = useState(null);
|
|
@@ -1159,7 +2260,7 @@ function useCoupons() {
|
|
|
1159
2260
|
setLoading(true);
|
|
1160
2261
|
setError(null);
|
|
1161
2262
|
try {
|
|
1162
|
-
const result = await ctx.client.
|
|
2263
|
+
const result = await ctx.client.validateDeal(code, cartId ? { cart_id: cartId } : void 0);
|
|
1163
2264
|
setValidation(result);
|
|
1164
2265
|
return result;
|
|
1165
2266
|
} catch (err) {
|
|
@@ -1175,7 +2276,7 @@ function useCoupons() {
|
|
|
1175
2276
|
setLoading(true);
|
|
1176
2277
|
setError(null);
|
|
1177
2278
|
try {
|
|
1178
|
-
const cart = await ctx.client.
|
|
2279
|
+
const cart = await ctx.client.applyDeal(cartId, code);
|
|
1179
2280
|
return cart;
|
|
1180
2281
|
} catch (err) {
|
|
1181
2282
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
@@ -1189,7 +2290,7 @@ function useCoupons() {
|
|
|
1189
2290
|
setLoading(true);
|
|
1190
2291
|
setError(null);
|
|
1191
2292
|
try {
|
|
1192
|
-
const cart = await ctx.client.
|
|
2293
|
+
const cart = await ctx.client.removeDeal(cartId);
|
|
1193
2294
|
setValidation(null);
|
|
1194
2295
|
return cart;
|
|
1195
2296
|
} catch (err) {
|
|
@@ -1206,16 +2307,133 @@ function useCoupons() {
|
|
|
1206
2307
|
}, []);
|
|
1207
2308
|
return { validation, loading, error, validate, apply, remove, clear };
|
|
1208
2309
|
}
|
|
2310
|
+
var useCoupons = useDeals;
|
|
2311
|
+
function useReferral() {
|
|
2312
|
+
const ctx = useContext(WhaleContext);
|
|
2313
|
+
if (!ctx) throw new Error("useReferral must be used within <WhaleProvider>");
|
|
2314
|
+
const customer = useStore(ctx.authStore, (s) => s.customer);
|
|
2315
|
+
const [status, setStatus] = useState(null);
|
|
2316
|
+
const [loading, setLoading] = useState(false);
|
|
2317
|
+
const [error, setError] = useState(null);
|
|
2318
|
+
const refresh = useCallback(async () => {
|
|
2319
|
+
if (!customer?.id) {
|
|
2320
|
+
setStatus(null);
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
setLoading(true);
|
|
2324
|
+
setError(null);
|
|
2325
|
+
try {
|
|
2326
|
+
const result = await ctx.client.getReferralStatus(customer.id);
|
|
2327
|
+
setStatus(result);
|
|
2328
|
+
} catch (err) {
|
|
2329
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2330
|
+
} finally {
|
|
2331
|
+
setLoading(false);
|
|
2332
|
+
}
|
|
2333
|
+
}, [customer?.id, ctx.client]);
|
|
2334
|
+
useEffect(() => {
|
|
2335
|
+
refresh();
|
|
2336
|
+
}, [refresh]);
|
|
2337
|
+
const enroll = useCallback(async () => {
|
|
2338
|
+
if (!customer?.id) throw new Error("Not authenticated");
|
|
2339
|
+
setLoading(true);
|
|
2340
|
+
setError(null);
|
|
2341
|
+
try {
|
|
2342
|
+
const result = await ctx.client.enrollReferral(customer.id);
|
|
2343
|
+
await refresh();
|
|
2344
|
+
return result;
|
|
2345
|
+
} catch (err) {
|
|
2346
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
2347
|
+
setError(e);
|
|
2348
|
+
throw e;
|
|
2349
|
+
} finally {
|
|
2350
|
+
setLoading(false);
|
|
2351
|
+
}
|
|
2352
|
+
}, [customer?.id, ctx.client, refresh]);
|
|
2353
|
+
const attributeReferral = useCallback(
|
|
2354
|
+
async (code) => {
|
|
2355
|
+
if (!customer?.id) throw new Error("Not authenticated");
|
|
2356
|
+
setLoading(true);
|
|
2357
|
+
setError(null);
|
|
2358
|
+
try {
|
|
2359
|
+
const result = await ctx.client.attributeReferral(customer.id, code);
|
|
2360
|
+
await refresh();
|
|
2361
|
+
return result;
|
|
2362
|
+
} catch (err) {
|
|
2363
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
2364
|
+
setError(e);
|
|
2365
|
+
throw e;
|
|
2366
|
+
} finally {
|
|
2367
|
+
setLoading(false);
|
|
2368
|
+
}
|
|
2369
|
+
},
|
|
2370
|
+
[customer?.id, ctx.client, refresh]
|
|
2371
|
+
);
|
|
2372
|
+
useEffect(() => {
|
|
2373
|
+
if (!customer?.id || !status || status.referred_by) return;
|
|
2374
|
+
if (typeof window === "undefined") return;
|
|
2375
|
+
const params = new URLSearchParams(window.location.search);
|
|
2376
|
+
const code = params.get("code") || localStorage.getItem("whale_ref_code");
|
|
2377
|
+
if (!code) return;
|
|
2378
|
+
ctx.client.attributeReferral(customer.id, code).then(() => {
|
|
2379
|
+
localStorage.removeItem("whale_ref_code");
|
|
2380
|
+
refresh();
|
|
2381
|
+
}).catch(() => {
|
|
2382
|
+
});
|
|
2383
|
+
}, [customer?.id, status, ctx.client, refresh]);
|
|
2384
|
+
const share = useCallback(async () => {
|
|
2385
|
+
if (!status?.share_url) throw new Error("Not enrolled in referral program");
|
|
2386
|
+
const shareData = {
|
|
2387
|
+
title: "Check this out!",
|
|
2388
|
+
text: `Use my referral code ${status.referral_code} for rewards!`,
|
|
2389
|
+
url: status.share_url
|
|
2390
|
+
};
|
|
2391
|
+
if (typeof navigator !== "undefined" && navigator.share) {
|
|
2392
|
+
try {
|
|
2393
|
+
await navigator.share(shareData);
|
|
2394
|
+
return;
|
|
2395
|
+
} catch {
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
if (typeof navigator !== "undefined" && navigator.clipboard) {
|
|
2399
|
+
await navigator.clipboard.writeText(status.share_url);
|
|
2400
|
+
}
|
|
2401
|
+
}, [status]);
|
|
2402
|
+
return {
|
|
2403
|
+
status,
|
|
2404
|
+
loading,
|
|
2405
|
+
error,
|
|
2406
|
+
enroll,
|
|
2407
|
+
refresh,
|
|
2408
|
+
share,
|
|
2409
|
+
attributeReferral,
|
|
2410
|
+
referralCode: status?.referral_code ?? null,
|
|
2411
|
+
shareUrl: status?.share_url ?? null,
|
|
2412
|
+
isEnrolled: status?.enrolled ?? false,
|
|
2413
|
+
referredBy: status?.referred_by ?? null
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
function trackClick(tracking, label, url, position) {
|
|
2417
|
+
if (!tracking?.gatewayUrl || !tracking?.code) return;
|
|
2418
|
+
const body = JSON.stringify({ label, url, position });
|
|
2419
|
+
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
2420
|
+
navigator.sendBeacon(
|
|
2421
|
+
`${tracking.gatewayUrl}/q/${encodeURIComponent(tracking.code)}/click`,
|
|
2422
|
+
new Blob([body], { type: "application/json" })
|
|
2423
|
+
);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
1209
2426
|
function SectionRenderer({
|
|
1210
2427
|
section,
|
|
1211
2428
|
data,
|
|
1212
|
-
theme
|
|
2429
|
+
theme,
|
|
2430
|
+
tracking
|
|
1213
2431
|
}) {
|
|
1214
2432
|
const [showCOA, setShowCOA] = useState(false);
|
|
1215
2433
|
const el = (() => {
|
|
1216
2434
|
switch (section.type) {
|
|
1217
2435
|
case "hero":
|
|
1218
|
-
return /* @__PURE__ */ jsx(HeroSection, { section, theme });
|
|
2436
|
+
return /* @__PURE__ */ jsx(HeroSection, { section, theme, tracking });
|
|
1219
2437
|
case "text":
|
|
1220
2438
|
return /* @__PURE__ */ jsx(TextSection, { section, theme });
|
|
1221
2439
|
case "image":
|
|
@@ -1225,15 +2443,17 @@ function SectionRenderer({
|
|
|
1225
2443
|
case "gallery":
|
|
1226
2444
|
return /* @__PURE__ */ jsx(GallerySection, { section, theme });
|
|
1227
2445
|
case "cta":
|
|
1228
|
-
return /* @__PURE__ */ jsx(CTASection, { section, theme });
|
|
2446
|
+
return /* @__PURE__ */ jsx(CTASection, { section, theme, tracking });
|
|
1229
2447
|
case "stats":
|
|
1230
2448
|
return /* @__PURE__ */ jsx(StatsSection, { section, theme });
|
|
1231
2449
|
case "product_card":
|
|
1232
|
-
return /* @__PURE__ */ jsx(ProductCardSection, { section, data, theme });
|
|
2450
|
+
return /* @__PURE__ */ jsx(ProductCardSection, { section, data, theme, tracking });
|
|
1233
2451
|
case "coa_viewer":
|
|
1234
|
-
return /* @__PURE__ */ jsx(COAViewerSection, { section, data, theme, onShowCOA: () => setShowCOA(true) });
|
|
2452
|
+
return /* @__PURE__ */ jsx(COAViewerSection, { section, data, theme, onShowCOA: () => setShowCOA(true), tracking });
|
|
1235
2453
|
case "social_links":
|
|
1236
2454
|
return /* @__PURE__ */ jsx(SocialLinksSection, { section, theme });
|
|
2455
|
+
case "lead_capture":
|
|
2456
|
+
return /* @__PURE__ */ jsx(LeadCaptureSection, { section, data, theme });
|
|
1237
2457
|
case "divider":
|
|
1238
2458
|
return /* @__PURE__ */ jsx(DividerSection, { theme });
|
|
1239
2459
|
default:
|
|
@@ -1245,7 +2465,7 @@ function SectionRenderer({
|
|
|
1245
2465
|
showCOA && data?.coa && /* @__PURE__ */ jsx(COAModal, { coa: data.coa, theme, onClose: () => setShowCOA(false) })
|
|
1246
2466
|
] });
|
|
1247
2467
|
}
|
|
1248
|
-
function HeroSection({ section, theme }) {
|
|
2468
|
+
function HeroSection({ section, theme, tracking }) {
|
|
1249
2469
|
const { title, subtitle, background_image, cta_text, cta_url } = section.content;
|
|
1250
2470
|
return /* @__PURE__ */ jsxs(
|
|
1251
2471
|
"div",
|
|
@@ -1287,6 +2507,7 @@ function HeroSection({ section, theme }) {
|
|
|
1287
2507
|
"a",
|
|
1288
2508
|
{
|
|
1289
2509
|
href: cta_url,
|
|
2510
|
+
onClick: () => trackClick(tracking, cta_text, cta_url),
|
|
1290
2511
|
style: {
|
|
1291
2512
|
display: "inline-block",
|
|
1292
2513
|
padding: "0.875rem 2rem",
|
|
@@ -1372,7 +2593,7 @@ function GallerySection({ section, theme }) {
|
|
|
1372
2593
|
}
|
|
1373
2594
|
) }, i)) }) });
|
|
1374
2595
|
}
|
|
1375
|
-
function CTASection({ section, theme }) {
|
|
2596
|
+
function CTASection({ section, theme, tracking }) {
|
|
1376
2597
|
const { buttons } = section.content;
|
|
1377
2598
|
if (!buttons || buttons.length === 0) return null;
|
|
1378
2599
|
return /* @__PURE__ */ jsx("div", { style: { padding: "2rem 1.5rem", maxWidth: 480, margin: "0 auto", display: "flex", flexDirection: "column", gap: "0.75rem" }, children: buttons.map((btn, i) => {
|
|
@@ -1381,6 +2602,7 @@ function CTASection({ section, theme }) {
|
|
|
1381
2602
|
"a",
|
|
1382
2603
|
{
|
|
1383
2604
|
href: btn.url,
|
|
2605
|
+
onClick: () => trackClick(tracking, btn.text, btn.url, i),
|
|
1384
2606
|
style: {
|
|
1385
2607
|
display: "block",
|
|
1386
2608
|
width: "100%",
|
|
@@ -1452,7 +2674,7 @@ function StatsSection({ section, theme }) {
|
|
|
1452
2674
|
}, children: stat.label })
|
|
1453
2675
|
] }, i)) }) });
|
|
1454
2676
|
}
|
|
1455
|
-
function ProductCardSection({ section, data, theme }) {
|
|
2677
|
+
function ProductCardSection({ section, data, theme, tracking }) {
|
|
1456
2678
|
const product = data?.product;
|
|
1457
2679
|
const c = section.content;
|
|
1458
2680
|
const name = c.name || product?.name || "";
|
|
@@ -1468,6 +2690,7 @@ function ProductCardSection({ section, data, theme }) {
|
|
|
1468
2690
|
"a",
|
|
1469
2691
|
{
|
|
1470
2692
|
href: url,
|
|
2693
|
+
onClick: () => trackClick(tracking, "View Product", url),
|
|
1471
2694
|
style: {
|
|
1472
2695
|
display: "block",
|
|
1473
2696
|
width: "100%",
|
|
@@ -1492,7 +2715,8 @@ function COAViewerSection({
|
|
|
1492
2715
|
section,
|
|
1493
2716
|
data,
|
|
1494
2717
|
theme,
|
|
1495
|
-
onShowCOA
|
|
2718
|
+
onShowCOA,
|
|
2719
|
+
tracking
|
|
1496
2720
|
}) {
|
|
1497
2721
|
const coa = data?.coa;
|
|
1498
2722
|
const c = section.content;
|
|
@@ -1513,10 +2737,200 @@ function COAViewerSection({
|
|
|
1513
2737
|
display: "block",
|
|
1514
2738
|
boxSizing: "border-box"
|
|
1515
2739
|
};
|
|
2740
|
+
const buttonLabel = c.button_text || "View Lab Results";
|
|
1516
2741
|
if (coa.viewer_url) {
|
|
1517
|
-
return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsx(
|
|
2742
|
+
return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsx(
|
|
2743
|
+
"a",
|
|
2744
|
+
{
|
|
2745
|
+
href: coa.viewer_url,
|
|
2746
|
+
target: "_blank",
|
|
2747
|
+
rel: "noopener noreferrer",
|
|
2748
|
+
onClick: () => trackClick(tracking, buttonLabel, coa.viewer_url),
|
|
2749
|
+
style: buttonStyle,
|
|
2750
|
+
children: buttonLabel
|
|
2751
|
+
}
|
|
2752
|
+
) });
|
|
1518
2753
|
}
|
|
1519
|
-
return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsx("button", { onClick:
|
|
2754
|
+
return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsx("button", { onClick: () => {
|
|
2755
|
+
trackClick(tracking, buttonLabel, coa.url);
|
|
2756
|
+
onShowCOA();
|
|
2757
|
+
}, style: buttonStyle, children: buttonLabel }) });
|
|
2758
|
+
}
|
|
2759
|
+
function LeadCaptureSection({ section, data, theme }) {
|
|
2760
|
+
const c = section.content;
|
|
2761
|
+
const [firstName, setFirstName] = useState("");
|
|
2762
|
+
const [email, setEmail] = useState("");
|
|
2763
|
+
const [status, setStatus] = useState("idle");
|
|
2764
|
+
const [errorMsg, setErrorMsg] = useState("");
|
|
2765
|
+
const gatewayUrl = c.gateway_url || data.gatewayUrl || "https://whale-gateway.fly.dev";
|
|
2766
|
+
const storeId = c.store_id || data.store?.id;
|
|
2767
|
+
const slug = c.landing_page_slug || data.landing_page?.slug;
|
|
2768
|
+
async function handleSubmit(e) {
|
|
2769
|
+
e.preventDefault();
|
|
2770
|
+
if (!email || !storeId) return;
|
|
2771
|
+
setStatus("loading");
|
|
2772
|
+
setErrorMsg("");
|
|
2773
|
+
try {
|
|
2774
|
+
const res = await fetch(`${gatewayUrl}/v1/stores/${storeId}/storefront/leads`, {
|
|
2775
|
+
method: "POST",
|
|
2776
|
+
headers: { "Content-Type": "application/json" },
|
|
2777
|
+
body: JSON.stringify({
|
|
2778
|
+
email,
|
|
2779
|
+
first_name: firstName || void 0,
|
|
2780
|
+
source: c.source || "landing_page",
|
|
2781
|
+
landing_page_slug: slug || void 0,
|
|
2782
|
+
tags: c.tags || void 0
|
|
2783
|
+
})
|
|
2784
|
+
});
|
|
2785
|
+
if (!res.ok) {
|
|
2786
|
+
const body = await res.json().catch(() => ({}));
|
|
2787
|
+
throw new Error(body?.error?.message || "Something went wrong. Please try again.");
|
|
2788
|
+
}
|
|
2789
|
+
setStatus("success");
|
|
2790
|
+
} catch (err) {
|
|
2791
|
+
setErrorMsg(err instanceof Error ? err.message : "Something went wrong. Please try again.");
|
|
2792
|
+
setStatus("error");
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
const heading = c.heading || "get 10% off your first visit.";
|
|
2796
|
+
const subtitle = c.subtitle || "drop your email and we will send you the code.";
|
|
2797
|
+
const buttonText = c.button_text || "Claim My Discount";
|
|
2798
|
+
const successHeading = c.success_heading || "You\u2019re in!";
|
|
2799
|
+
const successMessage = c.success_message || "Check your inbox for the discount code.";
|
|
2800
|
+
const inputStyle = {
|
|
2801
|
+
flex: 1,
|
|
2802
|
+
minWidth: 0,
|
|
2803
|
+
padding: "0.875rem 1rem",
|
|
2804
|
+
background: theme.surface,
|
|
2805
|
+
border: `1px solid ${theme.fg}15`,
|
|
2806
|
+
color: theme.fg,
|
|
2807
|
+
fontSize: "0.95rem",
|
|
2808
|
+
fontWeight: 300,
|
|
2809
|
+
outline: "none",
|
|
2810
|
+
boxSizing: "border-box",
|
|
2811
|
+
fontFamily: "inherit",
|
|
2812
|
+
transition: "border-color 0.2s"
|
|
2813
|
+
};
|
|
2814
|
+
return /* @__PURE__ */ jsxs("div", { style: { padding: "3.5rem 1.5rem", maxWidth: 560, margin: "0 auto" }, children: [
|
|
2815
|
+
/* @__PURE__ */ jsx("style", { children: `@keyframes lc-spin { to { transform: rotate(360deg) } }` }),
|
|
2816
|
+
/* @__PURE__ */ jsx("div", { style: {
|
|
2817
|
+
background: theme.surface,
|
|
2818
|
+
border: `1px solid ${theme.fg}12`,
|
|
2819
|
+
padding: "clamp(2rem, 6vw, 3rem)"
|
|
2820
|
+
}, children: status === "success" ? /* @__PURE__ */ jsxs("div", { style: { textAlign: "center" }, children: [
|
|
2821
|
+
/* @__PURE__ */ jsx("h2", { style: {
|
|
2822
|
+
fontSize: "clamp(1.5rem, 5vw, 2rem)",
|
|
2823
|
+
fontWeight: 300,
|
|
2824
|
+
fontFamily: theme.fontDisplay || "inherit",
|
|
2825
|
+
margin: "0 0 0.75rem",
|
|
2826
|
+
lineHeight: 1.2,
|
|
2827
|
+
letterSpacing: "-0.02em",
|
|
2828
|
+
color: theme.fg
|
|
2829
|
+
}, children: successHeading }),
|
|
2830
|
+
/* @__PURE__ */ jsx("p", { style: {
|
|
2831
|
+
fontSize: "0.9rem",
|
|
2832
|
+
color: `${theme.fg}99`,
|
|
2833
|
+
margin: "0 0 1.5rem",
|
|
2834
|
+
lineHeight: 1.6,
|
|
2835
|
+
fontWeight: 300
|
|
2836
|
+
}, children: successMessage }),
|
|
2837
|
+
c.coupon_code && /* @__PURE__ */ jsx("div", { style: {
|
|
2838
|
+
display: "inline-block",
|
|
2839
|
+
padding: "0.75rem 2rem",
|
|
2840
|
+
background: `${theme.fg}08`,
|
|
2841
|
+
border: `1px dashed ${theme.fg}30`,
|
|
2842
|
+
fontSize: "clamp(1.25rem, 4vw, 1.75rem)",
|
|
2843
|
+
fontWeight: 500,
|
|
2844
|
+
fontFamily: "monospace",
|
|
2845
|
+
letterSpacing: "0.12em",
|
|
2846
|
+
color: theme.accent
|
|
2847
|
+
}, children: c.coupon_code })
|
|
2848
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2849
|
+
/* @__PURE__ */ jsxs("div", { style: { textAlign: "center", marginBottom: "clamp(1.5rem, 4vw, 2rem)" }, children: [
|
|
2850
|
+
/* @__PURE__ */ jsx("h2", { style: {
|
|
2851
|
+
fontSize: "clamp(1.5rem, 5vw, 2.25rem)",
|
|
2852
|
+
fontWeight: 300,
|
|
2853
|
+
fontFamily: theme.fontDisplay || "inherit",
|
|
2854
|
+
margin: "0 0 0.5rem",
|
|
2855
|
+
lineHeight: 1.15,
|
|
2856
|
+
letterSpacing: "-0.02em",
|
|
2857
|
+
color: theme.fg
|
|
2858
|
+
}, children: heading }),
|
|
2859
|
+
/* @__PURE__ */ jsx("p", { style: {
|
|
2860
|
+
fontSize: "0.85rem",
|
|
2861
|
+
color: theme.accent,
|
|
2862
|
+
margin: 0,
|
|
2863
|
+
lineHeight: 1.6,
|
|
2864
|
+
textTransform: "uppercase",
|
|
2865
|
+
letterSpacing: "0.15em"
|
|
2866
|
+
}, children: subtitle })
|
|
2867
|
+
] }),
|
|
2868
|
+
/* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, style: { display: "flex", flexDirection: "column", gap: "0.75rem" }, children: [
|
|
2869
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "0.75rem", flexWrap: "wrap" }, children: [
|
|
2870
|
+
/* @__PURE__ */ jsx(
|
|
2871
|
+
"input",
|
|
2872
|
+
{
|
|
2873
|
+
type: "text",
|
|
2874
|
+
placeholder: "First name",
|
|
2875
|
+
value: firstName,
|
|
2876
|
+
onChange: (e) => setFirstName(e.target.value),
|
|
2877
|
+
style: inputStyle
|
|
2878
|
+
}
|
|
2879
|
+
),
|
|
2880
|
+
/* @__PURE__ */ jsx(
|
|
2881
|
+
"input",
|
|
2882
|
+
{
|
|
2883
|
+
type: "email",
|
|
2884
|
+
placeholder: "Email address",
|
|
2885
|
+
value: email,
|
|
2886
|
+
onChange: (e) => setEmail(e.target.value),
|
|
2887
|
+
required: true,
|
|
2888
|
+
style: inputStyle
|
|
2889
|
+
}
|
|
2890
|
+
)
|
|
2891
|
+
] }),
|
|
2892
|
+
status === "error" && errorMsg && /* @__PURE__ */ jsx("p", { style: { fontSize: "0.8rem", color: "#e55", margin: 0, fontWeight: 400 }, children: errorMsg }),
|
|
2893
|
+
/* @__PURE__ */ jsxs(
|
|
2894
|
+
"button",
|
|
2895
|
+
{
|
|
2896
|
+
type: "submit",
|
|
2897
|
+
disabled: status === "loading",
|
|
2898
|
+
style: {
|
|
2899
|
+
width: "100%",
|
|
2900
|
+
padding: "0.875rem",
|
|
2901
|
+
background: theme.fg,
|
|
2902
|
+
color: theme.bg,
|
|
2903
|
+
border: "none",
|
|
2904
|
+
fontSize: "0.85rem",
|
|
2905
|
+
fontWeight: 500,
|
|
2906
|
+
cursor: status === "loading" ? "wait" : "pointer",
|
|
2907
|
+
letterSpacing: "0.08em",
|
|
2908
|
+
textTransform: "uppercase",
|
|
2909
|
+
fontFamily: "inherit",
|
|
2910
|
+
display: "flex",
|
|
2911
|
+
alignItems: "center",
|
|
2912
|
+
justifyContent: "center",
|
|
2913
|
+
gap: "0.5rem",
|
|
2914
|
+
opacity: status === "loading" ? 0.7 : 1,
|
|
2915
|
+
transition: "opacity 0.2s"
|
|
2916
|
+
},
|
|
2917
|
+
children: [
|
|
2918
|
+
status === "loading" && /* @__PURE__ */ jsx("span", { style: {
|
|
2919
|
+
display: "inline-block",
|
|
2920
|
+
width: 16,
|
|
2921
|
+
height: 16,
|
|
2922
|
+
border: `2px solid ${theme.bg}40`,
|
|
2923
|
+
borderTopColor: theme.bg,
|
|
2924
|
+
borderRadius: "50%",
|
|
2925
|
+
animation: "lc-spin 0.8s linear infinite"
|
|
2926
|
+
} }),
|
|
2927
|
+
buttonText
|
|
2928
|
+
]
|
|
2929
|
+
}
|
|
2930
|
+
)
|
|
2931
|
+
] })
|
|
2932
|
+
] }) })
|
|
2933
|
+
] });
|
|
1520
2934
|
}
|
|
1521
2935
|
function SocialLinksSection({ section, theme }) {
|
|
1522
2936
|
const { links } = section.content;
|
|
@@ -1639,15 +3053,16 @@ function QRLandingPage({
|
|
|
1639
3053
|
const logoUrl = data.qr_code.logo_url || data.store?.logo_url;
|
|
1640
3054
|
const storeName = data.store?.name;
|
|
1641
3055
|
const sorted = [...sections].sort((a, b) => a.order - b.order);
|
|
3056
|
+
const tracking = { gatewayUrl, code };
|
|
1642
3057
|
return /* @__PURE__ */ jsxs("div", { style: { minHeight: "100dvh", background: theme.bg, color: theme.fg, fontFamily }, children: [
|
|
1643
3058
|
lp?.custom_css && /* @__PURE__ */ jsx("style", { children: lp.custom_css }),
|
|
1644
3059
|
logoUrl && /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsx("img", { src: logoUrl, alt: storeName || "Store", style: { height: 40, objectFit: "contain" } }) }),
|
|
1645
3060
|
sorted.map((section) => {
|
|
1646
|
-
const defaultRenderer = () => /* @__PURE__ */ jsx(SectionRenderer, { section, data, theme }, section.id);
|
|
3061
|
+
const defaultRenderer = () => /* @__PURE__ */ jsx(SectionRenderer, { section, data, theme, tracking }, section.id);
|
|
1647
3062
|
if (renderSection) {
|
|
1648
3063
|
return /* @__PURE__ */ jsx("div", { children: renderSection(section, defaultRenderer) }, section.id);
|
|
1649
3064
|
}
|
|
1650
|
-
return /* @__PURE__ */ jsx(SectionRenderer, { section, data, theme }, section.id);
|
|
3065
|
+
return /* @__PURE__ */ jsx(SectionRenderer, { section, data, theme, tracking }, section.id);
|
|
1651
3066
|
}),
|
|
1652
3067
|
storeName && /* @__PURE__ */ jsx("div", { style: { padding: "2rem 1.5rem", borderTop: `1px solid ${theme.surface}`, textAlign: "center" }, children: /* @__PURE__ */ jsxs("p", { style: { fontSize: "0.75rem", color: theme.muted, margin: 0 }, children: [
|
|
1653
3068
|
storeName,
|
|
@@ -1675,10 +3090,10 @@ function buildDefaultSections(data) {
|
|
|
1675
3090
|
let order = 0;
|
|
1676
3091
|
const productName = qr.landing_page.title || product?.name || qr.name;
|
|
1677
3092
|
const productImage = qr.landing_page.image_url || product?.featured_image || null;
|
|
1678
|
-
const description =
|
|
1679
|
-
const ctaUrl = qr.landing_page.cta_url || qr.destination_url;
|
|
3093
|
+
const description = product?.description || "";
|
|
1680
3094
|
const categoryName = product?.category_name ?? null;
|
|
1681
3095
|
const strainType = toStr(cf?.strain_type);
|
|
3096
|
+
const tagline = toStr(cf?.tagline);
|
|
1682
3097
|
if (productImage) {
|
|
1683
3098
|
sections.push({
|
|
1684
3099
|
id: "auto-hero",
|
|
@@ -1687,9 +3102,7 @@ function buildDefaultSections(data) {
|
|
|
1687
3102
|
content: {
|
|
1688
3103
|
title: productName,
|
|
1689
3104
|
subtitle: [categoryName, strainType].filter(Boolean).join(" \xB7 "),
|
|
1690
|
-
background_image: productImage
|
|
1691
|
-
cta_text: qr.landing_page.cta_text || null,
|
|
1692
|
-
cta_url: ctaUrl
|
|
3105
|
+
background_image: productImage
|
|
1693
3106
|
}
|
|
1694
3107
|
});
|
|
1695
3108
|
} else {
|
|
@@ -1704,6 +3117,15 @@ function buildDefaultSections(data) {
|
|
|
1704
3117
|
config: { align: "center" }
|
|
1705
3118
|
});
|
|
1706
3119
|
}
|
|
3120
|
+
if (tagline) {
|
|
3121
|
+
sections.push({
|
|
3122
|
+
id: "auto-tagline",
|
|
3123
|
+
type: "text",
|
|
3124
|
+
order: order++,
|
|
3125
|
+
content: { body: tagline },
|
|
3126
|
+
config: { align: "center" }
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
1707
3129
|
const thca = toNum(cf?.thca_percentage);
|
|
1708
3130
|
const thc = toNum(cf?.d9_percentage);
|
|
1709
3131
|
const cbd = toNum(cf?.cbd_total);
|
|
@@ -1719,23 +3141,23 @@ function buildDefaultSections(data) {
|
|
|
1719
3141
|
content: { stats }
|
|
1720
3142
|
});
|
|
1721
3143
|
}
|
|
1722
|
-
const
|
|
3144
|
+
const profileDetails = [];
|
|
1723
3145
|
const genetics = toStr(cf?.genetics);
|
|
1724
3146
|
const terpenes = toStr(cf?.terpenes);
|
|
1725
3147
|
const effects = toStr(cf?.effects);
|
|
1726
|
-
const
|
|
1727
|
-
const
|
|
1728
|
-
if (genetics)
|
|
1729
|
-
if (terpenes)
|
|
1730
|
-
if (effects)
|
|
1731
|
-
if (
|
|
1732
|
-
if (
|
|
1733
|
-
if (
|
|
3148
|
+
const flavorProfile = toStr(cf?.flavor_profile);
|
|
3149
|
+
const bestFor = toStr(cf?.best_for);
|
|
3150
|
+
if (genetics) profileDetails.push({ label: "Genetics", value: genetics });
|
|
3151
|
+
if (terpenes) profileDetails.push({ label: "Terpenes", value: terpenes });
|
|
3152
|
+
if (effects) profileDetails.push({ label: "Effects", value: effects });
|
|
3153
|
+
if (flavorProfile) profileDetails.push({ label: "Flavor", value: flavorProfile });
|
|
3154
|
+
if (bestFor) profileDetails.push({ label: "Best For", value: bestFor });
|
|
3155
|
+
if (profileDetails.length > 0) {
|
|
1734
3156
|
sections.push({
|
|
1735
|
-
id: "auto-
|
|
3157
|
+
id: "auto-profile",
|
|
1736
3158
|
type: "stats",
|
|
1737
3159
|
order: order++,
|
|
1738
|
-
content: { stats:
|
|
3160
|
+
content: { stats: profileDetails },
|
|
1739
3161
|
config: { layout: "list" }
|
|
1740
3162
|
});
|
|
1741
3163
|
}
|
|
@@ -1755,16 +3177,34 @@ function buildDefaultSections(data) {
|
|
|
1755
3177
|
content: { button_text: "View Lab Results" }
|
|
1756
3178
|
});
|
|
1757
3179
|
}
|
|
1758
|
-
|
|
3180
|
+
const labDetails = [];
|
|
3181
|
+
const batchNumber = toStr(cf?.batch_number);
|
|
3182
|
+
const dateTested = toStr(cf?.date_tested);
|
|
3183
|
+
if (batchNumber) labDetails.push({ label: "Batch", value: batchNumber });
|
|
3184
|
+
if (dateTested) labDetails.push({ label: "Tested", value: formatDate(dateTested) });
|
|
3185
|
+
if (labDetails.length > 0) {
|
|
1759
3186
|
sections.push({
|
|
1760
|
-
id: "auto-
|
|
1761
|
-
type: "
|
|
3187
|
+
id: "auto-lab-info",
|
|
3188
|
+
type: "stats",
|
|
1762
3189
|
order: order++,
|
|
1763
|
-
content: {
|
|
1764
|
-
|
|
1765
|
-
}
|
|
3190
|
+
content: { stats: labDetails },
|
|
3191
|
+
config: { layout: "list" }
|
|
1766
3192
|
});
|
|
1767
3193
|
}
|
|
3194
|
+
const productSlug = product?.slug;
|
|
3195
|
+
if (productSlug) {
|
|
3196
|
+
const storeDomain = data.store?.name === "Flora Distro" ? "floradistro.com" : null;
|
|
3197
|
+
if (storeDomain) {
|
|
3198
|
+
sections.push({
|
|
3199
|
+
id: "auto-shop",
|
|
3200
|
+
type: "cta",
|
|
3201
|
+
order: order++,
|
|
3202
|
+
content: {
|
|
3203
|
+
buttons: [{ text: "Shop This Product", url: `https://${storeDomain}/shop/${productSlug}`, style: "outline" }]
|
|
3204
|
+
}
|
|
3205
|
+
});
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
1768
3208
|
return sections;
|
|
1769
3209
|
}
|
|
1770
3210
|
function toNum(v) {
|
|
@@ -1872,10 +3312,11 @@ function LandingPage({
|
|
|
1872
3312
|
if (state === "expired") return /* @__PURE__ */ jsx(DefaultExpired2, {});
|
|
1873
3313
|
if (state === "error") return /* @__PURE__ */ jsx(DefaultError2, { message: errorMsg });
|
|
1874
3314
|
if (!data) return null;
|
|
1875
|
-
return /* @__PURE__ */ jsx(PageLayout, { data, renderSection });
|
|
3315
|
+
return /* @__PURE__ */ jsx(PageLayout, { data, gatewayUrl, renderSection });
|
|
1876
3316
|
}
|
|
1877
3317
|
function PageLayout({
|
|
1878
3318
|
data,
|
|
3319
|
+
gatewayUrl,
|
|
1879
3320
|
renderSection
|
|
1880
3321
|
}) {
|
|
1881
3322
|
const { landing_page: lp, store } = data;
|
|
@@ -1891,15 +3332,16 @@ function PageLayout({
|
|
|
1891
3332
|
const fontFamily = lp.font_family || theme.fontDisplay || "system-ui, -apple-system, sans-serif";
|
|
1892
3333
|
const logoUrl = store?.logo_url;
|
|
1893
3334
|
const sorted = [...lp.sections].sort((a, b) => a.order - b.order);
|
|
3335
|
+
const sectionData = { ...data, gatewayUrl, landing_page: { slug: lp.slug } };
|
|
1894
3336
|
return /* @__PURE__ */ jsxs("div", { style: { minHeight: "100dvh", background: theme.bg, color: theme.fg, fontFamily }, children: [
|
|
1895
3337
|
lp.custom_css && /* @__PURE__ */ jsx("style", { children: lp.custom_css }),
|
|
1896
3338
|
logoUrl && /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsx("img", { src: logoUrl, alt: store?.name || "Store", style: { height: 40, objectFit: "contain" } }) }),
|
|
1897
3339
|
sorted.map((section) => {
|
|
1898
|
-
const defaultRenderer = () => /* @__PURE__ */ jsx(SectionRenderer, { section, data, theme }, section.id);
|
|
3340
|
+
const defaultRenderer = () => /* @__PURE__ */ jsx(SectionRenderer, { section, data: sectionData, theme }, section.id);
|
|
1899
3341
|
if (renderSection) {
|
|
1900
3342
|
return /* @__PURE__ */ jsx("div", { children: renderSection(section, defaultRenderer) }, section.id);
|
|
1901
3343
|
}
|
|
1902
|
-
return /* @__PURE__ */ jsx(SectionRenderer, { section, data, theme }, section.id);
|
|
3344
|
+
return /* @__PURE__ */ jsx(SectionRenderer, { section, data: sectionData, theme }, section.id);
|
|
1903
3345
|
}),
|
|
1904
3346
|
store?.name && /* @__PURE__ */ jsx("div", { style: { padding: "2rem 1.5rem", borderTop: `1px solid ${theme.surface}`, textAlign: "center" }, children: /* @__PURE__ */ jsxs("p", { style: { fontSize: "0.75rem", color: theme.muted, margin: 0 }, children: [
|
|
1905
3347
|
"Powered by ",
|
|
@@ -1943,6 +3385,6 @@ function DefaultError2({ message }) {
|
|
|
1943
3385
|
] }) });
|
|
1944
3386
|
}
|
|
1945
3387
|
|
|
1946
|
-
export { AnalyticsTracker, AuthInitializer, CartInitializer, LandingPage, PixelInitializer, QRLandingPage, SectionRenderer, WhaleContext, WhaleProvider, useAnalytics, useAuth, useCart, useCartItemCount, useCartTotal, useCategories, useCheckout, useCoupons, useCustomerAnalytics, useCustomerOrders, useLocations, useLoyalty, useProduct, useProducts, useRecommendations, useReviews, useSearch, useShipping, useWhaleClient, useWishlist };
|
|
3388
|
+
export { AnalyticsTracker, AuthInitializer, BehavioralTrackerComponent, CartInitializer, FingerprintCollector, LandingPage, PixelInitializer, QRLandingPage, SectionRenderer, SessionRecorderComponent, WhaleContext, WhaleProvider, useAnalytics, useAuth, useCart, useCartItemCount, useCartTotal, useCategories, useCheckout, useCoupons, useCustomerAnalytics, useCustomerOrders, useDeals, useLocations, useLoyalty, useProduct, useProducts, useRecommendations, useReferral, useReviews, useSearch, useShipping, useWhaleClient, useWishlist };
|
|
1947
3389
|
//# sourceMappingURL=index.js.map
|
|
1948
3390
|
//# sourceMappingURL=index.js.map
|