@neowhale/storefront 0.2.13 → 0.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/{chunk-M2MR6C55.js → chunk-3Q7CPJBA.js} +68 -18
  2. package/dist/chunk-3Q7CPJBA.js.map +1 -0
  3. package/dist/chunk-7KXJLHGA.cjs +160 -0
  4. package/dist/chunk-7KXJLHGA.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-3VKRKDPL.cjs → chunk-VAA2KKCH.cjs} +68 -18
  8. package/dist/chunk-VAA2KKCH.cjs.map +1 -0
  9. package/dist/{client-Ca8Otk-R.d.cts → client-BSO263Uv.d.cts} +91 -6
  10. package/dist/{client-Ca8Otk-R.d.ts → client-BSO263Uv.d.ts} +91 -6
  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-BcL95odX.d.ts} +1 -1
  22. package/dist/{pixel-manager-CIR16DXY.d.cts → pixel-manager-DJ9m2FaQ.d.cts} +1 -1
  23. package/dist/react/index.cjs +1503 -56
  24. package/dist/react/index.cjs.map +1 -1
  25. package/dist/react/index.d.cts +71 -9
  26. package/dist/react/index.d.ts +71 -9
  27. package/dist/react/index.js +1497 -55
  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 chunkVAA2KKCH_cjs = require('../chunk-VAA2KKCH.cjs');
5
5
  var react = require('react');
6
6
  var navigation = require('next/navigation');
7
7
  var vanilla = require('zustand/vanilla');
@@ -12,7 +12,7 @@ var ui = require('@neowhale/ui');
12
12
  var jsxRuntime = require('react/jsx-runtime');
13
13
 
14
14
  var WhaleContext = react.createContext(null);
15
- function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
15
+ function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart, onCartChange) {
16
16
  return vanilla.createStore()(
17
17
  middleware.persist(
18
18
  (set, get) => ({
@@ -100,6 +100,8 @@ function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
100
100
  }
101
101
  await get().syncCart();
102
102
  onAddToCart?.(productId, productName || "", quantity, unitPrice || 0, tier);
103
+ const state = get();
104
+ if (state.cartId) onCartChange?.(state.cartId, state.total, state.itemCount);
103
105
  } finally {
104
106
  set({ cartLoading: false, addItemInFlight: false });
105
107
  }
@@ -111,6 +113,8 @@ function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
111
113
  if (!cartId) return;
112
114
  await client.updateCartItem(cartId, itemId, quantity);
113
115
  await get().syncCart();
116
+ const state = get();
117
+ if (state.cartId) onCartChange?.(state.cartId, state.total, state.itemCount);
114
118
  } finally {
115
119
  set({ cartLoading: false });
116
120
  }
@@ -126,6 +130,8 @@ function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
126
130
  if (item) {
127
131
  onRemoveFromCart?.(item.product_id, productName || item.product_name);
128
132
  }
133
+ const state = get();
134
+ if (state.cartId) onCartChange?.(state.cartId, state.total, state.itemCount);
129
135
  } finally {
130
136
  set({ cartLoading: false });
131
137
  }
@@ -239,6 +245,12 @@ function createAuthStore(client, storagePrefix) {
239
245
  set({ authLoading: false });
240
246
  }
241
247
  },
248
+ updateProfile: async (data) => {
249
+ const customer = get().customer;
250
+ if (!customer?.id) throw new Error("Not authenticated");
251
+ const updated = await client.updateProfile(customer.id, data);
252
+ set({ customer: updated });
253
+ },
242
254
  restoreSession: async () => {
243
255
  const { sessionToken, sessionExpiresAt, customer } = get();
244
256
  if (!sessionToken || !sessionExpiresAt) return;
@@ -299,6 +311,38 @@ function createAuthStore(client, storagePrefix) {
299
311
  );
300
312
  }
301
313
  var SESSION_KEY_SUFFIX = "-analytics-session";
