@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,5 +1,5 @@
1
- import { PixelManager } from '../chunk-NLH3W6JA.js';
2
- import { WhaleClient } from '../chunk-M2MR6C55.js';
1
+ import { PixelManager } from '../chunk-PXS2DPVL.js';
2
+ import { resilientSend, WhaleClient } from '../chunk-XHWAUMWS.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
- trackPageView(pathname, referrer || void 0);
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 cartStore = createCartStore(client, resolvedConfig.storagePrefix);
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
  }
@@ -1149,9 +2246,9 @@ function useShipping() {
1149
2246
  }, [ctx.client]);
1150
2247
  return { methods, rates, loading, error, refreshMethods, calculateRates };
1151
2248
  }
1152
- function useCoupons() {
2249
+ function useDeals() {
1153
2250
  const ctx = useContext(WhaleContext);
1154
- if (!ctx) throw new Error("useCoupons must be used within <WhaleProvider>");
2251
+ if (!ctx) throw new Error("useDeals must be used within <WhaleProvider>");
1155
2252
  const [validation, setValidation] = useState(null);
1156
2253
  const [loading, setLoading] = useState(false);
1157
2254
  const [error, setError] = useState(null);
@@ -1159,7 +2256,7 @@ function useCoupons() {
1159
2256
  setLoading(true);
1160
2257
  setError(null);
1161
2258
  try {
1162
- const result = await ctx.client.validateCoupon(code, cartId ? { cart_id: cartId } : void 0);
2259
+ const result = await ctx.client.validateDeal(code, cartId ? { cart_id: cartId } : void 0);
1163
2260
  setValidation(result);
1164
2261
  return result;
1165
2262
  } catch (err) {
@@ -1175,7 +2272,7 @@ function useCoupons() {
1175
2272
  setLoading(true);
1176
2273
  setError(null);
1177
2274
  try {
1178
- const cart = await ctx.client.applyCoupon(cartId, code);
2275
+ const cart = await ctx.client.applyDeal(cartId, code);
1179
2276
  return cart;
1180
2277
  } catch (err) {
1181
2278
  const e = err instanceof Error ? err : new Error(String(err));
@@ -1189,7 +2286,7 @@ function useCoupons() {
1189
2286
  setLoading(true);
1190
2287
  setError(null);
1191
2288
  try {
1192
- const cart = await ctx.client.removeCoupon(cartId);
2289
+ const cart = await ctx.client.removeDeal(cartId);
1193
2290
  setValidation(null);
1194
2291
  return cart;
1195
2292
  } catch (err) {
@@ -1206,6 +2303,112 @@ function useCoupons() {
1206
2303
  }, []);
1207
2304
  return { validation, loading, error, validate, apply, remove, clear };
1208
2305
  }
2306
+ var useCoupons = useDeals;
2307
+ function useReferral() {
2308
+ const ctx = useContext(WhaleContext);
2309
+ if (!ctx) throw new Error("useReferral must be used within <WhaleProvider>");
2310
+ const customer = useStore(ctx.authStore, (s) => s.customer);
2311
+ const [status, setStatus] = useState(null);
2312
+ const [loading, setLoading] = useState(false);
2313
+ const [error, setError] = useState(null);
2314
+ const refresh = useCallback(async () => {
2315
+ if (!customer?.id) {
2316
+ setStatus(null);
2317
+ return;
2318
+ }
2319
+ setLoading(true);
2320
+ setError(null);
2321
+ try {
2322
+ const result = await ctx.client.getReferralStatus(customer.id);
2323
+ setStatus(result);
2324
+ } catch (err) {
2325
+ setError(err instanceof Error ? err : new Error(String(err)));
2326
+ } finally {
2327
+ setLoading(false);
2328
+ }
2329
+ }, [customer?.id, ctx.client]);
2330
+ useEffect(() => {
2331
+ refresh();
2332
+ }, [refresh]);
2333
+ const enroll = useCallback(async () => {
2334
+ if (!customer?.id) throw new Error("Not authenticated");
2335
+ setLoading(true);
2336
+ setError(null);
2337
+ try {
2338
+ const result = await ctx.client.enrollReferral(customer.id);
2339
+ await refresh();
2340
+ return result;
2341
+ } catch (err) {
2342
+ const e = err instanceof Error ? err : new Error(String(err));
2343
+ setError(e);
2344
+ throw e;
2345
+ } finally {
2346
+ setLoading(false);
2347
+ }
2348
+ }, [customer?.id, ctx.client, refresh]);
2349
+ const attributeReferral = useCallback(
2350
+ async (code) => {
2351
+ if (!customer?.id) throw new Error("Not authenticated");
2352
+ setLoading(true);
2353
+ setError(null);
2354
+ try {
2355
+ const result = await ctx.client.attributeReferral(customer.id, code);
2356
+ await refresh();
2357
+ return result;
2358
+ } catch (err) {
2359
+ const e = err instanceof Error ? err : new Error(String(err));
2360
+ setError(e);
2361
+ throw e;
2362
+ } finally {
2363
+ setLoading(false);
2364
+ }
2365
+ },
2366
+ [customer?.id, ctx.client, refresh]
2367
+ );
2368
+ useEffect(() => {
2369
+ if (!customer?.id || !status || status.referred_by) return;
2370
+ if (typeof window === "undefined") return;
2371
+ const params = new URLSearchParams(window.location.search);
2372
+ const code = params.get("code") || localStorage.getItem("whale_ref_code");
2373
+ if (!code) return;
2374
+ ctx.client.attributeReferral(customer.id, code).then(() => {
2375
+ localStorage.removeItem("whale_ref_code");
2376
+ refresh();
2377
+ }).catch(() => {
2378
+ });
2379
+ }, [customer?.id, status, ctx.client, refresh]);
2380
+ const share = useCallback(async () => {
2381
+ if (!status?.share_url) throw new Error("Not enrolled in referral program");
2382
+ const shareData = {
2383
+ title: "Check this out!",
2384
+ text: `Use my referral code ${status.referral_code} for rewards!`,
2385
+ url: status.share_url
2386
+ };
2387
+ if (typeof navigator !== "undefined" && navigator.share) {
2388
+ try {
2389
+ await navigator.share(shareData);
2390
+ return;
2391
+ } catch {
2392
+ }
2393
+ }
2394
+ if (typeof navigator !== "undefined" && navigator.clipboard) {
2395
+ await navigator.clipboard.writeText(status.share_url);
2396
+ }
2397
+ }, [status]);
2398
+ return {
2399
+ status,
2400
+ loading,
2401
+ error,
2402
+ enroll,
2403
+ refresh,
2404
+ share,
2405
+ attributeReferral,
2406
+ referralCode: status?.referral_code ?? null,
2407
+ shareUrl: status?.share_url ?? null,
2408
+ isEnrolled: status?.enrolled ?? false,
2409
+ referredBy: status?.referred_by ?? null
2410
+ };
2411
+ }
1209
2412
  function SectionRenderer({
1210
2413
  section,
1211
2414
  data,
@@ -1675,10 +2878,10 @@ function buildDefaultSections(data) {
1675
2878
  let order = 0;
1676
2879
  const productName = qr.landing_page.title || product?.name || qr.name;
1677
2880
  const productImage = qr.landing_page.image_url || product?.featured_image || null;
1678
- const description = qr.landing_page.description || product?.description || "";
1679
- const ctaUrl = qr.landing_page.cta_url || qr.destination_url;
2881
+ const description = product?.description || "";
1680
2882
  const categoryName = product?.category_name ?? null;
1681
2883
  const strainType = toStr(cf?.strain_type);
2884
+ const tagline = toStr(cf?.tagline);
1682
2885
  if (productImage) {
1683
2886
  sections.push({
1684
2887
  id: "auto-hero",
@@ -1687,9 +2890,7 @@ function buildDefaultSections(data) {
1687
2890
  content: {
1688
2891
  title: productName,
1689
2892
  subtitle: [categoryName, strainType].filter(Boolean).join(" \xB7 "),
1690
- background_image: productImage,
1691
- cta_text: qr.landing_page.cta_text || null,
1692
- cta_url: ctaUrl
2893
+ background_image: productImage
1693
2894
  }
1694
2895
  });
1695
2896
  } else {
@@ -1704,6 +2905,15 @@ function buildDefaultSections(data) {
1704
2905
  config: { align: "center" }
1705
2906
  });
1706
2907
  }
2908
+ if (tagline) {
2909
+ sections.push({
2910
+ id: "auto-tagline",
2911
+ type: "text",
2912
+ order: order++,
2913
+ content: { body: tagline },
2914
+ config: { align: "center" }
2915
+ });
2916
+ }
1707
2917
  const thca = toNum(cf?.thca_percentage);
1708
2918
  const thc = toNum(cf?.d9_percentage);
1709
2919
  const cbd = toNum(cf?.cbd_total);
@@ -1719,23 +2929,23 @@ function buildDefaultSections(data) {
1719
2929
  content: { stats }
1720
2930
  });
1721
2931
  }
1722
- const details = [];
2932
+ const profileDetails = [];
1723
2933
  const genetics = toStr(cf?.genetics);
1724
2934
  const terpenes = toStr(cf?.terpenes);
1725
2935
  const effects = toStr(cf?.effects);
1726
- const batchNumber = toStr(cf?.batch_number);
1727
- const dateTested = toStr(cf?.date_tested);
1728
- if (genetics) details.push({ label: "Genetics", value: genetics });
1729
- if (terpenes) details.push({ label: "Terpenes", value: terpenes });
1730
- if (effects) details.push({ label: "Effects", value: effects });
1731
- if (batchNumber) details.push({ label: "Batch", value: batchNumber });
1732
- if (dateTested) details.push({ label: "Tested", value: formatDate(dateTested) });
1733
- if (details.length > 0) {
2936
+ const flavorProfile = toStr(cf?.flavor_profile);
2937
+ const bestFor = toStr(cf?.best_for);
2938
+ if (genetics) profileDetails.push({ label: "Genetics", value: genetics });
2939
+ if (terpenes) profileDetails.push({ label: "Terpenes", value: terpenes });
2940
+ if (effects) profileDetails.push({ label: "Effects", value: effects });
2941
+ if (flavorProfile) profileDetails.push({ label: "Flavor", value: flavorProfile });
2942
+ if (bestFor) profileDetails.push({ label: "Best For", value: bestFor });
2943
+ if (profileDetails.length > 0) {
1734
2944
  sections.push({
1735
- id: "auto-details",
2945
+ id: "auto-profile",
1736
2946
  type: "stats",
1737
2947
  order: order++,
1738
- content: { stats: details },
2948
+ content: { stats: profileDetails },
1739
2949
  config: { layout: "list" }
1740
2950
  });
1741
2951
  }
@@ -1755,16 +2965,34 @@ function buildDefaultSections(data) {
1755
2965
  content: { button_text: "View Lab Results" }
1756
2966
  });
1757
2967
  }
1758
- if (ctaUrl) {
2968
+ const labDetails = [];
2969
+ const batchNumber = toStr(cf?.batch_number);
2970
+ const dateTested = toStr(cf?.date_tested);
2971
+ if (batchNumber) labDetails.push({ label: "Batch", value: batchNumber });
2972
+ if (dateTested) labDetails.push({ label: "Tested", value: formatDate(dateTested) });
2973
+ if (labDetails.length > 0) {
1759
2974
  sections.push({
1760
- id: "auto-cta",
1761
- type: "cta",
2975
+ id: "auto-lab-info",
2976
+ type: "stats",
1762
2977
  order: order++,
1763
- content: {
1764
- buttons: [{ text: qr.landing_page.cta_text || "Shop Online", url: ctaUrl, style: "primary" }]
1765
- }
2978
+ content: { stats: labDetails },
2979
+ config: { layout: "list" }
1766
2980
  });
1767
2981
  }
2982
+ const productSlug = product?.slug;
2983
+ if (productSlug) {
2984
+ const storeDomain = data.store?.name === "Flora Distro" ? "floradistro.com" : null;
2985
+ if (storeDomain) {
2986
+ sections.push({
2987
+ id: "auto-shop",
2988
+ type: "cta",
2989
+ order: order++,
2990
+ content: {
2991
+ buttons: [{ text: "Shop This Product", url: `https://${storeDomain}/shop/${productSlug}`, style: "outline" }]
2992
+ }
2993
+ });
2994
+ }
2995
+ }
1768
2996
  return sections;
1769
2997
  }
1770
2998
  function toNum(v) {
@@ -1943,6 +3171,6 @@ function DefaultError2({ message }) {
1943
3171
  ] }) });
1944
3172
  }
1945
3173
 
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 };
3174
+ 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
3175
  //# sourceMappingURL=index.js.map
1948
3176
  //# sourceMappingURL=index.js.map