@neowhale/storefront 0.2.13 → 0.2.19

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