314
+ var VISITOR_KEY_SUFFIX = "-visitor-id";
315
+ function parseMarketingParams() {
316
+ if (typeof window === "undefined") return {};
317
+ const params = new URLSearchParams(window.location.search);
318
+ const result = {};
319
+ for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term", "gclid", "fbclid"]) {
320
+ const val = params.get(key);
321
+ if (val) result[key] = val;
322
+ }
323
+ return result;
324
+ }
325
+ function getVisitorId(prefix) {
326
+ const key = `${prefix}${VISITOR_KEY_SUFFIX}`;
327
+ try {
328
+ const existing = localStorage.getItem(key);
329
+ if (existing) return existing;
330
+ } catch {
331
+ }
332
+ const id = `v-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
333
+ try {
334
+ localStorage.setItem(key, id);
335
+ } catch {
336
+ }
337
+ return id;
338
+ }
339
+ function detectDevice() {
340
+ if (typeof navigator === "undefined") return "unknown";
341
+ const ua = navigator.userAgent;
342
+ if (/Mobi|Android/i.test(ua)) return "mobile";
343
+ if (/Tablet|iPad/i.test(ua)) return "tablet";
344
+ return "desktop";
345
+ }
302
346
  function useAnalytics() {
303
347
  const ctx = react.useContext(WhaleContext);
304
348
  if (!ctx) throw new Error("useAnalytics must be used within <WhaleProvider>");
@@ -321,9 +365,15 @@ function useAnalytics() {
321
365
  } catch {
322
366
  }
323
367
  try {
368
+ const marketing = parseMarketingParams();
369
+ const visitorId = getVisitorId(config.storagePrefix);
324
370
  const session = await client.createSession({
371
+ visitor_id: visitorId,
325
372
  user_agent: navigator.userAgent,
326
- referrer: document.referrer || void 0
373
+ referrer: document.referrer || void 0,
374
+ page_url: window.location.href,
375
+ device: detectDevice(),
376
+ ...marketing
327
377
  });
328
378
  if (session?.id) {
329
379
  localStorage.setItem(sessionKey, JSON.stringify({ id: session.id, createdAt: Date.now() }));
@@ -348,10 +398,12 @@ function useAnalytics() {
348
398
  pixelManager?.track(eventType, { ...data, eventID: eventId });
349
399
  try {
350
400
  const sessionId = await getOrCreateSession();
401
+ const visitorId = getVisitorId(config.storagePrefix);
351
402
  await client.trackEvent({
352
403
  session_id: sessionId,
353
404
  event_type: eventType,
354
- event_data: { ...data, event_id: eventId }
405
+ event_data: { ...data, event_id: eventId },
406
+ visitor_id: visitorId
355
407
  });
356
408
  } catch {
357
409
  }
@@ -370,9 +422,38 @@ function useAnalytics() {
370
422
  },
371
423
  [client, getOrCreateSession, trackingEnabled]
372
424
  );
425
+ const updateSessionCart = react.useCallback(
426
+ async (cartId, cartTotal, cartItemCount) => {
427
+ if (!trackingEnabled) return;
428
+ try {
429
+ const sessionId = await getOrCreateSession();
430
+ if (sessionId.startsWith("local-")) return;
431
+ await client.updateSession(sessionId, {
432
+ cart_id: cartId,
433
+ cart_total: cartTotal,
434
+ cart_item_count: cartItemCount,
435
+ status: "carting"
436
+ });
437
+ } catch {
438
+ }
439
+ },
440
+ [client, getOrCreateSession, trackingEnabled]
441
+ );
442
+ const updateSessionOrder = react.useCallback(
443
+ async (orderId) => {
444
+ if (!trackingEnabled) return;
445
+ try {
446
+ const sessionId = await getOrCreateSession();
447
+ if (sessionId.startsWith("local-")) return;
448
+ await client.updateSession(sessionId, { order_id: orderId, status: "converted" });
449
+ } catch {
450
+ }
451
+ },
452
+ [client, getOrCreateSession, trackingEnabled]
453
+ );
373
454
  const trackPageView = react.useCallback(
374
455
  (url, referrer) => {
375
- track("page_view", { url, referrer });
456
+ track("page_view", { url, referrer, page_url: url });
376
457
  },
377
458
  [track]
378
459
  );
@@ -429,6 +510,8 @@ function useAnalytics() {
429
510
  trackAddToCart,
430
511
  trackRemoveFromCart,
431
512
  linkCustomer,
513
+ updateSessionCart,
514
+ updateSessionOrder,
432
515
  getOrCreateSession,
433
516
  /** Whether tracking is globally enabled for this storefront */
434
517
  trackingEnabled,
@@ -446,6 +529,7 @@ function useAuth() {
446
529
  isAuthenticated: s.isSessionValid(),
447
530
  sendCode: s.sendOTP,
448
531
  verifyCode: s.verifyOTP,
532
+ updateProfile: s.updateProfile,
449
533
  restoreSession: s.restoreSession,
450
534
  logout: s.logout,
451
535
  fetchCustomer: s.fetchCustomer
@@ -465,7 +549,8 @@ function AnalyticsTracker({ pathname }) {
465
549
  if (pathname === prevPathname.current) return;
466
550
  const referrer = prevPathname.current || (typeof document !== "undefined" ? document.referrer : "");
467
551
  prevPathname.current = pathname;
468
- 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 chunkVAA2KKCH_cjs.resilientSend(endpoint, batch, {
1031
+ "Content-Type": "application/json",
1032
+ "x-api-key": config.apiKey
1033
+ });
1034
+ };
1035
+ const tracker = new BehavioralTracker({ sendBatch, sessionId, visitorId });
1036
+ tracker.start();
1037
+ trackerRef.current = tracker;
1038
+ };
1039
+ tryInit();
1040
+ return () => {
1041
+ cancelled = true;
1042
+ if (pollTimer) clearTimeout(pollTimer);
1043
+ if (trackerRef.current) {
1044
+ trackerRef.current.stop();
1045
+ trackerRef.current = null;
1046
+ }
1047
+ };
1048
+ }, []);
1049
+ react.useEffect(() => {
1050
+ if (!trackerRef.current || !pathname) return;
1051
+ const url = typeof window !== "undefined" ? window.location.href : pathname;
1052
+ trackerRef.current.setPageContext(url, pathname);
1053
+ }, [pathname]);
1054
+ return null;
1055
+ }
1056
+
1057
+ // src/fingerprint/collector.ts
1058
+ async function sha256(input) {
1059
+ const data = new TextEncoder().encode(input);
1060
+ const hash = await crypto.subtle.digest("SHA-256", data);
1061
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
1062
+ }
1063
+ async function getCanvasFingerprint() {
1064
+ try {
1065
+ const canvas = document.createElement("canvas");
1066
+ canvas.width = 256;
1067
+ canvas.height = 256;
1068
+ const ctx = canvas.getContext("2d");
1069
+ if (!ctx) return "";
1070
+ const gradient = ctx.createLinearGradient(0, 0, 256, 256);
1071
+ gradient.addColorStop(0, "#ff6b35");
1072
+ gradient.addColorStop(0.5, "#1a73e8");
1073
+ gradient.addColorStop(1, "#34a853");
1074
+ ctx.fillStyle = gradient;
1075
+ ctx.fillRect(0, 0, 256, 256);
1076
+ ctx.fillStyle = "#ffffff";
1077
+ ctx.font = "18px Arial";
1078
+ ctx.textBaseline = "top";
1079
+ ctx.fillText("WhaleTools", 10, 10);
1080
+ ctx.beginPath();
1081
+ ctx.arc(128, 128, 60, 0, Math.PI * 2);
1082
+ ctx.strokeStyle = "#fbbc04";
1083
+ ctx.lineWidth = 3;
1084
+ ctx.stroke();
1085
+ ctx.beginPath();
1086
+ ctx.moveTo(0, 0);
1087
+ ctx.lineTo(256, 256);
1088
+ ctx.strokeStyle = "#ea4335";
1089
+ ctx.lineWidth = 2;
1090
+ ctx.stroke();
1091
+ const dataUrl = canvas.toDataURL();
1092
+ return sha256(dataUrl);
1093
+ } catch {
1094
+ return "";
1095
+ }
1096
+ }
1097
+ async function getWebGLFingerprint() {
1098
+ try {
1099
+ const canvas = document.createElement("canvas");
1100
+ const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
1101
+ if (!gl || !(gl instanceof WebGLRenderingContext)) return "";
1102
+ const ext = gl.getExtension("WEBGL_debug_renderer_info");
1103
+ const renderer = ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : "unknown";
1104
+ const vendor = ext ? gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) : "unknown";
1105
+ const version = gl.getParameter(gl.VERSION);
1106
+ const combined = `${renderer}|${vendor}|${version}`;
1107
+ return sha256(combined);
1108
+ } catch {
1109
+ return "";
1110
+ }
1111
+ }
1112
+ async function getAudioFingerprint() {
1113
+ try {
1114
+ const AudioCtx = window.OfflineAudioContext || window.webkitOfflineAudioContext;
1115
+ if (!AudioCtx) return "";
1116
+ const context = new AudioCtx(1, 44100, 44100);
1117
+ const oscillator = context.createOscillator();
1118
+ oscillator.type = "triangle";
1119
+ oscillator.frequency.setValueAtTime(1e4, context.currentTime);
1120
+ const compressor = context.createDynamicsCompressor();
1121
+ compressor.threshold.setValueAtTime(-50, context.currentTime);
1122
+ compressor.knee.setValueAtTime(40, context.currentTime);
1123
+ compressor.ratio.setValueAtTime(12, context.currentTime);
1124
+ compressor.attack.setValueAtTime(0, context.currentTime);
1125
+ compressor.release.setValueAtTime(0.25, context.currentTime);
1126
+ oscillator.connect(compressor);
1127
+ compressor.connect(context.destination);
1128
+ oscillator.start(0);
1129
+ const buffer = await context.startRendering();
1130
+ const samples = buffer.getChannelData(0).slice(0, 100);
1131
+ const sampleStr = Array.from(samples).map((s) => s.toString()).join(",");
1132
+ return sha256(sampleStr);
1133
+ } catch {
1134
+ return "";
1135
+ }
1136
+ }
1137
+ async function collectFingerprint() {
1138
+ const [canvas_fingerprint, webgl_fingerprint, audio_fingerprint] = await Promise.all([
1139
+ getCanvasFingerprint(),
1140
+ getWebGLFingerprint(),
1141
+ getAudioFingerprint()
1142
+ ]);
1143
+ const screen_resolution = `${window.screen.width}x${window.screen.height}`;
1144
+ const platform = navigator.platform || "";
1145
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "";
1146
+ const language = navigator.language || "";
1147
+ const hardware_concurrency = navigator.hardwareConcurrency || 0;
1148
+ const device_memory = navigator.deviceMemory ?? null;
1149
+ const color_depth = window.screen.colorDepth;
1150
+ const pixel_ratio = window.devicePixelRatio || 1;
1151
+ const touch_support = "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
1152
+ const cookie_enabled = navigator.cookieEnabled;
1153
+ const do_not_track = navigator.doNotTrack ?? null;
1154
+ const fingerprintSource = [
1155
+ canvas_fingerprint,
1156
+ webgl_fingerprint,
1157
+ audio_fingerprint,
1158
+ screen_resolution,
1159
+ platform,
1160
+ timezone,
1161
+ language,
1162
+ String(hardware_concurrency)
1163
+ ].join("|");
1164
+ const fingerprint_id = await sha256(fingerprintSource);
1165
+ return {
1166
+ fingerprint_id,
1167
+ canvas_fingerprint,
1168
+ webgl_fingerprint,
1169
+ audio_fingerprint,
1170
+ screen_resolution,
1171
+ platform,
1172
+ timezone,
1173
+ language,
1174
+ hardware_concurrency,
1175
+ device_memory,
1176
+ color_depth,
1177
+ pixel_ratio,
1178
+ touch_support,
1179
+ cookie_enabled,
1180
+ do_not_track
1181
+ };
1182
+ }
1183
+
1184
+ // src/react/components/fingerprint-collector.tsx
1185
+ var SESSION_KEY_SUFFIX3 = "-analytics-session";
1186
+ function FingerprintCollector() {
1187
+ const ctx = react.useContext(WhaleContext);
1188
+ const sent = react.useRef(false);
1189
+ react.useEffect(() => {
1190
+ if (!ctx || sent.current) return;
1191
+ if (!ctx.config.trackingEnabled) return;
1192
+ if (typeof window === "undefined") return;
1193
+ sent.current = true;
1194
+ const { config, client } = ctx;
1195
+ const prefix = config.storagePrefix;
1196
+ const fpKey = `${prefix}-fingerprint-sent`;
1197
+ const linkFingerprintToSession = (fingerprintId) => {
1198
+ try {
1199
+ const sessionRaw = localStorage.getItem(`${prefix}${SESSION_KEY_SUFFIX3}`);
1200
+ if (sessionRaw) {
1201
+ const session = JSON.parse(sessionRaw);
1202
+ client.updateSession(session.id, { fingerprint_id: fingerprintId }).catch(() => {
1203
+ });
1204
+ }
1205
+ } catch {
1206
+ }
1207
+ };
1208
+ const existing = localStorage.getItem(fpKey);
1209
+ if (existing) {
1210
+ linkFingerprintToSession(existing);
1211
+ return;
1212
+ }
1213
+ collectFingerprint().then(async (fp) => {
1214
+ const baseUrl = config.proxyPath;
1215
+ const url = `${baseUrl}/v1/stores/${config.storeId}/storefront/fingerprints`;
1216
+ await chunkVAA2KKCH_cjs.resilientSend(url, fp, {
1217
+ "Content-Type": "application/json",
1218
+ "x-api-key": config.apiKey
1219
+ }).catch(() => {
1220
+ });
1221
+ localStorage.setItem(fpKey, fp.fingerprint_id);
1222
+ linkFingerprintToSession(fp.fingerprint_id);
1223
+ }).catch(() => {
1224
+ });
1225
+ }, [ctx]);
1226
+ return null;
1227
+ }
1228
+
1229
+ // src/recording/recorder.ts
1230
+ function now() {
1231
+ return Date.now();
1232
+ }
1233
+ function throttle(fn, ms) {
1234
+ let last = 0;
1235
+ let timer = null;
1236
+ const throttled = (...args) => {
1237
+ const elapsed = now() - last;
1238
+ if (elapsed >= ms) {
1239
+ last = now();
1240
+ fn(...args);
1241
+ } else if (!timer) {
1242
+ timer = setTimeout(() => {
1243
+ last = now();
1244
+ timer = null;
1245
+ fn(...args);
1246
+ }, ms - elapsed);
1247
+ }
1248
+ };
1249
+ return throttled;
1250
+ }
1251
+ function shortSelector(el) {
1252
+ if (el.id) return `#${el.id}`;
1253
+ let sel = el.tagName.toLowerCase();
1254
+ if (el.className && typeof el.className === "string") {
1255
+ const cls = el.className.trim().split(/\s+/).slice(0, 2).join(".");
1256
+ if (cls) sel += `.${cls}`;
1257
+ }
1258
+ return sel;
1259
+ }
1260
+ var SessionRecorder = class {
1261
+ constructor(config) {
1262
+ this.events = [];
1263
+ this.sequence = 0;
1264
+ this.observer = null;
1265
+ this.flushTimer = null;
1266
+ this.listeners = [];
1267
+ this.started = false;
1268
+ this.flushing = false;
1269
+ /** Pending mutations collected within the current animation frame. */
1270
+ this.pendingMutations = [];
1271
+ this.mutationRafId = null;
1272
+ this.config = {
1273
+ sendChunk: config.sendChunk,
1274
+ flushIntervalMs: config.flushIntervalMs ?? 5e3,
1275
+ maxChunkSize: config.maxChunkSize ?? 200
1276
+ };
1277
+ }
1278
+ // -----------------------------------------------------------------------
1279
+ // Public API
1280
+ // -----------------------------------------------------------------------
1281
+ start() {
1282
+ if (this.started) return;
1283
+ this.started = true;
1284
+ this.captureFullSnapshot();
1285
+ this.setupMutationObserver();
1286
+ this.setupEventListeners();
1287
+ this.flushTimer = setInterval(() => {
1288
+ void this.flush();
1289
+ }, this.config.flushIntervalMs);
1290
+ }
1291
+ stop() {
1292
+ if (!this.started) return;
1293
+ this.started = false;
1294
+ if (this.observer) {
1295
+ this.observer.disconnect();
1296
+ this.observer = null;
1297
+ }
1298
+ if (this.mutationRafId !== null) {
1299
+ cancelAnimationFrame(this.mutationRafId);
1300
+ this.mutationRafId = null;
1301
+ }
1302
+ for (const [target, event, handler, options] of this.listeners) {
1303
+ target.removeEventListener(event, handler, options);
1304
+ }
1305
+ this.listeners = [];
1306
+ if (this.flushTimer !== null) {
1307
+ clearInterval(this.flushTimer);
1308
+ this.flushTimer = null;
1309
+ }
1310
+ void this.flush();
1311
+ }
1312
+ // -----------------------------------------------------------------------
1313
+ // Full Snapshot (type 0)
1314
+ // -----------------------------------------------------------------------
1315
+ captureFullSnapshot() {
1316
+ const tree = this.serializeNode(document.documentElement);
1317
+ this.push({
1318
+ type: 0,
1319
+ timestamp: now(),
1320
+ data: {
1321
+ href: location.href,
1322
+ width: window.innerWidth,
1323
+ height: window.innerHeight,
1324
+ tree: tree ?? {}
1325
+ }
1326
+ });
1327
+ }
1328
+ serializeNode(node) {
1329
+ if (node.nodeType === Node.ELEMENT_NODE) {
1330
+ const el = node;
1331
+ const tag = el.tagName.toLowerCase();
1332
+ if (tag === "script" || tag === "noscript") return null;
1333
+ const attrs = {};
1334
+ for (const attr of Array.from(el.attributes)) {
1335
+ if (attr.name === "value" && el instanceof HTMLInputElement && el.type === "password") {
1336
+ continue;
1337
+ }
1338
+ attrs[attr.name] = attr.value;
1339
+ }
1340
+ const children = [];
1341
+ for (const child of Array.from(el.childNodes)) {
1342
+ const serialized = this.serializeNode(child);
1343
+ if (serialized) children.push(serialized);
1344
+ }
1345
+ return { tag, attrs, children };
1346
+ }
1347
+ if (node.nodeType === Node.TEXT_NODE) {
1348
+ const text = node.textContent || "";
1349
+ if (!text.trim()) return null;
1350
+ return { text };
1351
+ }
1352
+ return null;
1353
+ }
1354
+ // -----------------------------------------------------------------------
1355
+ // Mutation Observer (type 1)
1356
+ // -----------------------------------------------------------------------
1357
+ setupMutationObserver() {
1358
+ this.observer = new MutationObserver((mutations) => {
1359
+ this.pendingMutations.push(...mutations);
1360
+ if (this.mutationRafId === null) {
1361
+ this.mutationRafId = requestAnimationFrame(() => {
1362
+ this.processMutations(this.pendingMutations);
1363
+ this.pendingMutations = [];
1364
+ this.mutationRafId = null;
1365
+ });
1366
+ }
1367
+ });
1368
+ this.observer.observe(document.documentElement, {
1369
+ childList: true,
1370
+ attributes: true,
1371
+ characterData: true,
1372
+ subtree: true,
1373
+ attributeOldValue: false
1374
+ });
1375
+ }
1376
+ processMutations(mutations) {
1377
+ const ts = now();
1378
+ const adds = [];
1379
+ const removes = [];
1380
+ const attrs = [];
1381
+ const texts = [];
1382
+ for (const m of mutations) {
1383
+ if (m.type === "childList") {
1384
+ for (const node of Array.from(m.addedNodes)) {
1385
+ if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE) {
1386
+ const serialized = this.serializeNode(node);
1387
+ if (serialized) {
1388
+ adds.push({
1389
+ parentSelector: m.target.nodeType === Node.ELEMENT_NODE ? shortSelector(m.target) : null,
1390
+ node: serialized
1391
+ });
1392
+ }
1393
+ }
1394
+ }
1395
+ for (const node of Array.from(m.removedNodes)) {
1396
+ removes.push({
1397
+ parentSelector: m.target.nodeType === Node.ELEMENT_NODE ? shortSelector(m.target) : null,
1398
+ tag: node.nodeType === Node.ELEMENT_NODE ? node.tagName.toLowerCase() : "#text"
1399
+ });
1400
+ }
1401
+ } else if (m.type === "attributes" && m.target.nodeType === Node.ELEMENT_NODE) {
1402
+ const el = m.target;
1403
+ const name = m.attributeName || "";
1404
+ attrs.push({
1405
+ selector: shortSelector(el),
1406
+ name,
1407
+ value: el.getAttribute(name)
1408
+ });
1409
+ } else if (m.type === "characterData") {
1410
+ texts.push({
1411
+ parentSelector: m.target.parentElement ? shortSelector(m.target.parentElement) : null,
1412
+ value: (m.target.textContent || "").slice(0, 200)
1413
+ });
1414
+ }
1415
+ }
1416
+ if (adds.length || removes.length || attrs.length || texts.length) {
1417
+ this.push({
1418
+ type: 1,
1419
+ timestamp: ts,
1420
+ data: {
1421
+ adds: adds.length ? adds : void 0,
1422
+ removes: removes.length ? removes : void 0,
1423
+ attrs: attrs.length ? attrs : void 0,
1424
+ texts: texts.length ? texts : void 0
1425
+ }
1426
+ });
1427
+ }
1428
+ }
1429
+ // -----------------------------------------------------------------------
1430
+ // Event Listeners (types 2–5)
1431
+ // -----------------------------------------------------------------------
1432
+ setupEventListeners() {
1433
+ const onMouseMove = throttle(((e) => {
1434
+ this.push({
1435
+ type: 2,
1436
+ timestamp: now(),
1437
+ data: { source: "move", x: e.clientX, y: e.clientY }
1438
+ });
1439
+ }), 100);
1440
+ const onClick = ((e) => {
1441
+ const target = e.target;
1442
+ this.push({
1443
+ type: 2,
1444
+ timestamp: now(),
1445
+ data: {
1446
+ source: "click",
1447
+ x: e.clientX,
1448
+ y: e.clientY,
1449
+ target: target ? shortSelector(target) : null
1450
+ }
1451
+ });
1452
+ });
1453
+ this.addListener(document, "mousemove", onMouseMove, { passive: true, capture: true });
1454
+ this.addListener(document, "click", onClick, { passive: true, capture: true });
1455
+ const onScroll = throttle(((e) => {
1456
+ const target = e.target;
1457
+ if (target === document || target === document.documentElement || target === window) {
1458
+ this.push({
1459
+ type: 3,
1460
+ timestamp: now(),
1461
+ data: { target: "window", x: window.scrollX, y: window.scrollY }
1462
+ });
1463
+ } else if (target instanceof Element) {
1464
+ this.push({
1465
+ type: 3,
1466
+ timestamp: now(),
1467
+ data: {
1468
+ target: shortSelector(target),
1469
+ x: target.scrollLeft,
1470
+ y: target.scrollTop
1471
+ }
1472
+ });
1473
+ }
1474
+ }), 200);
1475
+ this.addListener(document, "scroll", onScroll, { passive: true, capture: true });
1476
+ const onInput = ((e) => {
1477
+ const target = e.target;
1478
+ if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement)) {
1479
+ return;
1480
+ }
1481
+ const isPassword = target instanceof HTMLInputElement && target.type === "password";
1482
+ this.push({
1483
+ type: 4,
1484
+ timestamp: now(),
1485
+ data: {
1486
+ selector: shortSelector(target),
1487
+ tag: target.tagName.toLowerCase(),
1488
+ inputType: target instanceof HTMLInputElement ? target.type : void 0,
1489
+ value: isPassword ? "***" : (target.value || "").slice(0, 100)
1490
+ }
1491
+ });
1492
+ });
1493
+ this.addListener(document, "input", onInput, { passive: true, capture: true });
1494
+ this.addListener(document, "change", onInput, { passive: true, capture: true });
1495
+ const onResize = throttle((() => {
1496
+ this.push({
1497
+ type: 5,
1498
+ timestamp: now(),
1499
+ data: { width: window.innerWidth, height: window.innerHeight }
1500
+ });
1501
+ }), 500);
1502
+ this.addListener(window, "resize", onResize, { passive: true });
1503
+ const onVisibility = (() => {
1504
+ if (document.visibilityState === "hidden") {
1505
+ void this.flush();
1506
+ }
1507
+ });
1508
+ this.addListener(document, "visibilitychange", onVisibility);
1509
+ }
1510
+ // -----------------------------------------------------------------------
1511
+ // Event buffer & flushing
1512
+ // -----------------------------------------------------------------------
1513
+ push(event) {
1514
+ if (!this.started) return;
1515
+ this.events.push(event);
1516
+ if (this.events.length >= this.config.maxChunkSize) {
1517
+ void this.flush();
1518
+ }
1519
+ }
1520
+ async flush() {
1521
+ if (this.events.length === 0) return;
1522
+ if (this.flushing) return;
1523
+ this.flushing = true;
1524
+ const chunk = this.events;
1525
+ this.events = [];
1526
+ const seq = this.sequence++;
1527
+ try {
1528
+ await this.config.sendChunk(chunk, seq);
1529
+ } catch {
1530
+ } finally {
1531
+ this.flushing = false;
1532
+ }
1533
+ }
1534
+ // -----------------------------------------------------------------------
1535
+ // Listener bookkeeping
1536
+ // -----------------------------------------------------------------------
1537
+ addListener(target, event, handler, options) {
1538
+ target.addEventListener(event, handler, options);
1539
+ this.listeners.push([target, event, handler, options]);
1540
+ }
1541
+ };
1542
+
1543
+ // src/react/components/session-recorder.tsx
1544
+ var SESSION_KEY_SUFFIX4 = "-analytics-session";
1545
+ var VISITOR_KEY_SUFFIX3 = "-visitor-id";
1546
+ var MAX_SESSION_WAIT_MS2 = 1e4;
1547
+ var SESSION_POLL_MS2 = 500;
1548
+ function SessionRecorderComponent() {
1549
+ const ctx = react.useContext(WhaleContext);
1550
+ const recorderRef = react.useRef(null);
1551
+ const sampledRef = react.useRef(null);
1552
+ const initRef = react.useRef(false);
1553
+ react.useEffect(() => {
1554
+ if (!ctx || initRef.current) return;
1555
+ if (!ctx.config.trackingEnabled) return;
1556
+ if (typeof window === "undefined") return;
1557
+ if (sampledRef.current === null) {
1558
+ sampledRef.current = Math.random() < ctx.config.recordingRate;
1559
+ }
1560
+ if (!sampledRef.current) return;
1561
+ const { config } = ctx;
1562
+ const prefix = config.storagePrefix;
1563
+ let cancelled = false;
1564
+ let pollTimer = null;
1565
+ const startTime = Date.now();
1566
+ const tryInit = () => {
1567
+ if (cancelled) return;
1568
+ let sessionId = null;
1569
+ try {
1570
+ const sessionRaw = localStorage.getItem(`${prefix}${SESSION_KEY_SUFFIX4}`);
1571
+ if (sessionRaw) {
1572
+ const session = JSON.parse(sessionRaw);
1573
+ sessionId = session.id || null;
1574
+ }
1575
+ } catch {
1576
+ }
1577
+ if (!sessionId) {
1578
+ if (Date.now() - startTime < MAX_SESSION_WAIT_MS2) {
1579
+ pollTimer = setTimeout(tryInit, SESSION_POLL_MS2);
1580
+ return;
1581
+ }
1582
+ return;
1583
+ }
1584
+ initRef.current = true;
1585
+ const visitorId = (() => {
1586
+ try {
1587
+ return localStorage.getItem(`${prefix}${VISITOR_KEY_SUFFIX3}`) || "unknown";
1588
+ } catch {
1589
+ return "unknown";
1590
+ }
1591
+ })();
1592
+ const baseUrl = config.proxyPath;
1593
+ const url = `${baseUrl}/v1/stores/${config.storeId}/storefront/recordings`;
1594
+ const sid = sessionId;
1595
+ const recorder = new SessionRecorder({
1596
+ sendChunk: async (events, sequence) => {
1597
+ await chunkVAA2KKCH_cjs.resilientSend(url, {
1598
+ session_id: sid,
1599
+ visitor_id: visitorId,
1600
+ events,
1601
+ sequence,
1602
+ started_at: sequence === 0 ? (/* @__PURE__ */ new Date()).toISOString() : void 0
1603
+ }, {
1604
+ "Content-Type": "application/json",
1605
+ "x-api-key": config.apiKey
1606
+ });
1607
+ }
1608
+ });
1609
+ recorder.start();
1610
+ recorderRef.current = recorder;
1611
+ };
1612
+ tryInit();
1613
+ return () => {
1614
+ cancelled = true;
1615
+ if (pollTimer) clearTimeout(pollTimer);
1616
+ if (recorderRef.current) {
1617
+ recorderRef.current.stop();
1618
+ recorderRef.current = null;
1619
+ }
1620
+ };
1621
+ }, []);
1622
+ return null;
1623
+ }
562
1624
  function envBool(name) {
563
1625
  if (typeof process === "undefined") return void 0;
564
1626
  const raw = process.env[name];
@@ -611,13 +1673,45 @@ function WhaleProvider({
611
1673
  trackingEnabled: trackingEnabled ?? envBool("NEXT_PUBLIC_TRACKING_ENABLED") ?? true,
612
1674
  recordingRate: recordingRate ?? envNumber("NEXT_PUBLIC_RECORDING_RATE") ?? 0.1
613
1675
  };
614
- const client = new chunk3VKRKDPL_cjs.WhaleClient({
1676
+ const client = new chunkVAA2KKCH_cjs.WhaleClient({
615
1677
  storeId,
616
1678
  apiKey,
617
1679
  gatewayUrl: resolvedConfig.gatewayUrl,
618
1680
  proxyPath: resolvedConfig.proxyPath
619
1681
  });
620
- const 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
  }
@@ -764,12 +1861,12 @@ function useCheckout() {
764
1861
  setLoading(false);
765
1862
  }
766
1863
  }, [ctx.client, session]);
767
- const complete = react.useCallback(async (payment) => {
1864
+ const complete = react.useCallback(async (payment, opts) => {
768
1865
  if (!session) throw new Error("No active checkout session");
769
1866
  setLoading(true);
770
1867
  setError(null);
771
1868
  try {
772
- const order = await ctx.client.completeCheckout(session.id, payment);
1869
+ const order = await ctx.client.completeCheckout(session.id, payment, opts);
773
1870
  setSession(null);
774
1871
  return order;
775
1872
  } catch (err) {
@@ -923,7 +2020,11 @@ function useLoyalty() {
923
2020
  await refresh();
924
2021
  return result;
925
2022
  }, [customer?.id, ctx.client, refresh]);
926
- return { account, rewards, transactions, loading, error, refresh, redeemReward };
2023
+ const fetchProductsByCategory = react.useCallback(async (category, locationId, tier) => {
2024
+ const res = await ctx.client.listLoyaltyProducts({ category, location_id: locationId, tier });
2025
+ return res.data;
2026
+ }, [ctx.client]);
2027
+ return { account, rewards, transactions, loading, error, refresh, redeemReward, fetchProductsByCategory };
927
2028
  }
928
2029
  function useReviews(productId) {
929
2030
  const ctx = react.useContext(WhaleContext);
@@ -1151,9 +2252,9 @@ function useShipping() {
1151
2252
  }, [ctx.client]);
1152
2253
  return { methods, rates, loading, error, refreshMethods, calculateRates };
1153
2254
  }
1154
- function useCoupons() {
2255
+ function useDeals() {
1155
2256
  const ctx = react.useContext(WhaleContext);
1156
- if (!ctx) throw new Error("useCoupons must be used within <WhaleProvider>");
2257
+ if (!ctx) throw new Error("useDeals must be used within <WhaleProvider>");
1157
2258
  const [validation, setValidation] = react.useState(null);
1158
2259
  const [loading, setLoading] = react.useState(false);
1159
2260
  const [error, setError] = react.useState(null);
@@ -1161,7 +2262,7 @@ function useCoupons() {
1161
2262
  setLoading(true);
1162
2263
  setError(null);
1163
2264
  try {
1164
- const result = await ctx.client.validateCoupon(code, cartId ? { cart_id: cartId } : void 0);
2265
+ const result = await ctx.client.validateDeal(code, cartId ? { cart_id: cartId } : void 0);
1165
2266
  setValidation(result);
1166
2267
  return result;
1167
2268
  } catch (err) {
@@ -1177,7 +2278,7 @@ function useCoupons() {
1177
2278
  setLoading(true);
1178
2279
  setError(null);
1179
2280
  try {
1180
- const cart = await ctx.client.applyCoupon(cartId, code);
2281
+ const cart = await ctx.client.applyDeal(cartId, code);
1181
2282
  return cart;
1182
2283
  } catch (err) {
1183
2284
  const e = err instanceof Error ? err : new Error(String(err));
@@ -1191,7 +2292,7 @@ function useCoupons() {
1191
2292
  setLoading(true);
1192
2293
  setError(null);
1193
2294
  try {
1194
- const cart = await ctx.client.removeCoupon(cartId);
2295
+ const cart = await ctx.client.removeDeal(cartId);
1195
2296
  setValidation(null);
1196
2297
  return cart;
1197
2298
  } catch (err) {
@@ -1208,16 +2309,133 @@ function useCoupons() {
1208
2309
  }, []);
1209
2310
  return { validation, loading, error, validate, apply, remove, clear };
1210
2311
  }
2312
+ var useCoupons = useDeals;
2313
+ function useReferral() {
2314
+ const ctx = react.useContext(WhaleContext);
2315
+ if (!ctx) throw new Error("useReferral must be used within <WhaleProvider>");
2316
+ const customer = zustand.useStore(ctx.authStore, (s) => s.customer);
2317
+ const [status, setStatus] = react.useState(null);
2318
+ const [loading, setLoading] = react.useState(false);
2319
+ const [error, setError] = react.useState(null);
2320
+ const refresh = react.useCallback(async () => {
2321
+ if (!customer?.id) {
2322
+ setStatus(null);
2323
+ return;
2324
+ }
2325
+ setLoading(true);
2326
+ setError(null);
2327
+ try {
2328
+ const result = await ctx.client.getReferralStatus(customer.id);
2329
+ setStatus(result);
2330
+ } catch (err) {
2331
+ setError(err instanceof Error ? err : new Error(String(err)));
2332
+ } finally {
2333
+ setLoading(false);
2334
+ }
2335
+ }, [customer?.id, ctx.client]);
2336
+ react.useEffect(() => {
2337
+ refresh();
2338
+ }, [refresh]);
2339
+ const enroll = react.useCallback(async () => {
2340
+ if (!customer?.id) throw new Error("Not authenticated");
2341
+ setLoading(true);
2342
+ setError(null);
2343
+ try {
2344
+ const result = await ctx.client.enrollReferral(customer.id);
2345
+ await refresh();
2346
+ return result;
2347
+ } catch (err) {
2348
+ const e = err instanceof Error ? err : new Error(String(err));
2349
+ setError(e);
2350
+ throw e;
2351
+ } finally {
2352
+ setLoading(false);
2353
+ }
2354
+ }, [customer?.id, ctx.client, refresh]);
2355
+ const attributeReferral = react.useCallback(
2356
+ async (code) => {
2357
+ if (!customer?.id) throw new Error("Not authenticated");
2358
+ setLoading(true);
2359
+ setError(null);
2360
+ try {
2361
+ const result = await ctx.client.attributeReferral(customer.id, code);
2362
+ await refresh();
2363
+ return result;
2364
+ } catch (err) {
2365
+ const e = err instanceof Error ? err : new Error(String(err));
2366
+ setError(e);
2367
+ throw e;
2368
+ } finally {
2369
+ setLoading(false);
2370
+ }
2371
+ },
2372
+ [customer?.id, ctx.client, refresh]
2373
+ );
2374
+ react.useEffect(() => {
2375
+ if (!customer?.id || !status || status.referred_by) return;
2376
+ if (typeof window === "undefined") return;
2377
+ const params = new URLSearchParams(window.location.search);
2378
+ const code = params.get("code") || localStorage.getItem("whale_ref_code");
2379
+ if (!code) return;
2380
+ ctx.client.attributeReferral(customer.id, code).then(() => {
2381
+ localStorage.removeItem("whale_ref_code");
2382
+ refresh();
2383
+ }).catch(() => {
2384
+ });
2385
+ }, [customer?.id, status, ctx.client, refresh]);
2386
+ const share = react.useCallback(async () => {
2387
+ if (!status?.share_url) throw new Error("Not enrolled in referral program");
2388
+ const shareData = {
2389
+ title: "Check this out!",
2390
+ text: `Use my referral code ${status.referral_code} for rewards!`,
2391
+ url: status.share_url
2392
+ };
2393
+ if (typeof navigator !== "undefined" && navigator.share) {
2394
+ try {
2395
+ await navigator.share(shareData);
2396
+ return;
2397
+ } catch {
2398
+ }
2399
+ }
2400
+ if (typeof navigator !== "undefined" && navigator.clipboard) {
2401
+ await navigator.clipboard.writeText(status.share_url);
2402
+ }
2403
+ }, [status]);
2404
+ return {
2405
+ status,
2406
+ loading,
2407
+ error,
2408
+ enroll,
2409
+ refresh,
2410
+ share,
2411
+ attributeReferral,
2412
+ referralCode: status?.referral_code ?? null,
2413
+ shareUrl: status?.share_url ?? null,
2414
+ isEnrolled: status?.enrolled ?? false,
2415
+ referredBy: status?.referred_by ?? null
2416
+ };
2417
+ }
2418
+ function trackClick(tracking, label, url, position) {
2419
+ if (!tracking?.gatewayUrl || !tracking?.code) return;
2420
+ const body = JSON.stringify({ label, url, position });
2421
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
2422
+ navigator.sendBeacon(
2423
+ `${tracking.gatewayUrl}/q/${encodeURIComponent(tracking.code)}/click`,
2424
+ new Blob([body], { type: "application/json" })
2425
+ );
2426
+ }
2427
+ }
1211
2428
  function SectionRenderer({
1212
2429
  section,
1213
2430
  data,
1214
- theme
2431
+ theme,
2432
+ tracking
1215
2433
  }) {
1216
2434
  const [showCOA, setShowCOA] = react.useState(false);
1217
2435
  const el = (() => {
1218
2436
  switch (section.type) {
1219
2437
  case "hero":
1220
- return /* @__PURE__ */ jsxRuntime.jsx(HeroSection, { section, theme });
2438
+ return /* @__PURE__ */ jsxRuntime.jsx(HeroSection, { section, theme, tracking });
1221
2439
  case "text":
1222
2440
  return /* @__PURE__ */ jsxRuntime.jsx(TextSection, { section, theme });
1223
2441
  case "image":
@@ -1227,15 +2445,17 @@ function SectionRenderer({
1227
2445
  case "gallery":
1228
2446
  return /* @__PURE__ */ jsxRuntime.jsx(GallerySection, { section, theme });
1229
2447
  case "cta":
1230
- return /* @__PURE__ */ jsxRuntime.jsx(CTASection, { section, theme });
2448
+ return /* @__PURE__ */ jsxRuntime.jsx(CTASection, { section, theme, tracking });
1231
2449
  case "stats":
1232
2450
  return /* @__PURE__ */ jsxRuntime.jsx(StatsSection, { section, theme });
1233
2451
  case "product_card":
1234
- return /* @__PURE__ */ jsxRuntime.jsx(ProductCardSection, { section, data, theme });
2452
+ return /* @__PURE__ */ jsxRuntime.jsx(ProductCardSection, { section, data, theme, tracking });
1235
2453
  case "coa_viewer":
1236
- return /* @__PURE__ */ jsxRuntime.jsx(COAViewerSection, { section, data, theme, onShowCOA: () => setShowCOA(true) });
2454
+ return /* @__PURE__ */ jsxRuntime.jsx(COAViewerSection, { section, data, theme, onShowCOA: () => setShowCOA(true), tracking });
1237
2455
  case "social_links":
1238
2456
  return /* @__PURE__ */ jsxRuntime.jsx(SocialLinksSection, { section, theme });
2457
+ case "lead_capture":
2458
+ return /* @__PURE__ */ jsxRuntime.jsx(LeadCaptureSection, { section, data, theme });
1239
2459
  case "divider":
1240
2460
  return /* @__PURE__ */ jsxRuntime.jsx(DividerSection, { theme });
1241
2461
  default:
@@ -1247,7 +2467,7 @@ function SectionRenderer({
1247
2467
  showCOA && data?.coa && /* @__PURE__ */ jsxRuntime.jsx(COAModal, { coa: data.coa, theme, onClose: () => setShowCOA(false) })
1248
2468
  ] });
1249
2469
  }
1250
- function HeroSection({ section, theme }) {
2470
+ function HeroSection({ section, theme, tracking }) {
1251
2471
  const { title, subtitle, background_image, cta_text, cta_url } = section.content;
1252
2472
  return /* @__PURE__ */ jsxRuntime.jsxs(
1253
2473
  "div",
@@ -1289,6 +2509,7 @@ function HeroSection({ section, theme }) {
1289
2509
  "a",
1290
2510
  {
1291
2511
  href: cta_url,
2512
+ onClick: () => trackClick(tracking, cta_text, cta_url),
1292
2513
  style: {
1293
2514
  display: "inline-block",
1294
2515
  padding: "0.875rem 2rem",
@@ -1374,7 +2595,7 @@ function GallerySection({ section, theme }) {
1374
2595
  }
1375
2596
  ) }, i)) }) });
1376
2597
  }
1377
- function CTASection({ section, theme }) {
2598
+ function CTASection({ section, theme, tracking }) {
1378
2599
  const { buttons } = section.content;
1379
2600
  if (!buttons || buttons.length === 0) return null;
1380
2601
  return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "2rem 1.5rem", maxWidth: 480, margin: "0 auto", display: "flex", flexDirection: "column", gap: "0.75rem" }, children: buttons.map((btn, i) => {
@@ -1383,6 +2604,7 @@ function CTASection({ section, theme }) {
1383
2604
  "a",
1384
2605
  {
1385
2606
  href: btn.url,
2607
+ onClick: () => trackClick(tracking, btn.text, btn.url, i),
1386
2608
  style: {
1387
2609
  display: "block",
1388
2610
  width: "100%",
@@ -1454,7 +2676,7 @@ function StatsSection({ section, theme }) {
1454
2676
  }, children: stat.label })
1455
2677
  ] }, i)) }) });
1456
2678
  }
1457
- function ProductCardSection({ section, data, theme }) {
2679
+ function ProductCardSection({ section, data, theme, tracking }) {
1458
2680
  const product = data?.product;
1459
2681
  const c = section.content;
1460
2682
  const name = c.name || product?.name || "";
@@ -1470,6 +2692,7 @@ function ProductCardSection({ section, data, theme }) {
1470
2692
  "a",
1471
2693
  {
1472
2694
  href: url,
2695
+ onClick: () => trackClick(tracking, "View Product", url),
1473
2696
  style: {
1474
2697
  display: "block",
1475
2698
  width: "100%",
@@ -1494,7 +2717,8 @@ function COAViewerSection({
1494
2717
  section,
1495
2718
  data,
1496
2719
  theme,
1497
- onShowCOA
2720
+ onShowCOA,
2721
+ tracking
1498
2722
  }) {
1499
2723
  const coa = data?.coa;
1500
2724
  const c = section.content;
@@ -1515,10 +2739,200 @@ function COAViewerSection({
1515
2739
  display: "block",
1516
2740
  boxSizing: "border-box"
1517
2741
  };
2742
+ const buttonLabel = c.button_text || "View Lab Results";
1518
2743
  if (coa.viewer_url) {
1519
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("a", { href: coa.viewer_url, target: "_blank", rel: "noopener noreferrer", style: buttonStyle, children: c.button_text || "View Lab Results" }) });
2744
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx(
2745
+ "a",
2746
+ {
2747
+ href: coa.viewer_url,
2748
+ target: "_blank",
2749
+ rel: "noopener noreferrer",
2750
+ onClick: () => trackClick(tracking, buttonLabel, coa.viewer_url),
2751
+ style: buttonStyle,
2752
+ children: buttonLabel
2753
+ }
2754
+ ) });
1520
2755
  }
1521
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onShowCOA, style: buttonStyle, children: c.button_text || "View Lab Results" }) });
2756
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => {
2757
+ trackClick(tracking, buttonLabel, coa.url);
2758
+ onShowCOA();
2759
+ }, style: buttonStyle, children: buttonLabel }) });
2760
+ }
2761
+ function LeadCaptureSection({ section, data, theme }) {
2762
+ const c = section.content;
2763
+ const [firstName, setFirstName] = react.useState("");
2764
+ const [email, setEmail] = react.useState("");
2765
+ const [status, setStatus] = react.useState("idle");
2766
+ const [errorMsg, setErrorMsg] = react.useState("");
2767
+ const gatewayUrl = c.gateway_url || data.gatewayUrl || "https://whale-gateway.fly.dev";
2768
+ const storeId = c.store_id || data.store?.id;
2769
+ const slug = c.landing_page_slug || data.landing_page?.slug;
2770
+ async function handleSubmit(e) {
2771
+ e.preventDefault();
2772
+ if (!email || !storeId) return;
2773
+ setStatus("loading");
2774
+ setErrorMsg("");
2775
+ try {
2776
+ const res = await fetch(`${gatewayUrl}/v1/stores/${storeId}/storefront/leads`, {
2777
+ method: "POST",
2778
+ headers: { "Content-Type": "application/json" },
2779
+ body: JSON.stringify({
2780
+ email,
2781
+ first_name: firstName || void 0,
2782
+ source: c.source || "landing_page",
2783
+ landing_page_slug: slug || void 0,
2784
+ tags: c.tags || void 0
2785
+ })
2786
+ });
2787
+ if (!res.ok) {
2788
+ const body = await res.json().catch(() => ({}));
2789
+ throw new Error(body?.error?.message || "Something went wrong. Please try again.");
2790
+ }
2791
+ setStatus("success");
2792
+ } catch (err) {
2793
+ setErrorMsg(err instanceof Error ? err.message : "Something went wrong. Please try again.");
2794
+ setStatus("error");
2795
+ }
2796
+ }
2797
+ const heading = c.heading || "get 10% off your first visit.";
2798
+ const subtitle = c.subtitle || "drop your email and we will send you the code.";
2799
+ const buttonText = c.button_text || "Claim My Discount";
2800
+ const successHeading = c.success_heading || "You\u2019re in!";
2801
+ const successMessage = c.success_message || "Check your inbox for the discount code.";
2802
+ const inputStyle = {
2803
+ flex: 1,
2804
+ minWidth: 0,
2805
+ padding: "0.875rem 1rem",
2806
+ background: theme.surface,
2807
+ border: `1px solid ${theme.fg}15`,
2808
+ color: theme.fg,
2809
+ fontSize: "0.95rem",
2810
+ fontWeight: 300,
2811
+ outline: "none",
2812
+ boxSizing: "border-box",
2813
+ fontFamily: "inherit",
2814
+ transition: "border-color 0.2s"
2815
+ };
2816
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "3.5rem 1.5rem", maxWidth: 560, margin: "0 auto" }, children: [
2817
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `@keyframes lc-spin { to { transform: rotate(360deg) } }` }),
2818
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
2819
+ background: theme.surface,
2820
+ border: `1px solid ${theme.fg}12`,
2821
+ padding: "clamp(2rem, 6vw, 3rem)"
2822
+ }, children: status === "success" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { textAlign: "center" }, children: [
2823
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { style: {
2824
+ fontSize: "clamp(1.5rem, 5vw, 2rem)",
2825
+ fontWeight: 300,
2826
+ fontFamily: theme.fontDisplay || "inherit",
2827
+ margin: "0 0 0.75rem",
2828
+ lineHeight: 1.2,
2829
+ letterSpacing: "-0.02em",
2830
+ color: theme.fg
2831
+ }, children: successHeading }),
2832
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: {
2833
+ fontSize: "0.9rem",
2834
+ color: `${theme.fg}99`,
2835
+ margin: "0 0 1.5rem",
2836
+ lineHeight: 1.6,
2837
+ fontWeight: 300
2838
+ }, children: successMessage }),
2839
+ c.coupon_code && /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
2840
+ display: "inline-block",
2841
+ padding: "0.75rem 2rem",
2842
+ background: `${theme.fg}08`,
2843
+ border: `1px dashed ${theme.fg}30`,
2844
+ fontSize: "clamp(1.25rem, 4vw, 1.75rem)",
2845
+ fontWeight: 500,
2846
+ fontFamily: "monospace",
2847
+ letterSpacing: "0.12em",
2848
+ color: theme.accent
2849
+ }, children: c.coupon_code })
2850
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2851
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { textAlign: "center", marginBottom: "clamp(1.5rem, 4vw, 2rem)" }, children: [
2852
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { style: {
2853
+ fontSize: "clamp(1.5rem, 5vw, 2.25rem)",
2854
+ fontWeight: 300,
2855
+ fontFamily: theme.fontDisplay || "inherit",
2856
+ margin: "0 0 0.5rem",
2857
+ lineHeight: 1.15,
2858
+ letterSpacing: "-0.02em",
2859
+ color: theme.fg
2860
+ }, children: heading }),
2861
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: {
2862
+ fontSize: "0.85rem",
2863
+ color: theme.accent,
2864
+ margin: 0,
2865
+ lineHeight: 1.6,
2866
+ textTransform: "uppercase",
2867
+ letterSpacing: "0.15em"
2868
+ }, children: subtitle })
2869
+ ] }),
2870
+ /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, style: { display: "flex", flexDirection: "column", gap: "0.75rem" }, children: [
2871
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "0.75rem", flexWrap: "wrap" }, children: [
2872
+ /* @__PURE__ */ jsxRuntime.jsx(
2873
+ "input",
2874
+ {
2875
+ type: "text",
2876
+ placeholder: "First name",
2877
+ value: firstName,
2878
+ onChange: (e) => setFirstName(e.target.value),
2879
+ style: inputStyle
2880
+ }
2881
+ ),
2882
+ /* @__PURE__ */ jsxRuntime.jsx(
2883
+ "input",
2884
+ {
2885
+ type: "email",
2886
+ placeholder: "Email address",
2887
+ value: email,
2888
+ onChange: (e) => setEmail(e.target.value),
2889
+ required: true,
2890
+ style: inputStyle
2891
+ }
2892
+ )
2893
+ ] }),
2894
+ status === "error" && errorMsg && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "0.8rem", color: "#e55", margin: 0, fontWeight: 400 }, children: errorMsg }),
2895
+ /* @__PURE__ */ jsxRuntime.jsxs(
2896
+ "button",
2897
+ {
2898
+ type: "submit",
2899
+ disabled: status === "loading",
2900
+ style: {
2901
+ width: "100%",
2902
+ padding: "0.875rem",
2903
+ background: theme.fg,
2904
+ color: theme.bg,
2905
+ border: "none",
2906
+ fontSize: "0.85rem",
2907
+ fontWeight: 500,
2908
+ cursor: status === "loading" ? "wait" : "pointer",
2909
+ letterSpacing: "0.08em",
2910
+ textTransform: "uppercase",
2911
+ fontFamily: "inherit",
2912
+ display: "flex",
2913
+ alignItems: "center",
2914
+ justifyContent: "center",
2915
+ gap: "0.5rem",
2916
+ opacity: status === "loading" ? 0.7 : 1,
2917
+ transition: "opacity 0.2s"
2918
+ },
2919
+ children: [
2920
+ status === "loading" && /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
2921
+ display: "inline-block",
2922
+ width: 16,
2923
+ height: 16,
2924
+ border: `2px solid ${theme.bg}40`,
2925
+ borderTopColor: theme.bg,
2926
+ borderRadius: "50%",
2927
+ animation: "lc-spin 0.8s linear infinite"
2928
+ } }),
2929
+ buttonText
2930
+ ]
2931
+ }
2932
+ )
2933
+ ] })
2934
+ ] }) })
2935
+ ] });
1522
2936
  }
