@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,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-3Q7CPJBA.js';
3
3
  import { createContext, useContext, useRef, useCallback, useEffect, useState, useMemo } from 'react';
4
4
  import { usePathname } from 'next/navigation';
5
5
  import { createStore } from 'zustand/vanilla';
@@ -10,7 +10,7 @@ import { ThemeProvider } from '@neowhale/ui';
10
10
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
11
11
 
12
12
  var WhaleContext = createContext(null);
13
- function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
13
+ function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart, onCartChange) {
14
14
  return createStore()(
15
15
  persist(
16
16
  (set, get) => ({
@@ -98,6 +98,8 @@ function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
98
98
  }
99
99
  await get().syncCart();
100
100
  onAddToCart?.(productId, productName || "", quantity, unitPrice || 0, tier);
101
+ const state = get();
102
+ if (state.cartId) onCartChange?.(state.cartId, state.total, state.itemCount);
101
103
  } finally {
102
104
  set({ cartLoading: false, addItemInFlight: false });
103
105
  }
@@ -109,6 +111,8 @@ function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
109
111
  if (!cartId) return;
110
112
  await client.updateCartItem(cartId, itemId, quantity);
111
113
  await get().syncCart();
114
+ const state = get();
115
+ if (state.cartId) onCartChange?.(state.cartId, state.total, state.itemCount);
112
116
  } finally {
113
117
  set({ cartLoading: false });
114
118
  }
@@ -124,6 +128,8 @@ function createCartStore(client, storagePrefix, onAddToCart, onRemoveFromCart) {
124
128
  if (item) {
125
129
  onRemoveFromCart?.(item.product_id, productName || item.product_name);
126
130
  }
131
+ const state = get();
132
+ if (state.cartId) onCartChange?.(state.cartId, state.total, state.itemCount);
127
133
  } finally {
128
134
  set({ cartLoading: false });
129
135
  }
@@ -237,6 +243,12 @@ function createAuthStore(client, storagePrefix) {
237
243
  set({ authLoading: false });
238
244
  }
239
245
  },
246
+ updateProfile: async (data) => {
247
+ const customer = get().customer;
248
+ if (!customer?.id) throw new Error("Not authenticated");
249
+ const updated = await client.updateProfile(customer.id, data);
250
+ set({ customer: updated });
251
+ },
240
252
  restoreSession: async () => {
241
253
  const { sessionToken, sessionExpiresAt, customer } = get();
242
254
  if (!sessionToken || !sessionExpiresAt) return;
@@ -297,6 +309,38 @@ function createAuthStore(client, storagePrefix) {
297
309
  );
298
310
  }
299
311
  var SESSION_KEY_SUFFIX = "-analytics-session";
312
+ var VISITOR_KEY_SUFFIX = "-visitor-id";
313
+ function parseMarketingParams() {
314
+ if (typeof window === "undefined") return {};
315
+ const params = new URLSearchParams(window.location.search);
316
+ const result = {};
317
+ for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term", "gclid", "fbclid"]) {
318
+ const val = params.get(key);
319
+ if (val) result[key] = val;
320
+ }
321
+ return result;
322
+ }
323
+ function getVisitorId(prefix) {
324
+ const key = `${prefix}${VISITOR_KEY_SUFFIX}`;
325
+ try {
326
+ const existing = localStorage.getItem(key);
327
+ if (existing) return existing;
328
+ } catch {
329
+ }
330
+ const id = `v-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
331
+ try {
332
+ localStorage.setItem(key, id);
333
+ } catch {
334
+ }
335
+ return id;
336
+ }
337
+ function detectDevice() {
338
+ if (typeof navigator === "undefined") return "unknown";
339
+ const ua = navigator.userAgent;
340
+ if (/Mobi|Android/i.test(ua)) return "mobile";
341
+ if (/Tablet|iPad/i.test(ua)) return "tablet";
342
+ return "desktop";
343
+ }
300
344
  function useAnalytics() {
301
345
  const ctx = useContext(WhaleContext);
302
346
  if (!ctx) throw new Error("useAnalytics must be used within <WhaleProvider>");
@@ -319,9 +363,15 @@ function useAnalytics() {
319
363
  } catch {
320
364
  }
321
365
  try {
366
+ const marketing = parseMarketingParams();
367
+ const visitorId = getVisitorId(config.storagePrefix);
322
368
  const session = await client.createSession({
369
+ visitor_id: visitorId,
323
370
  user_agent: navigator.userAgent,
324
- referrer: document.referrer || void 0
371
+ referrer: document.referrer || void 0,
372
+ page_url: window.location.href,
373
+ device: detectDevice(),
374
+ ...marketing
325
375
  });
326
376
  if (session?.id) {
327
377
  localStorage.setItem(sessionKey, JSON.stringify({ id: session.id, createdAt: Date.now() }));
@@ -346,10 +396,12 @@ function useAnalytics() {
346
396
  pixelManager?.track(eventType, { ...data, eventID: eventId });
347
397
  try {
348
398
  const sessionId = await getOrCreateSession();
399
+ const visitorId = getVisitorId(config.storagePrefix);
349
400
  await client.trackEvent({
350
401
  session_id: sessionId,
351
402
  event_type: eventType,
352
- event_data: { ...data, event_id: eventId }
403
+ event_data: { ...data, event_id: eventId },
404
+ visitor_id: visitorId
353
405
  });
354
406
  } catch {
355
407
  }
@@ -368,9 +420,38 @@ function useAnalytics() {
368
420
  },
369
421
  [client, getOrCreateSession, trackingEnabled]
370
422
  );
423
+ const updateSessionCart = useCallback(
424
+ async (cartId, cartTotal, cartItemCount) => {
425
+ if (!trackingEnabled) return;
426
+ try {
427
+ const sessionId = await getOrCreateSession();
428
+ if (sessionId.startsWith("local-")) return;
429
+ await client.updateSession(sessionId, {
430
+ cart_id: cartId,
431
+ cart_total: cartTotal,
432
+ cart_item_count: cartItemCount,
433
+ status: "carting"
434
+ });
435
+ } catch {
436
+ }
437
+ },
438
+ [client, getOrCreateSession, trackingEnabled]
439
+ );
440
+ const updateSessionOrder = useCallback(
441
+ async (orderId) => {
442
+ if (!trackingEnabled) return;
443
+ try {
444
+ const sessionId = await getOrCreateSession();
445
+ if (sessionId.startsWith("local-")) return;
446
+ await client.updateSession(sessionId, { order_id: orderId, status: "converted" });
447
+ } catch {
448
+ }
449
+ },
450
+ [client, getOrCreateSession, trackingEnabled]
451
+ );
371
452
  const trackPageView = useCallback(
372
453
  (url, referrer) => {
373
- track("page_view", { url, referrer });
454
+ track("page_view", { url, referrer, page_url: url });
374
455
  },
375
456
  [track]
376
457
  );
@@ -427,6 +508,8 @@ function useAnalytics() {
427
508
  trackAddToCart,
428
509
  trackRemoveFromCart,
429
510
  linkCustomer,
511
+ updateSessionCart,
512
+ updateSessionOrder,
430
513
  getOrCreateSession,
431
514
  /** Whether tracking is globally enabled for this storefront */
432
515
  trackingEnabled,
@@ -444,6 +527,7 @@ function useAuth() {
444
527
  isAuthenticated: s.isSessionValid(),
445
528
  sendCode: s.sendOTP,
446
529
  verifyCode: s.verifyOTP,
530
+ updateProfile: s.updateProfile,
447
531
  restoreSession: s.restoreSession,
448
532
  logout: s.logout,
449
533
  fetchCustomer: s.fetchCustomer
@@ -463,7 +547,8 @@ function AnalyticsTracker({ pathname }) {
463
547
  if (pathname === prevPathname.current) return;
464
548
  const referrer = prevPathname.current || (typeof document !== "undefined" ? document.referrer : "");
465
549
  prevPathname.current = pathname;
466
- 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
  }
@@ -762,12 +1859,12 @@ function useCheckout() {
762
1859
  setLoading(false);
763
1860
  }
764
1861
  }, [ctx.client, session]);
765
- const complete = useCallback(async (payment) => {
1862
+ const complete = useCallback(async (payment, opts) => {
766
1863
  if (!session) throw new Error("No active checkout session");
767
1864
  setLoading(true);
768
1865
  setError(null);
769
1866
  try {
770
- const order = await ctx.client.completeCheckout(session.id, payment);
1867
+ const order = await ctx.client.completeCheckout(session.id, payment, opts);
771
1868
  setSession(null);
772
1869
  return order;
773
1870
  } catch (err) {
@@ -921,7 +2018,11 @@ function useLoyalty() {
921
2018
  await refresh();
922
2019
  return result;
923
2020
  }, [customer?.id, ctx.client, refresh]);
924
- return { account, rewards, transactions, loading, error, refresh, redeemReward };
2021
+ const fetchProductsByCategory = useCallback(async (category, locationId, tier) => {
2022
+ const res = await ctx.client.listLoyaltyProducts({ category, location_id: locationId, tier });
2023
+ return res.data;
2024
+ }, [ctx.client]);
2025
+ return { account, rewards, transactions, loading, error, refresh, redeemReward, fetchProductsByCategory };
925
2026
  }
926
2027
  function useReviews(productId) {
927
2028
  const ctx = useContext(WhaleContext);
@@ -1149,9 +2250,9 @@ function useShipping() {
1149
2250
  }, [ctx.client]);
1150
2251
  return { methods, rates, loading, error, refreshMethods, calculateRates };
1151
2252
  }
1152
- function useCoupons() {
2253
+ function useDeals() {
1153
2254
  const ctx = useContext(WhaleContext);
1154
- if (!ctx) throw new Error("useCoupons must be used within <WhaleProvider>");
2255
+ if (!ctx) throw new Error("useDeals must be used within <WhaleProvider>");
1155
2256
  const [validation, setValidation] = useState(null);
1156
2257
  const [loading, setLoading] = useState(false);
1157
2258
  const [error, setError] = useState(null);
@@ -1159,7 +2260,7 @@ function useCoupons() {
1159
2260
  setLoading(true);
1160
2261
  setError(null);
1161
2262
  try {
1162
- const result = await ctx.client.validateCoupon(code, cartId ? { cart_id: cartId } : void 0);
2263
+ const result = await ctx.client.validateDeal(code, cartId ? { cart_id: cartId } : void 0);
1163
2264
  setValidation(result);
1164
2265
  return result;
1165
2266
  } catch (err) {
@@ -1175,7 +2276,7 @@ function useCoupons() {
1175
2276
  setLoading(true);
1176
2277
  setError(null);
1177
2278
  try {
1178
- const cart = await ctx.client.applyCoupon(cartId, code);
2279
+ const cart = await ctx.client.applyDeal(cartId, code);
1179
2280
  return cart;
1180
2281
  } catch (err) {
1181
2282
  const e = err instanceof Error ? err : new Error(String(err));
@@ -1189,7 +2290,7 @@ function useCoupons() {
1189
2290
  setLoading(true);
1190
2291
  setError(null);
1191
2292
  try {
1192
- const cart = await ctx.client.removeCoupon(cartId);
2293
+ const cart = await ctx.client.removeDeal(cartId);
1193
2294
  setValidation(null);
1194
2295
  return cart;
1195
2296
  } catch (err) {
@@ -1206,16 +2307,133 @@ function useCoupons() {
1206
2307
  }, []);
1207
2308
  return { validation, loading, error, validate, apply, remove, clear };
1208
2309
  }
2310
+ var useCoupons = useDeals;
2311
+ function useReferral() {
2312
+ const ctx = useContext(WhaleContext);
2313
+ if (!ctx) throw new Error("useReferral must be used within <WhaleProvider>");
2314
+ const customer = useStore(ctx.authStore, (s) => s.customer);
2315
+ const [status, setStatus] = useState(null);
2316
+ const [loading, setLoading] = useState(false);
2317
+ const [error, setError] = useState(null);
2318
+ const refresh = useCallback(async () => {
2319
+ if (!customer?.id) {
2320
+ setStatus(null);
2321
+ return;
2322
+ }
2323
+ setLoading(true);
2324
+ setError(null);
2325
+ try {
2326
+ const result = await ctx.client.getReferralStatus(customer.id);
2327
+ setStatus(result);
2328
+ } catch (err) {
2329
+ setError(err instanceof Error ? err : new Error(String(err)));
2330
+ } finally {
2331
+ setLoading(false);
2332
+ }
2333
+ }, [customer?.id, ctx.client]);
2334
+ useEffect(() => {
2335
+ refresh();
2336
+ }, [refresh]);
2337
+ const enroll = useCallback(async () => {
2338
+ if (!customer?.id) throw new Error("Not authenticated");
2339
+ setLoading(true);
2340
+ setError(null);
2341
+ try {
2342
+ const result = await ctx.client.enrollReferral(customer.id);
2343
+ await refresh();
2344
+ return result;
2345
+ } catch (err) {
2346
+ const e = err instanceof Error ? err : new Error(String(err));
2347
+ setError(e);
2348
+ throw e;
2349
+ } finally {
2350
+ setLoading(false);
2351
+ }
2352
+ }, [customer?.id, ctx.client, refresh]);
2353
+ const attributeReferral = useCallback(
2354
+ async (code) => {
2355
+ if (!customer?.id) throw new Error("Not authenticated");
2356
+ setLoading(true);
2357
+ setError(null);
2358
+ try {
2359
+ const result = await ctx.client.attributeReferral(customer.id, code);
2360
+ await refresh();
2361
+ return result;
2362
+ } catch (err) {
2363
+ const e = err instanceof Error ? err : new Error(String(err));
2364
+ setError(e);
2365
+ throw e;
2366
+ } finally {
2367
+ setLoading(false);
2368
+ }
2369
+ },
2370
+ [customer?.id, ctx.client, refresh]
2371
+ );
2372
+ useEffect(() => {
2373
+ if (!customer?.id || !status || status.referred_by) return;
2374
+ if (typeof window === "undefined") return;
2375
+ const params = new URLSearchParams(window.location.search);
2376
+ const code = params.get("code") || localStorage.getItem("whale_ref_code");
2377
+ if (!code) return;
2378
+ ctx.client.attributeReferral(customer.id, code).then(() => {
2379
+ localStorage.removeItem("whale_ref_code");
2380
+ refresh();
2381
+ }).catch(() => {
2382
+ });
2383
+ }, [customer?.id, status, ctx.client, refresh]);
2384
+ const share = useCallback(async () => {
2385
+ if (!status?.share_url) throw new Error("Not enrolled in referral program");
2386
+ const shareData = {
2387
+ title: "Check this out!",
2388
+ text: `Use my referral code ${status.referral_code} for rewards!`,
2389
+ url: status.share_url
2390
+ };
2391
+ if (typeof navigator !== "undefined" && navigator.share) {
2392
+ try {
2393
+ await navigator.share(shareData);
2394
+ return;
2395
+ } catch {
2396
+ }
2397
+ }
2398
+ if (typeof navigator !== "undefined" && navigator.clipboard) {
2399
+ await navigator.clipboard.writeText(status.share_url);
2400
+ }
2401
+ }, [status]);
2402
+ return {
2403
+ status,
2404
+ loading,
2405
+ error,
2406
+ enroll,
2407
+ refresh,
2408
+ share,
2409
+ attributeReferral,
2410
+ referralCode: status?.referral_code ?? null,
2411
+ shareUrl: status?.share_url ?? null,
2412
+ isEnrolled: status?.enrolled ?? false,
2413
+ referredBy: status?.referred_by ?? null
2414
+ };
2415
+ }
2416
+ function trackClick(tracking, label, url, position) {
2417
+ if (!tracking?.gatewayUrl || !tracking?.code) return;
2418
+ const body = JSON.stringify({ label, url, position });
2419
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
2420
+ navigator.sendBeacon(
2421
+ `${tracking.gatewayUrl}/q/${encodeURIComponent(tracking.code)}/click`,
2422
+ new Blob([body], { type: "application/json" })
2423
+ );
2424
+ }
2425
+ }
1209
2426
  function SectionRenderer({
1210
2427
  section,
1211
2428
  data,
1212
- theme
2429
+ theme,
2430
+ tracking
1213
2431
  }) {
1214
2432
  const [showCOA, setShowCOA] = useState(false);
1215
2433
  const el = (() => {
1216
2434
  switch (section.type) {
1217
2435
  case "hero":
1218
- return /* @__PURE__ */ jsx(HeroSection, { section, theme });
2436
+ return /* @__PURE__ */ jsx(HeroSection, { section, theme, tracking });
1219
2437
  case "text":
1220
2438
  return /* @__PURE__ */ jsx(TextSection, { section, theme });
1221
2439
  case "image":
@@ -1225,15 +2443,17 @@ function SectionRenderer({
1225
2443
  case "gallery":
1226
2444
  return /* @__PURE__ */ jsx(GallerySection, { section, theme });
1227
2445
  case "cta":
1228
- return /* @__PURE__ */ jsx(CTASection, { section, theme });
2446
+ return /* @__PURE__ */ jsx(CTASection, { section, theme, tracking });
1229
2447
  case "stats":
1230
2448
  return /* @__PURE__ */ jsx(StatsSection, { section, theme });
1231
2449
  case "product_card":
1232
- return /* @__PURE__ */ jsx(ProductCardSection, { section, data, theme });
2450
+ return /* @__PURE__ */ jsx(ProductCardSection, { section, data, theme, tracking });
1233
2451
  case "coa_viewer":
1234
- return /* @__PURE__ */ jsx(COAViewerSection, { section, data, theme, onShowCOA: () => setShowCOA(true) });
2452
+ return /* @__PURE__ */ jsx(COAViewerSection, { section, data, theme, onShowCOA: () => setShowCOA(true), tracking });
1235
2453
  case "social_links":
1236
2454
  return /* @__PURE__ */ jsx(SocialLinksSection, { section, theme });
2455
+ case "lead_capture":
2456
+ return /* @__PURE__ */ jsx(LeadCaptureSection, { section, data, theme });
1237
2457
  case "divider":
1238
2458
  return /* @__PURE__ */ jsx(DividerSection, { theme });
1239
2459
  default:
@@ -1245,7 +2465,7 @@ function SectionRenderer({
1245
2465
  showCOA && data?.coa && /* @__PURE__ */ jsx(COAModal, { coa: data.coa, theme, onClose: () => setShowCOA(false) })
1246
2466
  ] });
1247
2467
  }
1248
- function HeroSection({ section, theme }) {
2468
+ function HeroSection({ section, theme, tracking }) {
1249
2469
  const { title, subtitle, background_image, cta_text, cta_url } = section.content;
1250
2470
  return /* @__PURE__ */ jsxs(
1251
2471
  "div",
@@ -1287,6 +2507,7 @@ function HeroSection({ section, theme }) {
1287
2507
  "a",
1288
2508
  {
1289
2509
  href: cta_url,
2510
+ onClick: () => trackClick(tracking, cta_text, cta_url),
1290
2511
  style: {
1291
2512
  display: "inline-block",
1292
2513
  padding: "0.875rem 2rem",
@@ -1372,7 +2593,7 @@ function GallerySection({ section, theme }) {
1372
2593
  }
1373
2594
  ) }, i)) }) });
1374
2595
  }
1375
- function CTASection({ section, theme }) {
2596
+ function CTASection({ section, theme, tracking }) {
1376
2597
  const { buttons } = section.content;
1377
2598
  if (!buttons || buttons.length === 0) return null;
1378
2599
  return /* @__PURE__ */ jsx("div", { style: { padding: "2rem 1.5rem", maxWidth: 480, margin: "0 auto", display: "flex", flexDirection: "column", gap: "0.75rem" }, children: buttons.map((btn, i) => {
@@ -1381,6 +2602,7 @@ function CTASection({ section, theme }) {
1381
2602
  "a",
1382
2603
  {
1383
2604
  href: btn.url,
2605
+ onClick: () => trackClick(tracking, btn.text, btn.url, i),
1384
2606
  style: {
1385
2607
  display: "block",
1386
2608
  width: "100%",
@@ -1452,7 +2674,7 @@ function StatsSection({ section, theme }) {
1452
2674
  }, children: stat.label })
1453
2675
  ] }, i)) }) });
1454
2676
  }
1455
- function ProductCardSection({ section, data, theme }) {
2677
+ function ProductCardSection({ section, data, theme, tracking }) {
1456
2678
  const product = data?.product;
1457
2679
  const c = section.content;
1458
2680
  const name = c.name || product?.name || "";
@@ -1468,6 +2690,7 @@ function ProductCardSection({ section, data, theme }) {
1468
2690
  "a",
1469
2691
  {
1470
2692
  href: url,
2693
+ onClick: () => trackClick(tracking, "View Product", url),
1471
2694
  style: {
1472
2695
  display: "block",
1473
2696
  width: "100%",
@@ -1492,7 +2715,8 @@ function COAViewerSection({
1492
2715
  section,
1493
2716
  data,
1494
2717
  theme,
1495
- onShowCOA
2718
+ onShowCOA,
2719
+ tracking
1496
2720
  }) {
1497
2721
  const coa = data?.coa;
1498
2722
  const c = section.content;
@@ -1513,10 +2737,200 @@ function COAViewerSection({
1513
2737
  display: "block",
1514
2738
  boxSizing: "border-box"
1515
2739
  };
2740
+ const buttonLabel = c.button_text || "View Lab Results";
1516
2741
  if (coa.viewer_url) {
1517
- return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsx("a", { href: coa.viewer_url, target: "_blank", rel: "noopener noreferrer", style: buttonStyle, children: c.button_text || "View Lab Results" }) });
2742
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsx(
2743
+ "a",
2744
+ {
2745
+ href: coa.viewer_url,
2746
+ target: "_blank",
2747
+ rel: "noopener noreferrer",
2748
+ onClick: () => trackClick(tracking, buttonLabel, coa.viewer_url),
2749
+ style: buttonStyle,
2750
+ children: buttonLabel
2751
+ }
2752
+ ) });
1518
2753
  }
1519
- return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsx("button", { onClick: onShowCOA, style: buttonStyle, children: c.button_text || "View Lab Results" }) });
2754
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsx("button", { onClick: () => {
2755
+ trackClick(tracking, buttonLabel, coa.url);
2756
+ onShowCOA();
2757
+ }, style: buttonStyle, children: buttonLabel }) });
2758
+ }
2759
+ function LeadCaptureSection({ section, data, theme }) {
2760
+ const c = section.content;
2761
+ const [firstName, setFirstName] = useState("");
2762
+ const [email, setEmail] = useState("");
2763
+ const [status, setStatus] = useState("idle");
2764
+ const [errorMsg, setErrorMsg] = useState("");
2765
+ const gatewayUrl = c.gateway_url || data.gatewayUrl || "https://whale-gateway.fly.dev";
2766
+ const storeId = c.store_id || data.store?.id;
2767
+ const slug = c.landing_page_slug || data.landing_page?.slug;
2768
+ async function handleSubmit(e) {
2769
+ e.preventDefault();
2770
+ if (!email || !storeId) return;
2771
+ setStatus("loading");
2772
+ setErrorMsg("");
2773
+ try {
2774
+ const res = await fetch(`${gatewayUrl}/v1/stores/${storeId}/storefront/leads`, {
2775
+ method: "POST",
2776
+ headers: { "Content-Type": "application/json" },
2777
+ body: JSON.stringify({
2778
+ email,
2779
+ first_name: firstName || void 0,
2780
+ source: c.source || "landing_page",
2781
+ landing_page_slug: slug || void 0,
2782
+ tags: c.tags || void 0
2783
+ })
2784
+ });
2785
+ if (!res.ok) {
2786
+ const body = await res.json().catch(() => ({}));
2787
+ throw new Error(body?.error?.message || "Something went wrong. Please try again.");
2788
+ }
2789
+ setStatus("success");
2790
+ } catch (err) {
2791
+ setErrorMsg(err instanceof Error ? err.message : "Something went wrong. Please try again.");
2792
+ setStatus("error");
2793
+ }
2794
+ }
2795
+ const heading = c.heading || "get 10% off your first visit.";
2796
+ const subtitle = c.subtitle || "drop your email and we will send you the code.";
2797
+ const buttonText = c.button_text || "Claim My Discount";
2798
+ const successHeading = c.success_heading || "You\u2019re in!";
2799
+ const successMessage = c.success_message || "Check your inbox for the discount code.";
2800
+ const inputStyle = {
2801
+ flex: 1,
2802
+ minWidth: 0,
2803
+ padding: "0.875rem 1rem",
2804
+ background: theme.surface,
2805
+ border: `1px solid ${theme.fg}15`,
2806
+ color: theme.fg,
2807
+ fontSize: "0.95rem",
2808
+ fontWeight: 300,
2809
+ outline: "none",
2810
+ boxSizing: "border-box",
2811
+ fontFamily: "inherit",
2812
+ transition: "border-color 0.2s"
2813
+ };
2814
+ return /* @__PURE__ */ jsxs("div", { style: { padding: "3.5rem 1.5rem", maxWidth: 560, margin: "0 auto" }, children: [
2815
+ /* @__PURE__ */ jsx("style", { children: `@keyframes lc-spin { to { transform: rotate(360deg) } }` }),
2816
+ /* @__PURE__ */ jsx("div", { style: {
2817
+ background: theme.surface,
2818
+ border: `1px solid ${theme.fg}12`,
2819
+ padding: "clamp(2rem, 6vw, 3rem)"
2820
+ }, children: status === "success" ? /* @__PURE__ */ jsxs("div", { style: { textAlign: "center" }, children: [
2821
+ /* @__PURE__ */ jsx("h2", { style: {
2822
+ fontSize: "clamp(1.5rem, 5vw, 2rem)",
2823
+ fontWeight: 300,
2824
+ fontFamily: theme.fontDisplay || "inherit",
2825
+ margin: "0 0 0.75rem",
2826
+ lineHeight: 1.2,
2827
+ letterSpacing: "-0.02em",
2828
+ color: theme.fg
2829
+ }, children: successHeading }),
2830
+ /* @__PURE__ */ jsx("p", { style: {
2831
+ fontSize: "0.9rem",
2832
+ color: `${theme.fg}99`,
2833
+ margin: "0 0 1.5rem",
2834
+ lineHeight: 1.6,
2835
+ fontWeight: 300
2836
+ }, children: successMessage }),
2837
+ c.coupon_code && /* @__PURE__ */ jsx("div", { style: {
2838
+ display: "inline-block",
2839
+ padding: "0.75rem 2rem",
2840
+ background: `${theme.fg}08`,
2841
+ border: `1px dashed ${theme.fg}30`,
2842
+ fontSize: "clamp(1.25rem, 4vw, 1.75rem)",
2843
+ fontWeight: 500,
2844
+ fontFamily: "monospace",
2845
+ letterSpacing: "0.12em",
2846
+ color: theme.accent
2847
+ }, children: c.coupon_code })
2848
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2849
+ /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", marginBottom: "clamp(1.5rem, 4vw, 2rem)" }, children: [
2850
+ /* @__PURE__ */ jsx("h2", { style: {
2851
+ fontSize: "clamp(1.5rem, 5vw, 2.25rem)",
2852
+ fontWeight: 300,
2853
+ fontFamily: theme.fontDisplay || "inherit",
2854
+ margin: "0 0 0.5rem",
2855
+ lineHeight: 1.15,
2856
+ letterSpacing: "-0.02em",
2857
+ color: theme.fg
2858
+ }, children: heading }),
2859
+ /* @__PURE__ */ jsx("p", { style: {
2860
+ fontSize: "0.85rem",
2861
+ color: theme.accent,
2862
+ margin: 0,
2863
+ lineHeight: 1.6,
2864
+ textTransform: "uppercase",
2865
+ letterSpacing: "0.15em"
2866
+ }, children: subtitle })
2867
+ ] }),
2868
+ /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, style: { display: "flex", flexDirection: "column", gap: "0.75rem" }, children: [
2869
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "0.75rem", flexWrap: "wrap" }, children: [
2870
+ /* @__PURE__ */ jsx(
2871
+ "input",
2872
+ {
2873
+ type: "text",
2874
+ placeholder: "First name",
2875
+ value: firstName,
2876
+ onChange: (e) => setFirstName(e.target.value),
2877
+ style: inputStyle
2878
+ }
2879
+ ),
2880
+ /* @__PURE__ */ jsx(
2881
+ "input",
2882
+ {
2883
+ type: "email",
2884
+ placeholder: "Email address",
2885
+ value: email,
2886
+ onChange: (e) => setEmail(e.target.value),
2887
+ required: true,
2888
+ style: inputStyle
2889
+ }
2890
+ )
2891
+ ] }),
2892
+ status === "error" && errorMsg && /* @__PURE__ */ jsx("p", { style: { fontSize: "0.8rem", color: "#e55", margin: 0, fontWeight: 400 }, children: errorMsg }),
2893
+ /* @__PURE__ */ jsxs(
2894
+ "button",
2895
+ {
2896
+ type: "submit",
2897
+ disabled: status === "loading",
2898
+ style: {
2899
+ width: "100%",
2900
+ padding: "0.875rem",
2901
+ background: theme.fg,
2902
+ color: theme.bg,
2903
+ border: "none",
2904
+ fontSize: "0.85rem",
2905
+ fontWeight: 500,
2906
+ cursor: status === "loading" ? "wait" : "pointer",
2907
+ letterSpacing: "0.08em",
2908
+ textTransform: "uppercase",
2909
+ fontFamily: "inherit",
2910
+ display: "flex",
2911
+ alignItems: "center",
2912
+ justifyContent: "center",
2913
+ gap: "0.5rem",
2914
+ opacity: status === "loading" ? 0.7 : 1,
2915
+ transition: "opacity 0.2s"
2916
+ },
2917
+ children: [
2918
+ status === "loading" && /* @__PURE__ */ jsx("span", { style: {
2919
+ display: "inline-block",
2920
+ width: 16,
2921
+ height: 16,
2922
+ border: `2px solid ${theme.bg}40`,
2923
+ borderTopColor: theme.bg,
2924
+ borderRadius: "50%",
2925
+ animation: "lc-spin 0.8s linear infinite"
2926
+ } }),
2927
+ buttonText
2928
+ ]
2929
+ }
2930
+ )
2931
+ ] })
2932
+ ] }) })
2933
+ ] });
1520
2934
  }
1521
2935
  function SocialLinksSection({ section, theme }) {
1522
2936
  const { links } = section.content;
@@ -1639,15 +3053,16 @@ function QRLandingPage({
1639
3053
  const logoUrl = data.qr_code.logo_url || data.store?.logo_url;
1640
3054
  const storeName = data.store?.name;
1641
3055
  const sorted = [...sections].sort((a, b) => a.order - b.order);
3056
+ const tracking = { gatewayUrl, code };
1642
3057
  return /* @__PURE__ */ jsxs("div", { style: { minHeight: "100dvh", background: theme.bg, color: theme.fg, fontFamily }, children: [
1643
3058
  lp?.custom_css && /* @__PURE__ */ jsx("style", { children: lp.custom_css }),
1644
3059
  logoUrl && /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsx("img", { src: logoUrl, alt: storeName || "Store", style: { height: 40, objectFit: "contain" } }) }),
1645
3060
  sorted.map((section) => {
1646
- const defaultRenderer = () => /* @__PURE__ */ jsx(SectionRenderer, { section, data, theme }, section.id);
3061
+ const defaultRenderer = () => /* @__PURE__ */ jsx(SectionRenderer, { section, data, theme, tracking }, section.id);
1647
3062
  if (renderSection) {
1648
3063
  return /* @__PURE__ */ jsx("div", { children: renderSection(section, defaultRenderer) }, section.id);
1649
3064
  }
1650
- return /* @__PURE__ */ jsx(SectionRenderer, { section, data, theme }, section.id);
3065
+ return /* @__PURE__ */ jsx(SectionRenderer, { section, data, theme, tracking }, section.id);
1651
3066
  }),
1652
3067
  storeName && /* @__PURE__ */ jsx("div", { style: { padding: "2rem 1.5rem", borderTop: `1px solid ${theme.surface}`, textAlign: "center" }, children: /* @__PURE__ */ jsxs("p", { style: { fontSize: "0.75rem", color: theme.muted, margin: 0 }, children: [
1653
3068
  storeName,
@@ -1675,10 +3090,10 @@ function buildDefaultSections(data) {
1675
3090
  let order = 0;
1676
3091
  const productName = qr.landing_page.title || product?.name || qr.name;
1677
3092
  const productImage = qr.landing_page.image_url || product?.featured_image || null;
1678
- const description = qr.landing_page.description || product?.description || "";
1679
- const ctaUrl = qr.landing_page.cta_url || qr.destination_url;
3093
+ const description = product?.description || "";
1680
3094
  const categoryName = product?.category_name ?? null;
1681
3095
  const strainType = toStr(cf?.strain_type);
3096
+ const tagline = toStr(cf?.tagline);
1682
3097
  if (productImage) {
1683
3098
  sections.push({
1684
3099
  id: "auto-hero",
@@ -1687,9 +3102,7 @@ function buildDefaultSections(data) {
1687
3102
  content: {
1688
3103
  title: productName,
1689
3104
  subtitle: [categoryName, strainType].filter(Boolean).join(" \xB7 "),
1690
- background_image: productImage,
1691
- cta_text: qr.landing_page.cta_text || null,
1692
- cta_url: ctaUrl
3105
+ background_image: productImage
1693
3106
  }
1694
3107
  });
1695
3108
  } else {
@@ -1704,6 +3117,15 @@ function buildDefaultSections(data) {
1704
3117
  config: { align: "center" }
1705
3118
  });
1706
3119
  }
3120
+ if (tagline) {
3121
+ sections.push({
3122
+ id: "auto-tagline",
3123
+ type: "text",
3124
+ order: order++,
3125
+ content: { body: tagline },
3126
+ config: { align: "center" }
3127
+ });
3128
+ }
1707
3129
  const thca = toNum(cf?.thca_percentage);
1708
3130
  const thc = toNum(cf?.d9_percentage);
1709
3131
  const cbd = toNum(cf?.cbd_total);
@@ -1719,23 +3141,23 @@ function buildDefaultSections(data) {
1719
3141
  content: { stats }
1720
3142
  });
1721
3143
  }
1722
- const details = [];
3144
+ const profileDetails = [];
1723
3145
  const genetics = toStr(cf?.genetics);
1724
3146
  const terpenes = toStr(cf?.terpenes);
1725
3147
  const effects = toStr(cf?.effects);
1726
- const 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) {
3148
+ const flavorProfile = toStr(cf?.flavor_profile);
3149
+ const bestFor = toStr(cf?.best_for);
3150
+ if (genetics) profileDetails.push({ label: "Genetics", value: genetics });
3151
+ if (terpenes) profileDetails.push({ label: "Terpenes", value: terpenes });
3152
+ if (effects) profileDetails.push({ label: "Effects", value: effects });
3153
+ if (flavorProfile) profileDetails.push({ label: "Flavor", value: flavorProfile });
3154
+ if (bestFor) profileDetails.push({ label: "Best For", value: bestFor });
3155
+ if (profileDetails.length > 0) {
1734
3156
  sections.push({
1735
- id: "auto-details",
3157
+ id: "auto-profile",
1736
3158
  type: "stats",
1737
3159
  order: order++,
1738
- content: { stats: details },
3160
+ content: { stats: profileDetails },
1739
3161
  config: { layout: "list" }
1740
3162
  });
1741
3163
  }
@@ -1755,16 +3177,34 @@ function buildDefaultSections(data) {
1755
3177
  content: { button_text: "View Lab Results" }
1756
3178
  });
1757
3179
  }
1758
- if (ctaUrl) {
3180
+ const labDetails = [];
3181
+ const batchNumber = toStr(cf?.batch_number);
3182
+ const dateTested = toStr(cf?.date_tested);
3183
+ if (batchNumber) labDetails.push({ label: "Batch", value: batchNumber });
3184
+ if (dateTested) labDetails.push({ label: "Tested", value: formatDate(dateTested) });
3185
+ if (labDetails.length > 0) {
1759
3186
  sections.push({
1760
- id: "auto-cta",
1761
- type: "cta",
3187
+ id: "auto-lab-info",
3188
+ type: "stats",
1762
3189
  order: order++,
1763
- content: {
1764
- buttons: [{ text: qr.landing_page.cta_text || "Shop Online", url: ctaUrl, style: "primary" }]
1765
- }
3190
+ content: { stats: labDetails },
3191
+ config: { layout: "list" }
1766
3192
  });
1767
3193
  }
3194
+ const productSlug = product?.slug;
3195
+ if (productSlug) {
3196
+ const storeDomain = data.store?.name === "Flora Distro" ? "floradistro.com" : null;
3197
+ if (storeDomain) {
3198
+ sections.push({
3199
+ id: "auto-shop",
3200
+ type: "cta",
3201
+ order: order++,
3202
+ content: {
3203
+ buttons: [{ text: "Shop This Product", url: `https://${storeDomain}/shop/${productSlug}`, style: "outline" }]
3204
+ }
3205
+ });
3206
+ }
3207
+ }
1768
3208
  return sections;