1523
2937
  function SocialLinksSection({ section, theme }) {
1524
2938
  const { links } = section.content;
@@ -1641,15 +3055,16 @@ function QRLandingPage({
1641
3055
  const logoUrl = data.qr_code.logo_url || data.store?.logo_url;
1642
3056
  const storeName = data.store?.name;
1643
3057
  const sorted = [...sections].sort((a, b) => a.order - b.order);
3058
+ const tracking = { gatewayUrl, code };
1644
3059
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minHeight: "100dvh", background: theme.bg, color: theme.fg, fontFamily }, children: [
1645
3060
  lp?.custom_css && /* @__PURE__ */ jsxRuntime.jsx("style", { children: lp.custom_css }),
1646
3061
  logoUrl && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: logoUrl, alt: storeName || "Store", style: { height: 40, objectFit: "contain" } }) }),
1647
3062
  sorted.map((section) => {
1648
- const defaultRenderer = () => /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data, theme }, section.id);
3063
+ const defaultRenderer = () => /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data, theme, tracking }, section.id);
1649
3064
  if (renderSection) {
1650
3065
  return /* @__PURE__ */ jsxRuntime.jsx("div", { children: renderSection(section, defaultRenderer) }, section.id);
1651
3066
  }
1652
- return /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data, theme }, section.id);
3067
+ return /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data, theme, tracking }, section.id);
1653
3068
  }),
1654
3069
  storeName && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "2rem 1.5rem", borderTop: `1px solid ${theme.surface}`, textAlign: "center" }, children: /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { fontSize: "0.75rem", color: theme.muted, margin: 0 }, children: [
1655
3070
  storeName,
@@ -1677,10 +3092,10 @@ function buildDefaultSections(data) {
1677
3092
  let order = 0;
1678
3093
  const productName = qr.landing_page.title || product?.name || qr.name;
1679
3094
  const productImage = qr.landing_page.image_url || product?.featured_image || null;
1680
- const description = qr.landing_page.description || product?.description || "";
1681
- const ctaUrl = qr.landing_page.cta_url || qr.destination_url;
3095
+ const description = product?.description || "";
1682
3096
  const categoryName = product?.category_name ?? null;
1683
3097
  const strainType = toStr(cf?.strain_type);
3098
+ const tagline = toStr(cf?.tagline);
1684
3099
  if (productImage) {
1685
3100
  sections.push({
1686
3101
  id: "auto-hero",
@@ -1689,9 +3104,7 @@ function buildDefaultSections(data) {
1689
3104
  content: {
1690
3105
  title: productName,
1691
3106
  subtitle: [categoryName, strainType].filter(Boolean).join(" \xB7 "),
1692
- background_image: productImage,
1693
- cta_text: qr.landing_page.cta_text || null,
1694
- cta_url: ctaUrl
3107
+ background_image: productImage
1695
3108
  }
1696
3109
  });
1697
3110
  } else {
@@ -1706,6 +3119,15 @@ function buildDefaultSections(data) {
1706
3119
  config: { align: "center" }
1707
3120
  });
1708
3121
  }
3122
+ if (tagline) {
3123
+ sections.push({
3124
+ id: "auto-tagline",
3125
+ type: "text",
3126
+ order: order++,
3127
+ content: { body: tagline },
3128
+ config: { align: "center" }
3129
+ });
3130
+ }
1709
3131
  const thca = toNum(cf?.thca_percentage);
1710
3132
  const thc = toNum(cf?.d9_percentage);
1711
3133
  const cbd = toNum(cf?.cbd_total);
@@ -1721,23 +3143,23 @@ function buildDefaultSections(data) {
1721
3143
  content: { stats }
1722
3144
  });
1723
3145
  }