1769
3209
  }
1770
3210
  function toNum(v) {
@@ -1872,10 +3312,11 @@ function LandingPage({
1872
3312
  if (state === "expired") return /* @__PURE__ */ jsx(DefaultExpired2, {});
1873
3313
  if (state === "error") return /* @__PURE__ */ jsx(DefaultError2, { message: errorMsg });
1874
3314
  if (!data) return null;
1875
- return /* @__PURE__ */ jsx(PageLayout, { data, renderSection });
3315
+ return /* @__PURE__ */ jsx(PageLayout, { data, gatewayUrl, renderSection });
1876
3316
  }
1877
3317
  function PageLayout({
1878
3318
  data,
3319
+ gatewayUrl,
1879
3320
  renderSection
1880
3321
  }) {
1881
3322
  const { landing_page: lp, store } = data;
@@ -1891,15 +3332,16 @@ function PageLayout({
1891
3332
  const fontFamily = lp.font_family || theme.fontDisplay || "system-ui, -apple-system, sans-serif";
1892
3333
  const logoUrl = store?.logo_url;
1893
3334
  const sorted = [...lp.sections].sort((a, b) => a.order - b.order);
3335
+ const sectionData = { ...data, gatewayUrl, landing_page: { slug: lp.slug } };
1894
3336
  return /* @__PURE__ */ jsxs("div", { style: { minHeight: "100dvh", background: theme.bg, color: theme.fg, fontFamily }, children: [
1895
3337
  lp.custom_css && /* @__PURE__ */ jsx("style", { children: lp.custom_css }),
1896
3338
  logoUrl && /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsx("img", { src: logoUrl, alt: store?.name || "Store", style: { height: 40, objectFit: "contain" } }) }),
1897
3339
  sorted.map((section) => {
1898
- const defaultRenderer = () => /* @__PURE__ */ jsx(SectionRenderer, { section, data, theme }, section.id);
3340
+ const defaultRenderer = () => /* @__PURE__ */ jsx(SectionRenderer, { section, data: sectionData, theme }, section.id);
1899
3341
  if (renderSection) {
1900
3342
  return /* @__PURE__ */ jsx("div", { children: renderSection(section, defaultRenderer) }, section.id);
1901
3343
  }
1902
- return /* @__PURE__ */ jsx(SectionRenderer, { section, data, theme }, section.id);
3344
+ return /* @__PURE__ */ jsx(SectionRenderer, { section, data: sectionData, theme }, section.id);
1903
3345
  }),
1904
3346
  store?.name && /* @__PURE__ */ jsx("div", { style: { padding: "2rem 1.5rem", borderTop: `1px solid ${theme.surface}`, textAlign: "center" }, children: /* @__PURE__ */ jsxs("p", { style: { fontSize: "0.75rem", color: theme.muted, margin: 0 }, children: [
1905
3347
  "Powered by ",
@@ -1943,6 +3385,6 @@ function DefaultError2({ message }) {
1943
3385
  ] }) });
1944
3386
  }
1945
3387
 
1946
- export { AnalyticsTracker, AuthInitializer, CartInitializer, LandingPage, PixelInitializer, QRLandingPage, SectionRenderer, WhaleContext, WhaleProvider, useAnalytics, useAuth, useCart, useCartItemCount, useCartTotal, useCategories, useCheckout, useCoupons, useCustomerAnalytics, useCustomerOrders, useLocations, useLoyalty, useProduct, useProducts, useRecommendations, useReviews, useSearch, useShipping, useWhaleClient, useWishlist };
3388
+ export { AnalyticsTracker, AuthInitializer, BehavioralTrackerComponent, CartInitializer, FingerprintCollector, LandingPage, PixelInitializer, QRLandingPage, SectionRenderer, SessionRecorderComponent, WhaleContext, WhaleProvider, useAnalytics, useAuth, useCart, useCartItemCount, useCartTotal, useCategories, useCheckout, useCoupons, useCustomerAnalytics, useCustomerOrders, useDeals, useLocations, useLoyalty, useProduct, useProducts, useRecommendations, useReferral, useReviews, useSearch, useShipping, useWhaleClient, useWishlist };
1947
3389
  //# sourceMappingURL=index.js.map
1948
3390
  //# sourceMappingURL=index.js.map