1724
- const details = [];
3146
+ const profileDetails = [];
1725
3147
  const genetics = toStr(cf?.genetics);
1726
3148
  const terpenes = toStr(cf?.terpenes);
1727
3149
  const effects = toStr(cf?.effects);
1728
- const 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) {
3150
+ const flavorProfile = toStr(cf?.flavor_profile);
3151
+ const bestFor = toStr(cf?.best_for);
3152
+ if (genetics) profileDetails.push({ label: "Genetics", value: genetics });
3153
+ if (terpenes) profileDetails.push({ label: "Terpenes", value: terpenes });
3154
+ if (effects) profileDetails.push({ label: "Effects", value: effects });
3155
+ if (flavorProfile) profileDetails.push({ label: "Flavor", value: flavorProfile });
3156
+ if (bestFor) profileDetails.push({ label: "Best For", value: bestFor });
3157
+ if (profileDetails.length > 0) {
1736
3158
  sections.push({
1737
- id: "auto-details",
3159
+ id: "auto-profile",
1738
3160
  type: "stats",
1739
3161
  order: order++,
1740
- content: { stats: details },
3162
+ content: { stats: profileDetails },
1741
3163
  config: { layout: "list" }
1742
3164
  });
1743
3165
  }
@@ -1757,16 +3179,34 @@ function buildDefaultSections(data) {
1757
3179
  content: { button_text: "View Lab Results" }
1758
3180
  });
1759
3181
  }
1760
- if (ctaUrl) {
3182
+ const labDetails = [];
3183
+ const batchNumber = toStr(cf?.batch_number);
3184
+ const dateTested = toStr(cf?.date_tested);
3185
+ if (batchNumber) labDetails.push({ label: "Batch", value: batchNumber });
3186
+ if (dateTested) labDetails.push({ label: "Tested", value: formatDate(dateTested) });
3187
+ if (labDetails.length > 0) {
1761
3188
  sections.push({
1762
- id: "auto-cta",
1763
- type: "cta",
3189
+ id: "auto-lab-info",
3190
+ type: "stats",
1764
3191
  order: order++,
1765
- content: {
1766
- buttons: [{ text: qr.landing_page.cta_text || "Shop Online", url: ctaUrl, style: "primary" }]
1767
- }
3192
+ content: { stats: labDetails },
3193
+ config: { layout: "list" }
1768
3194
  });
1769
3195
  }
3196
+ const productSlug = product?.slug;
3197
+ if (productSlug) {
3198
+ const storeDomain = data.store?.name === "Flora Distro" ? "floradistro.com" : null;
3199
+ if (storeDomain) {
3200
+ sections.push({
3201
+ id: "auto-shop",
3202
+ type: "cta",
3203
+ order: order++,
3204
+ content: {
3205
+ buttons: [{ text: "Shop This Product", url: `https://${storeDomain}/shop/${productSlug}`, style: "outline" }]
3206
+ }
3207
+ });
3208
+ }
3209
+ }
1770
3210
  return sections;
1771
3211
  }
1772
3212
  function toNum(v) {
@@ -1874,10 +3314,11 @@ function LandingPage({
1874
3314
  if (state === "expired") return /* @__PURE__ */ jsxRuntime.jsx(DefaultExpired2, {});
1875
3315
  if (state === "error") return /* @__PURE__ */ jsxRuntime.jsx(DefaultError2, { message: errorMsg });
1876
3316
  if (!data) return null;
1877
- return /* @__PURE__ */ jsxRuntime.jsx(PageLayout, { data, renderSection });
3317
+ return /* @__PURE__ */ jsxRuntime.jsx(PageLayout, { data, gatewayUrl, renderSection });
1878
3318
  }
1879
3319
  function PageLayout({
1880
3320
  data,
3321
+ gatewayUrl,
1881
3322
  renderSection
1882
3323
  }) {
1883
3324
  const { landing_page: lp, store } = data;
@@ -1893,15 +3334,16 @@ function PageLayout({
1893
3334
  const fontFamily = lp.font_family || theme.fontDisplay || "system-ui, -apple-system, sans-serif";
1894
3335
  const logoUrl = store?.logo_url;
1895
3336
  const sorted = [...lp.sections].sort((a, b) => a.order - b.order);
3337
+ const sectionData = { ...data, gatewayUrl, landing_page: { slug: lp.slug } };
1896
3338
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minHeight: "100dvh", background: theme.bg, color: theme.fg, fontFamily }, children: [
1897
3339
  lp.custom_css && /* @__PURE__ */ jsxRuntime.jsx("style", { children: lp.custom_css }),
1898
3340
  logoUrl && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: logoUrl, alt: store?.name || "Store", style: { height: 40, objectFit: "contain" } }) }),
1899
3341
  sorted.map((section) => {
1900
- const defaultRenderer = () => /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data, theme }, section.id);
3342
+ const defaultRenderer = () => /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data: sectionData, theme }, section.id);
1901
3343
  if (renderSection) {
1902
3344
  return /* @__PURE__ */ jsxRuntime.jsx("div", { children: renderSection(section, defaultRenderer) }, section.id);
1903
3345
  }
1904
- return /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data, theme }, section.id);
3346
+ return /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data: sectionData, theme }, section.id);
1905
3347
  }),
1906
3348
  store?.name && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "2rem 1.5rem", borderTop: `1px solid ${theme.surface}`, textAlign: "center" }, children: /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { fontSize: "0.75rem", color: theme.muted, margin: 0 }, children: [
1907
3349
  "Powered by ",
@@ -1947,11 +3389,14 @@ function DefaultError2({ message }) {
1947
3389
 
1948
3390
  exports.AnalyticsTracker = AnalyticsTracker;
1949
3391
  exports.AuthInitializer = AuthInitializer;
3392
+ exports.BehavioralTrackerComponent = BehavioralTrackerComponent;
1950
3393
  exports.CartInitializer = CartInitializer;
3394
+ exports.FingerprintCollector = FingerprintCollector;
1951
3395
  exports.LandingPage = LandingPage;
1952
3396
  exports.PixelInitializer = PixelInitializer;
1953
3397
  exports.QRLandingPage = QRLandingPage;
1954
3398
  exports.SectionRenderer = SectionRenderer;
3399
+ exports.SessionRecorderComponent = SessionRecorderComponent;
1955
3400
  exports.WhaleContext = WhaleContext;
1956
3401
  exports.WhaleProvider = WhaleProvider;
1957
3402
  exports.useAnalytics = useAnalytics;
@@ -1964,11 +3409,13 @@ exports.useCheckout = useCheckout;
1964
3409
  exports.useCoupons = useCoupons;
1965
3410
  exports.useCustomerAnalytics = useCustomerAnalytics;
1966
3411
  exports.useCustomerOrders = useCustomerOrders;
3412
+ exports.useDeals = useDeals;
1967
3413
  exports.useLocations = useLocations;
1968
3414
  exports.useLoyalty = useLoyalty;
1969
3415
  exports.useProduct = useProduct;
1970
3416
  exports.useProducts = useProducts;
1971
3417
  exports.useRecommendations = useRecommendations;
3418
+ exports.useReferral = useReferral;
1972
3419
  exports.useReviews = useReviews;
1973
3420
  exports.useSearch = useSearch;
1974
3421
  exports.useShipping = useShipping;