@clianta/sdk 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Clianta SDK v1.1.1
2
+ * Clianta SDK v1.3.0
3
3
  * (c) 2026 Clianta
4
4
  * Released under the MIT License.
5
5
  */
@@ -11,7 +11,7 @@ import { createContext, useRef, useEffect, useContext } from 'react';
11
11
  * @see SDK_VERSION in core/config.ts
12
12
  */
13
13
  /** SDK Version */
14
- const SDK_VERSION = '1.1.0';
14
+ const SDK_VERSION = '1.3.0';
15
15
  /** Default API endpoint based on environment */
16
16
  const getDefaultApiEndpoint = () => {
17
17
  if (typeof window === 'undefined')
@@ -31,7 +31,6 @@ const DEFAULT_PLUGINS = [
31
31
  'engagement',
32
32
  'downloads',
33
33
  'exitIntent',
34
- 'popupForms',
35
34
  ];
36
35
  /** Default configuration values */
37
36
  const DEFAULT_CONFIG = {
@@ -575,14 +574,24 @@ function getDeviceInfo() {
575
574
  * @see SDK_VERSION in core/config.ts
576
575
  */
577
576
  const MAX_QUEUE_SIZE = 1000;
577
+ /** Rate limit: max events per window */
578
+ const RATE_LIMIT_MAX_EVENTS = 100;
579
+ /** Rate limit window in ms (1 minute) */
580
+ const RATE_LIMIT_WINDOW_MS = 60000;
578
581
  /**
579
- * Event queue with batching, persistence, and auto-flush
582
+ * Event queue with batching, persistence, rate limiting, and auto-flush
580
583
  */
581
584
  class EventQueue {
582
585
  constructor(transport, config = {}) {
583
586
  this.queue = [];
584
587
  this.flushTimer = null;
585
588
  this.isFlushing = false;
589
+ /** Rate limiting: timestamps of recent events */
590
+ this.eventTimestamps = [];
591
+ /** Unload handler references for cleanup */
592
+ this.boundBeforeUnload = null;
593
+ this.boundVisibilityChange = null;
594
+ this.boundPageHide = null;
586
595
  this.transport = transport;
587
596
  this.config = {
588
597
  batchSize: config.batchSize ?? 10,
@@ -601,6 +610,11 @@ class EventQueue {
601
610
  * Add an event to the queue
602
611
  */
603
612
  push(event) {
613
+ // Rate limiting check
614
+ if (!this.checkRateLimit()) {
615
+ logger.warn('Rate limit exceeded, event dropped:', event.eventName);
616
+ return;
617
+ }
604
618
  // Don't exceed max queue size
605
619
  if (this.queue.length >= this.config.maxQueueSize) {
606
620
  logger.warn('Queue full, dropping oldest event');
@@ -613,6 +627,22 @@ class EventQueue {
613
627
  this.flush();
614
628
  }
615
629
  }
630
+ /**
631
+ * Check and enforce rate limiting
632
+ * @returns true if event is allowed, false if rate limited
633
+ */
634
+ checkRateLimit() {
635
+ const now = Date.now();
636
+ // Remove timestamps outside the window
637
+ this.eventTimestamps = this.eventTimestamps.filter(ts => now - ts < RATE_LIMIT_WINDOW_MS);
638
+ // Check if under limit
639
+ if (this.eventTimestamps.length >= RATE_LIMIT_MAX_EVENTS) {
640
+ return false;
641
+ }
642
+ // Record this event
643
+ this.eventTimestamps.push(now);
644
+ return true;
645
+ }
616
646
  /**
617
647
  * Flush the queue (send all events)
618
648
  */
@@ -621,9 +651,10 @@ class EventQueue {
621
651
  return;
622
652
  }
623
653
  this.isFlushing = true;
654
+ // Atomically take snapshot of current queue length to avoid race condition
655
+ const count = this.queue.length;
656
+ const events = this.queue.splice(0, count);
624
657
  try {
625
- // Take all events from queue
626
- const events = this.queue.splice(0, this.queue.length);
627
658
  logger.debug(`Flushing ${events.length} events`);
628
659
  // Clear persisted queue
629
660
  this.persistQueue([]);
@@ -675,13 +706,25 @@ class EventQueue {
675
706
  this.persistQueue([]);
676
707
  }
677
708
  /**
678
- * Stop the flush timer
709
+ * Stop the flush timer and cleanup handlers
679
710
  */
680
711
  destroy() {
681
712
  if (this.flushTimer) {
682
713
  clearInterval(this.flushTimer);
683
714
  this.flushTimer = null;
684
715
  }
716
+ // Remove unload handlers
717
+ if (typeof window !== 'undefined') {
718
+ if (this.boundBeforeUnload) {
719
+ window.removeEventListener('beforeunload', this.boundBeforeUnload);
720
+ }
721
+ if (this.boundVisibilityChange) {
722
+ window.removeEventListener('visibilitychange', this.boundVisibilityChange);
723
+ }
724
+ if (this.boundPageHide) {
725
+ window.removeEventListener('pagehide', this.boundPageHide);
726
+ }
727
+ }
685
728
  }
686
729
  /**
687
730
  * Start auto-flush timer
@@ -701,19 +744,18 @@ class EventQueue {
701
744
  if (typeof window === 'undefined')
702
745
  return;
703
746
  // Flush on page unload
704
- window.addEventListener('beforeunload', () => {
705
- this.flushSync();
706
- });
747
+ this.boundBeforeUnload = () => this.flushSync();
748
+ window.addEventListener('beforeunload', this.boundBeforeUnload);
707
749
  // Flush when page becomes hidden
708
- window.addEventListener('visibilitychange', () => {
750
+ this.boundVisibilityChange = () => {
709
751
  if (document.visibilityState === 'hidden') {
710
752
  this.flushSync();
711
753
  }
712
- });
754
+ };
755
+ window.addEventListener('visibilitychange', this.boundVisibilityChange);
713
756
  // Flush on page hide (iOS Safari)
714
- window.addEventListener('pagehide', () => {
715
- this.flushSync();
716
- });
757
+ this.boundPageHide = () => this.flushSync();
758
+ window.addEventListener('pagehide', this.boundPageHide);
717
759
  }
718
760
  /**
719
761
  * Persist queue to localStorage
@@ -781,6 +823,9 @@ class PageViewPlugin extends BasePlugin {
781
823
  constructor() {
782
824
  super(...arguments);
783
825
  this.name = 'pageView';
826
+ this.originalPushState = null;
827
+ this.originalReplaceState = null;
828
+ this.popstateHandler = null;
784
829
  }
785
830
  init(tracker) {
786
831
  super.init(tracker);
@@ -788,22 +833,40 @@ class PageViewPlugin extends BasePlugin {
788
833
  this.trackPageView();
789
834
  // Track SPA navigation (History API)
790
835
  if (typeof window !== 'undefined') {
836
+ // Store originals for cleanup
837
+ this.originalPushState = history.pushState;
838
+ this.originalReplaceState = history.replaceState;
791
839
  // Intercept pushState and replaceState
792
- const originalPushState = history.pushState;
793
- const originalReplaceState = history.replaceState;
794
- history.pushState = (...args) => {
795
- originalPushState.apply(history, args);
796
- this.trackPageView();
840
+ const self = this;
841
+ history.pushState = function (...args) {
842
+ self.originalPushState.apply(history, args);
843
+ self.trackPageView();
797
844
  };
798
- history.replaceState = (...args) => {
799
- originalReplaceState.apply(history, args);
800
- this.trackPageView();
845
+ history.replaceState = function (...args) {
846
+ self.originalReplaceState.apply(history, args);
847
+ self.trackPageView();
801
848
  };
802
849
  // Handle back/forward navigation
803
- window.addEventListener('popstate', () => {
804
- this.trackPageView();
805
- });
850
+ this.popstateHandler = () => this.trackPageView();
851
+ window.addEventListener('popstate', this.popstateHandler);
852
+ }
853
+ }
854
+ destroy() {
855
+ // Restore original history methods
856
+ if (this.originalPushState) {
857
+ history.pushState = this.originalPushState;
858
+ this.originalPushState = null;
859
+ }
860
+ if (this.originalReplaceState) {
861
+ history.replaceState = this.originalReplaceState;
862
+ this.originalReplaceState = null;
806
863
  }
864
+ // Remove popstate listener
865
+ if (this.popstateHandler && typeof window !== 'undefined') {
866
+ window.removeEventListener('popstate', this.popstateHandler);
867
+ this.popstateHandler = null;
868
+ }
869
+ super.destroy();
807
870
  }
808
871
  trackPageView() {
809
872
  if (typeof window === 'undefined' || typeof document === 'undefined')
@@ -835,6 +898,10 @@ class ScrollPlugin extends BasePlugin {
835
898
  this.pageLoadTime = 0;
836
899
  this.scrollTimeout = null;
837
900
  this.boundHandler = null;
901
+ /** SPA navigation support */
902
+ this.originalPushState = null;
903
+ this.originalReplaceState = null;
904
+ this.popstateHandler = null;
838
905
  }
839
906
  init(tracker) {
840
907
  super.init(tracker);
@@ -842,6 +909,8 @@ class ScrollPlugin extends BasePlugin {
842
909
  if (typeof window !== 'undefined') {
843
910
  this.boundHandler = this.handleScroll.bind(this);
844
911
  window.addEventListener('scroll', this.boundHandler, { passive: true });
912
+ // Setup SPA navigation reset
913
+ this.setupNavigationReset();
845
914
  }
846
915
  }
847
916
  destroy() {
@@ -851,8 +920,53 @@ class ScrollPlugin extends BasePlugin {
851
920
  if (this.scrollTimeout) {
852
921
  clearTimeout(this.scrollTimeout);
853
922
  }
923
+ // Restore original history methods
924
+ if (this.originalPushState) {
925
+ history.pushState = this.originalPushState;
926
+ this.originalPushState = null;
927
+ }
928
+ if (this.originalReplaceState) {
929
+ history.replaceState = this.originalReplaceState;
930
+ this.originalReplaceState = null;
931
+ }
932
+ // Remove popstate listener
933
+ if (this.popstateHandler && typeof window !== 'undefined') {
934
+ window.removeEventListener('popstate', this.popstateHandler);
935
+ this.popstateHandler = null;
936
+ }
854
937
  super.destroy();
855
938
  }
939
+ /**
940
+ * Reset scroll tracking for SPA navigation
941
+ */
942
+ resetForNavigation() {
943
+ this.milestonesReached.clear();
944
+ this.maxScrollDepth = 0;
945
+ this.pageLoadTime = Date.now();
946
+ }
947
+ /**
948
+ * Setup History API interception for SPA navigation
949
+ */
950
+ setupNavigationReset() {
951
+ if (typeof window === 'undefined')
952
+ return;
953
+ // Store originals for cleanup
954
+ this.originalPushState = history.pushState;
955
+ this.originalReplaceState = history.replaceState;
956
+ // Intercept pushState and replaceState
957
+ const self = this;
958
+ history.pushState = function (...args) {
959
+ self.originalPushState.apply(history, args);
960
+ self.resetForNavigation();
961
+ };
962
+ history.replaceState = function (...args) {
963
+ self.originalReplaceState.apply(history, args);
964
+ self.resetForNavigation();
965
+ };
966
+ // Handle back/forward navigation
967
+ this.popstateHandler = () => this.resetForNavigation();
968
+ window.addEventListener('popstate', this.popstateHandler);
969
+ }
856
970
  handleScroll() {
857
971
  // Debounce scroll tracking
858
972
  if (this.scrollTimeout) {
@@ -866,7 +980,11 @@ class ScrollPlugin extends BasePlugin {
866
980
  const windowHeight = window.innerHeight;
867
981
  const documentHeight = document.documentElement.scrollHeight;
868
982
  const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
869
- const scrollPercent = Math.floor((scrollTop / (documentHeight - windowHeight)) * 100);
983
+ const scrollableHeight = documentHeight - windowHeight;
984
+ // Guard against divide-by-zero on short pages
985
+ if (scrollableHeight <= 0)
986
+ return;
987
+ const scrollPercent = Math.floor((scrollTop / scrollableHeight) * 100);
870
988
  // Clamp to valid range
871
989
  const clampedPercent = Math.max(0, Math.min(100, scrollPercent));
872
990
  // Update max scroll depth
@@ -901,6 +1019,7 @@ class FormsPlugin extends BasePlugin {
901
1019
  this.trackedForms = new WeakSet();
902
1020
  this.formInteractions = new Set();
903
1021
  this.observer = null;
1022
+ this.listeners = [];
904
1023
  }
905
1024
  init(tracker) {
906
1025
  super.init(tracker);
@@ -919,8 +1038,20 @@ class FormsPlugin extends BasePlugin {
919
1038
  this.observer.disconnect();
920
1039
  this.observer = null;
921
1040
  }
1041
+ // Remove all tracked event listeners
1042
+ for (const { element, event, handler } of this.listeners) {
1043
+ element.removeEventListener(event, handler);
1044
+ }
1045
+ this.listeners = [];
922
1046
  super.destroy();
923
1047
  }
1048
+ /**
1049
+ * Track event listener for cleanup
1050
+ */
1051
+ addListener(element, event, handler) {
1052
+ element.addEventListener(event, handler);
1053
+ this.listeners.push({ element, event, handler });
1054
+ }
924
1055
  trackAllForms() {
925
1056
  document.querySelectorAll('form').forEach((form) => {
926
1057
  this.setupFormTracking(form);
@@ -946,7 +1077,7 @@ class FormsPlugin extends BasePlugin {
946
1077
  if (!field.name || field.type === 'submit' || field.type === 'button')
947
1078
  return;
948
1079
  ['focus', 'blur', 'change'].forEach((eventType) => {
949
- field.addEventListener(eventType, () => {
1080
+ const handler = () => {
950
1081
  const key = `${formId}-${field.name}-${eventType}`;
951
1082
  if (!this.formInteractions.has(key)) {
952
1083
  this.formInteractions.add(key);
@@ -957,12 +1088,13 @@ class FormsPlugin extends BasePlugin {
957
1088
  interactionType: eventType,
958
1089
  });
959
1090
  }
960
- });
1091
+ };
1092
+ this.addListener(field, eventType, handler);
961
1093
  });
962
1094
  }
963
1095
  });
964
1096
  // Track form submission
965
- form.addEventListener('submit', () => {
1097
+ const submitHandler = () => {
966
1098
  this.track('form_submit', 'Form Submitted', {
967
1099
  formId,
968
1100
  action: form.action,
@@ -970,7 +1102,8 @@ class FormsPlugin extends BasePlugin {
970
1102
  });
971
1103
  // Auto-identify if email field found
972
1104
  this.autoIdentify(form);
973
- });
1105
+ };
1106
+ this.addListener(form, 'submit', submitHandler);
974
1107
  }
975
1108
  autoIdentify(form) {
976
1109
  const emailField = form.querySelector('input[type="email"], input[name*="email"]');
@@ -1054,6 +1187,7 @@ class EngagementPlugin extends BasePlugin {
1054
1187
  this.engagementTimeout = null;
1055
1188
  this.boundMarkEngaged = null;
1056
1189
  this.boundTrackTimeOnPage = null;
1190
+ this.boundVisibilityHandler = null;
1057
1191
  }
1058
1192
  init(tracker) {
1059
1193
  super.init(tracker);
@@ -1064,12 +1198,7 @@ class EngagementPlugin extends BasePlugin {
1064
1198
  // Setup engagement detection
1065
1199
  this.boundMarkEngaged = this.markEngaged.bind(this);
1066
1200
  this.boundTrackTimeOnPage = this.trackTimeOnPage.bind(this);
1067
- ['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
1068
- document.addEventListener(event, this.boundMarkEngaged, { passive: true });
1069
- });
1070
- // Track time on page before unload
1071
- window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
1072
- window.addEventListener('visibilitychange', () => {
1201
+ this.boundVisibilityHandler = () => {
1073
1202
  if (document.visibilityState === 'hidden') {
1074
1203
  this.trackTimeOnPage();
1075
1204
  }
@@ -1077,7 +1206,13 @@ class EngagementPlugin extends BasePlugin {
1077
1206
  // Reset engagement timer when page becomes visible again
1078
1207
  this.engagementStartTime = Date.now();
1079
1208
  }
1209
+ };
1210
+ ['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
1211
+ document.addEventListener(event, this.boundMarkEngaged, { passive: true });
1080
1212
  });
1213
+ // Track time on page before unload
1214
+ window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
1215
+ document.addEventListener('visibilitychange', this.boundVisibilityHandler);
1081
1216
  }
1082
1217
  destroy() {
1083
1218
  if (this.boundMarkEngaged && typeof document !== 'undefined') {
@@ -1088,6 +1223,9 @@ class EngagementPlugin extends BasePlugin {
1088
1223
  if (this.boundTrackTimeOnPage && typeof window !== 'undefined') {
1089
1224
  window.removeEventListener('beforeunload', this.boundTrackTimeOnPage);
1090
1225
  }
1226
+ if (this.boundVisibilityHandler && typeof document !== 'undefined') {
1227
+ document.removeEventListener('visibilitychange', this.boundVisibilityHandler);
1228
+ }
1091
1229
  if (this.engagementTimeout) {
1092
1230
  clearTimeout(this.engagementTimeout);
1093
1231
  }
@@ -1132,20 +1270,69 @@ class DownloadsPlugin extends BasePlugin {
1132
1270
  this.name = 'downloads';
1133
1271
  this.trackedDownloads = new Set();
1134
1272
  this.boundHandler = null;
1273
+ /** SPA navigation support */
1274
+ this.originalPushState = null;
1275
+ this.originalReplaceState = null;
1276
+ this.popstateHandler = null;
1135
1277
  }
1136
1278
  init(tracker) {
1137
1279
  super.init(tracker);
1138
1280
  if (typeof document !== 'undefined') {
1139
1281
  this.boundHandler = this.handleClick.bind(this);
1140
1282
  document.addEventListener('click', this.boundHandler, true);
1283
+ // Setup SPA navigation reset
1284
+ this.setupNavigationReset();
1141
1285
  }
1142
1286
  }
1143
1287
  destroy() {
1144
1288
  if (this.boundHandler && typeof document !== 'undefined') {
1145
1289
  document.removeEventListener('click', this.boundHandler, true);
1146
1290
  }
1291
+ // Restore original history methods
1292
+ if (this.originalPushState) {
1293
+ history.pushState = this.originalPushState;
1294
+ this.originalPushState = null;
1295
+ }
1296
+ if (this.originalReplaceState) {
1297
+ history.replaceState = this.originalReplaceState;
1298
+ this.originalReplaceState = null;
1299
+ }
1300
+ // Remove popstate listener
1301
+ if (this.popstateHandler && typeof window !== 'undefined') {
1302
+ window.removeEventListener('popstate', this.popstateHandler);
1303
+ this.popstateHandler = null;
1304
+ }
1147
1305
  super.destroy();
1148
1306
  }
1307
+ /**
1308
+ * Reset download tracking for SPA navigation
1309
+ */
1310
+ resetForNavigation() {
1311
+ this.trackedDownloads.clear();
1312
+ }
1313
+ /**
1314
+ * Setup History API interception for SPA navigation
1315
+ */
1316
+ setupNavigationReset() {
1317
+ if (typeof window === 'undefined')
1318
+ return;
1319
+ // Store originals for cleanup
1320
+ this.originalPushState = history.pushState;
1321
+ this.originalReplaceState = history.replaceState;
1322
+ // Intercept pushState and replaceState
1323
+ const self = this;
1324
+ history.pushState = function (...args) {
1325
+ self.originalPushState.apply(history, args);
1326
+ self.resetForNavigation();
1327
+ };
1328
+ history.replaceState = function (...args) {
1329
+ self.originalReplaceState.apply(history, args);
1330
+ self.resetForNavigation();
1331
+ };
1332
+ // Handle back/forward navigation
1333
+ this.popstateHandler = () => this.resetForNavigation();
1334
+ window.addEventListener('popstate', this.popstateHandler);
1335
+ }
1149
1336
  handleClick(e) {
1150
1337
  const link = e.target.closest('a');
1151
1338
  if (!link || !link.href)
@@ -1271,34 +1458,72 @@ class PerformancePlugin extends BasePlugin {
1271
1458
  constructor() {
1272
1459
  super(...arguments);
1273
1460
  this.name = 'performance';
1461
+ this.boundLoadHandler = null;
1462
+ this.observers = [];
1463
+ this.boundClsVisibilityHandler = null;
1274
1464
  }
1275
1465
  init(tracker) {
1276
1466
  super.init(tracker);
1277
1467
  if (typeof window !== 'undefined') {
1278
1468
  // Track performance after page load
1279
- window.addEventListener('load', () => {
1469
+ this.boundLoadHandler = () => {
1280
1470
  // Delay to ensure all metrics are available
1281
1471
  setTimeout(() => this.trackPerformance(), 100);
1282
- });
1472
+ };
1473
+ window.addEventListener('load', this.boundLoadHandler);
1474
+ }
1475
+ }
1476
+ destroy() {
1477
+ if (this.boundLoadHandler && typeof window !== 'undefined') {
1478
+ window.removeEventListener('load', this.boundLoadHandler);
1283
1479
  }
1480
+ for (const observer of this.observers) {
1481
+ observer.disconnect();
1482
+ }
1483
+ this.observers = [];
1484
+ if (this.boundClsVisibilityHandler && typeof window !== 'undefined') {
1485
+ window.removeEventListener('visibilitychange', this.boundClsVisibilityHandler);
1486
+ }
1487
+ super.destroy();
1284
1488
  }
1285
1489
  trackPerformance() {
1286
1490
  if (typeof performance === 'undefined')
1287
1491
  return;
1288
- // Use Navigation Timing API
1289
- const timing = performance.timing;
1290
- if (!timing)
1291
- return;
1292
- const loadTime = timing.loadEventEnd - timing.navigationStart;
1293
- const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;
1294
- const ttfb = timing.responseStart - timing.navigationStart;
1295
- const domInteractive = timing.domInteractive - timing.navigationStart;
1296
- this.track('performance', 'Page Performance', {
1297
- loadTime,
1298
- domReady,
1299
- ttfb, // Time to First Byte
1300
- domInteractive,
1301
- });
1492
+ // Use modern Navigation Timing API (PerformanceNavigationTiming)
1493
+ const entries = performance.getEntriesByType('navigation');
1494
+ if (entries.length > 0) {
1495
+ const navTiming = entries[0];
1496
+ const loadTime = Math.round(navTiming.loadEventEnd - navTiming.startTime);
1497
+ const domReady = Math.round(navTiming.domContentLoadedEventEnd - navTiming.startTime);
1498
+ const ttfb = Math.round(navTiming.responseStart - navTiming.requestStart);
1499
+ const domInteractive = Math.round(navTiming.domInteractive - navTiming.startTime);
1500
+ this.track('performance', 'Page Performance', {
1501
+ loadTime,
1502
+ domReady,
1503
+ ttfb, // Time to First Byte
1504
+ domInteractive,
1505
+ // Additional modern metrics
1506
+ dns: Math.round(navTiming.domainLookupEnd - navTiming.domainLookupStart),
1507
+ connection: Math.round(navTiming.connectEnd - navTiming.connectStart),
1508
+ transferSize: navTiming.transferSize,
1509
+ });
1510
+ }
1511
+ else {
1512
+ // Fallback for older browsers using deprecated API
1513
+ const timing = performance.timing;
1514
+ if (!timing)
1515
+ return;
1516
+ const loadTime = timing.loadEventEnd - timing.navigationStart;
1517
+ const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;
1518
+ const ttfb = timing.responseStart - timing.navigationStart;
1519
+ const domInteractive = timing.domInteractive - timing.navigationStart;
1520
+ this.track('performance', 'Page Performance', {
1521
+ loadTime,
1522
+ domReady,
1523
+ ttfb,
1524
+ domInteractive,
1525
+ });
1526
+ }
1302
1527
  // Track Web Vitals if available
1303
1528
  this.trackWebVitals();
1304
1529
  }
@@ -1317,6 +1542,7 @@ class PerformancePlugin extends BasePlugin {
1317
1542
  }
1318
1543
  });
1319
1544
  lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
1545
+ this.observers.push(lcpObserver);
1320
1546
  }
1321
1547
  catch {
1322
1548
  // LCP not supported
@@ -1334,6 +1560,7 @@ class PerformancePlugin extends BasePlugin {
1334
1560
  }
1335
1561
  });
1336
1562
  fidObserver.observe({ type: 'first-input', buffered: true });
1563
+ this.observers.push(fidObserver);
1337
1564
  }
1338
1565
  catch {
1339
1566
  // FID not supported
@@ -1350,15 +1577,17 @@ class PerformancePlugin extends BasePlugin {
1350
1577
  });
1351
1578
  });
1352
1579
  clsObserver.observe({ type: 'layout-shift', buffered: true });
1580
+ this.observers.push(clsObserver);
1353
1581
  // Report CLS after page is hidden
1354
- window.addEventListener('visibilitychange', () => {
1582
+ this.boundClsVisibilityHandler = () => {
1355
1583
  if (document.visibilityState === 'hidden' && clsValue > 0) {
1356
1584
  this.track('performance', 'Web Vital - CLS', {
1357
1585
  metric: 'CLS',
1358
1586
  value: Math.round(clsValue * 1000) / 1000,
1359
1587
  });
1360
1588
  }
1361
- }, { once: true });
1589
+ };
1590
+ window.addEventListener('visibilitychange', this.boundClsVisibilityHandler, { once: true });
1362
1591
  }
1363
1592
  catch {
1364
1593
  // CLS not supported
@@ -1575,8 +1804,8 @@ class PopupFormsPlugin extends BasePlugin {
1575
1804
  opacity: 0;
1576
1805
  transition: all 0.3s ease;
1577
1806
  `;
1578
- // Build form HTML
1579
- container.innerHTML = this.buildFormHTML(form);
1807
+ // Build form using safe DOM APIs (no innerHTML for user content)
1808
+ this.buildFormDOM(form, container);
1580
1809
  overlay.appendChild(container);
1581
1810
  document.body.appendChild(overlay);
1582
1811
  // Animate in
@@ -1588,95 +1817,162 @@ class PopupFormsPlugin extends BasePlugin {
1588
1817
  // Setup event listeners
1589
1818
  this.setupFormEvents(form, overlay, container);
1590
1819
  }
1591
- buildFormHTML(form) {
1820
+ /**
1821
+ * Escape HTML to prevent XSS - used only for static structure
1822
+ */
1823
+ escapeHTML(str) {
1824
+ const div = document.createElement('div');
1825
+ div.textContent = str;
1826
+ return div.innerHTML;
1827
+ }
1828
+ /**
1829
+ * Build form using safe DOM APIs (prevents XSS)
1830
+ */
1831
+ buildFormDOM(form, container) {
1592
1832
  const style = form.style || {};
1593
1833
  const primaryColor = style.primaryColor || '#10B981';
1594
1834
  const textColor = style.textColor || '#18181B';
1595
- let fieldsHTML = form.fields.map(field => {
1596
- const requiredMark = field.required ? '<span style="color: #EF4444;">*</span>' : '';
1597
- if (field.type === 'textarea') {
1598
- return `
1599
- <div style="margin-bottom: 12px;">
1600
- <label style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${textColor};">
1601
- ${field.label} ${requiredMark}
1602
- </label>
1603
- <textarea
1604
- name="${field.name}"
1605
- placeholder="${field.placeholder || ''}"
1606
- ${field.required ? 'required' : ''}
1607
- style="width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; resize: vertical; min-height: 80px;"
1608
- ></textarea>
1609
- </div>
1610
- `;
1611
- }
1612
- else if (field.type === 'checkbox') {
1613
- return `
1614
- <div style="margin-bottom: 12px;">
1615
- <label style="display: flex; align-items: center; gap: 8px; font-size: 14px; color: ${textColor}; cursor: pointer;">
1616
- <input
1617
- type="checkbox"
1618
- name="${field.name}"
1619
- ${field.required ? 'required' : ''}
1620
- style="width: 16px; height: 16px;"
1621
- />
1622
- ${field.label} ${requiredMark}
1623
- </label>
1624
- </div>
1625
- `;
1835
+ // Close button
1836
+ const closeBtn = document.createElement('button');
1837
+ closeBtn.id = 'clianta-form-close';
1838
+ closeBtn.style.cssText = `
1839
+ position: absolute;
1840
+ top: 12px;
1841
+ right: 12px;
1842
+ background: none;
1843
+ border: none;
1844
+ font-size: 20px;
1845
+ cursor: pointer;
1846
+ color: #71717A;
1847
+ padding: 4px;
1848
+ `;
1849
+ closeBtn.textContent = '×';
1850
+ container.appendChild(closeBtn);
1851
+ // Headline
1852
+ const headline = document.createElement('h2');
1853
+ headline.style.cssText = `font-size: 20px; font-weight: 700; margin-bottom: 8px; color: ${this.escapeHTML(textColor)};`;
1854
+ headline.textContent = form.headline || 'Stay in touch';
1855
+ container.appendChild(headline);
1856
+ // Subheadline
1857
+ const subheadline = document.createElement('p');
1858
+ subheadline.style.cssText = 'font-size: 14px; color: #71717A; margin-bottom: 16px;';
1859
+ subheadline.textContent = form.subheadline || 'Get the latest updates';
1860
+ container.appendChild(subheadline);
1861
+ // Form element
1862
+ const formElement = document.createElement('form');
1863
+ formElement.id = 'clianta-form-element';
1864
+ // Build fields
1865
+ form.fields.forEach(field => {
1866
+ const fieldWrapper = document.createElement('div');
1867
+ fieldWrapper.style.marginBottom = '12px';
1868
+ if (field.type === 'checkbox') {
1869
+ // Checkbox layout
1870
+ const label = document.createElement('label');
1871
+ label.style.cssText = `display: flex; align-items: center; gap: 8px; font-size: 14px; color: ${this.escapeHTML(textColor)}; cursor: pointer;`;
1872
+ const input = document.createElement('input');
1873
+ input.type = 'checkbox';
1874
+ input.name = field.name;
1875
+ if (field.required)
1876
+ input.required = true;
1877
+ input.style.cssText = 'width: 16px; height: 16px;';
1878
+ label.appendChild(input);
1879
+ const labelText = document.createTextNode(field.label + ' ');
1880
+ label.appendChild(labelText);
1881
+ if (field.required) {
1882
+ const requiredMark = document.createElement('span');
1883
+ requiredMark.style.color = '#EF4444';
1884
+ requiredMark.textContent = '*';
1885
+ label.appendChild(requiredMark);
1886
+ }
1887
+ fieldWrapper.appendChild(label);
1626
1888
  }
1627
1889
  else {
1628
- return `
1629
- <div style="margin-bottom: 12px;">
1630
- <label style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${textColor};">
1631
- ${field.label} ${requiredMark}
1632
- </label>
1633
- <input
1634
- type="${field.type}"
1635
- name="${field.name}"
1636
- placeholder="${field.placeholder || ''}"
1637
- ${field.required ? 'required' : ''}
1638
- style="width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box;"
1639
- />
1640
- </div>
1641
- `;
1890
+ // Label
1891
+ const label = document.createElement('label');
1892
+ label.style.cssText = `display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${this.escapeHTML(textColor)};`;
1893
+ label.textContent = field.label + ' ';
1894
+ if (field.required) {
1895
+ const requiredMark = document.createElement('span');
1896
+ requiredMark.style.color = '#EF4444';
1897
+ requiredMark.textContent = '*';
1898
+ label.appendChild(requiredMark);
1899
+ }
1900
+ fieldWrapper.appendChild(label);
1901
+ // Input/Textarea/Select
1902
+ if (field.type === 'textarea') {
1903
+ const textarea = document.createElement('textarea');
1904
+ textarea.name = field.name;
1905
+ if (field.placeholder)
1906
+ textarea.placeholder = field.placeholder;
1907
+ if (field.required)
1908
+ textarea.required = true;
1909
+ textarea.style.cssText = 'width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; resize: vertical; min-height: 80px; box-sizing: border-box;';
1910
+ fieldWrapper.appendChild(textarea);
1911
+ }
1912
+ else if (field.type === 'select') {
1913
+ const select = document.createElement('select');
1914
+ select.name = field.name;
1915
+ if (field.required)
1916
+ select.required = true;
1917
+ select.style.cssText = 'width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box; background: white; cursor: pointer;';
1918
+ // Add placeholder option
1919
+ if (field.placeholder) {
1920
+ const placeholderOption = document.createElement('option');
1921
+ placeholderOption.value = '';
1922
+ placeholderOption.textContent = field.placeholder;
1923
+ placeholderOption.disabled = true;
1924
+ placeholderOption.selected = true;
1925
+ select.appendChild(placeholderOption);
1926
+ }
1927
+ // Add options from field.options array if provided
1928
+ if (field.options && Array.isArray(field.options)) {
1929
+ field.options.forEach((opt) => {
1930
+ const option = document.createElement('option');
1931
+ if (typeof opt === 'string') {
1932
+ option.value = opt;
1933
+ option.textContent = opt;
1934
+ }
1935
+ else {
1936
+ option.value = opt.value;
1937
+ option.textContent = opt.label;
1938
+ }
1939
+ select.appendChild(option);
1940
+ });
1941
+ }
1942
+ fieldWrapper.appendChild(select);
1943
+ }
1944
+ else {
1945
+ const input = document.createElement('input');
1946
+ input.type = field.type;
1947
+ input.name = field.name;
1948
+ if (field.placeholder)
1949
+ input.placeholder = field.placeholder;
1950
+ if (field.required)
1951
+ input.required = true;
1952
+ input.style.cssText = 'width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box;';
1953
+ fieldWrapper.appendChild(input);
1954
+ }
1642
1955
  }
1643
- }).join('');
1644
- return `
1645
- <button id="clianta-form-close" style="
1646
- position: absolute;
1647
- top: 12px;
1648
- right: 12px;
1649
- background: none;
1650
- border: none;
1651
- font-size: 20px;
1652
- cursor: pointer;
1653
- color: #71717A;
1654
- padding: 4px;
1655
- ">&times;</button>
1656
- <h2 style="font-size: 20px; font-weight: 700; margin-bottom: 8px; color: ${textColor};">
1657
- ${form.headline || 'Stay in touch'}
1658
- </h2>
1659
- <p style="font-size: 14px; color: #71717A; margin-bottom: 16px;">
1660
- ${form.subheadline || 'Get the latest updates'}
1661
- </p>
1662
- <form id="clianta-form-element">
1663
- ${fieldsHTML}
1664
- <button type="submit" style="
1665
- width: 100%;
1666
- padding: 10px 16px;
1667
- background: ${primaryColor};
1668
- color: white;
1669
- border: none;
1670
- border-radius: 6px;
1671
- font-size: 14px;
1672
- font-weight: 500;
1673
- cursor: pointer;
1674
- margin-top: 8px;
1675
- ">
1676
- ${form.submitButtonText || 'Subscribe'}
1677
- </button>
1678
- </form>
1956
+ formElement.appendChild(fieldWrapper);
1957
+ });
1958
+ // Submit button
1959
+ const submitBtn = document.createElement('button');
1960
+ submitBtn.type = 'submit';
1961
+ submitBtn.style.cssText = `
1962
+ width: 100%;
1963
+ padding: 10px 16px;
1964
+ background: ${this.escapeHTML(primaryColor)};
1965
+ color: white;
1966
+ border: none;
1967
+ border-radius: 6px;
1968
+ font-size: 14px;
1969
+ font-weight: 500;
1970
+ cursor: pointer;
1971
+ margin-top: 8px;
1679
1972
  `;
1973
+ submitBtn.textContent = form.submitButtonText || 'Subscribe';
1974
+ formElement.appendChild(submitBtn);
1975
+ container.appendChild(formElement);
1680
1976
  }
1681
1977
  setupFormEvents(form, overlay, container) {
1682
1978
  // Close button
@@ -1737,19 +2033,29 @@ class PopupFormsPlugin extends BasePlugin {
1737
2033
  });
1738
2034
  const result = await response.json();
1739
2035
  if (result.success) {
1740
- // Show success message
1741
- container.innerHTML = `
1742
- <div style="text-align: center; padding: 20px;">
1743
- <div style="width: 48px; height: 48px; background: #10B981; border-radius: 50%; margin: 0 auto 16px; display: flex; align-items: center; justify-content: center;">
1744
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
1745
- <polyline points="20 6 9 17 4 12"></polyline>
1746
- </svg>
1747
- </div>
1748
- <p style="font-size: 16px; font-weight: 500; color: #18181B;">
1749
- ${form.successMessage || 'Thank you!'}
1750
- </p>
1751
- </div>
1752
- `;
2036
+ // Show success message using safe DOM APIs
2037
+ container.innerHTML = '';
2038
+ const successWrapper = document.createElement('div');
2039
+ successWrapper.style.cssText = 'text-align: center; padding: 20px;';
2040
+ const iconWrapper = document.createElement('div');
2041
+ iconWrapper.style.cssText = 'width: 48px; height: 48px; background: #10B981; border-radius: 50%; margin: 0 auto 16px; display: flex; align-items: center; justify-content: center;';
2042
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2043
+ svg.setAttribute('width', '24');
2044
+ svg.setAttribute('height', '24');
2045
+ svg.setAttribute('viewBox', '0 0 24 24');
2046
+ svg.setAttribute('fill', 'none');
2047
+ svg.setAttribute('stroke', 'white');
2048
+ svg.setAttribute('stroke-width', '2');
2049
+ const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
2050
+ polyline.setAttribute('points', '20 6 9 17 4 12');
2051
+ svg.appendChild(polyline);
2052
+ iconWrapper.appendChild(svg);
2053
+ const message = document.createElement('p');
2054
+ message.style.cssText = 'font-size: 16px; font-weight: 500; color: #18181B;';
2055
+ message.textContent = form.successMessage || 'Thank you!';
2056
+ successWrapper.appendChild(iconWrapper);
2057
+ successWrapper.appendChild(message);
2058
+ container.appendChild(successWrapper);
1753
2059
  // Track identify
1754
2060
  if (data.email) {
1755
2061
  this.tracker?.identify(data.email, data);
@@ -1886,6 +2192,8 @@ function hasStoredConsent() {
1886
2192
  * Manages consent state and event buffering for GDPR/CCPA compliance
1887
2193
  * @see SDK_VERSION in core/config.ts
1888
2194
  */
2195
+ /** Maximum events to buffer while waiting for consent */
2196
+ const MAX_BUFFER_SIZE = 100;
1889
2197
  /**
1890
2198
  * Manages user consent state for tracking
1891
2199
  */
@@ -2002,6 +2310,11 @@ class ConsentManager {
2002
2310
  * Buffer an event (for waitForConsent mode)
2003
2311
  */
2004
2312
  bufferEvent(event) {
2313
+ // Prevent unbounded buffer growth
2314
+ if (this.eventBuffer.length >= MAX_BUFFER_SIZE) {
2315
+ logger.warn('Consent event buffer full, dropping oldest event');
2316
+ this.eventBuffer.shift();
2317
+ }
2005
2318
  this.eventBuffer.push(event);
2006
2319
  logger.debug('Event buffered (waiting for consent):', event.eventName);
2007
2320
  }
@@ -2061,6 +2374,8 @@ class Tracker {
2061
2374
  constructor(workspaceId, userConfig = {}) {
2062
2375
  this.plugins = [];
2063
2376
  this.isInitialized = false;
2377
+ /** Pending identify retry on next flush */
2378
+ this.pendingIdentify = null;
2064
2379
  if (!workspaceId) {
2065
2380
  throw new Error('[Clianta] Workspace ID is required');
2066
2381
  }
@@ -2146,6 +2461,7 @@ class Tracker {
2146
2461
  }
2147
2462
  /**
2148
2463
  * Initialize enabled plugins
2464
+ * Handles both sync and async plugin init methods
2149
2465
  */
2150
2466
  initPlugins() {
2151
2467
  const pluginsToLoad = this.config.plugins;
@@ -2156,7 +2472,13 @@ class Tracker {
2156
2472
  for (const pluginName of filteredPlugins) {
2157
2473
  try {
2158
2474
  const plugin = getPlugin(pluginName);
2159
- plugin.init(this);
2475
+ // Handle both sync and async init (fire-and-forget for async)
2476
+ const result = plugin.init(this);
2477
+ if (result instanceof Promise) {
2478
+ result.catch((error) => {
2479
+ logger.error(`Async plugin init failed: ${pluginName}`, error);
2480
+ });
2481
+ }
2160
2482
  this.plugins.push(plugin);
2161
2483
  logger.debug(`Plugin loaded: ${pluginName}`);
2162
2484
  }
@@ -2183,7 +2505,7 @@ class Tracker {
2183
2505
  referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
2184
2506
  properties,
2185
2507
  device: getDeviceInfo(),
2186
- utm: getUTMParams(),
2508
+ ...getUTMParams(),
2187
2509
  timestamp: new Date().toISOString(),
2188
2510
  sdkVersion: SDK_VERSION,
2189
2511
  };
@@ -2228,11 +2550,24 @@ class Tracker {
2228
2550
  });
2229
2551
  if (result.success) {
2230
2552
  logger.info('Visitor identified successfully');
2553
+ this.pendingIdentify = null;
2231
2554
  }
2232
2555
  else {
2233
2556
  logger.error('Failed to identify visitor:', result.error);
2557
+ // Store for retry on next flush
2558
+ this.pendingIdentify = { email, traits };
2234
2559
  }
2235
2560
  }
2561
+ /**
2562
+ * Retry pending identify call
2563
+ */
2564
+ async retryPendingIdentify() {
2565
+ if (!this.pendingIdentify)
2566
+ return;
2567
+ const { email, traits } = this.pendingIdentify;
2568
+ this.pendingIdentify = null;
2569
+ await this.identify(email, traits);
2570
+ }
2236
2571
  /**
2237
2572
  * Update consent state
2238
2573
  */
@@ -2280,6 +2615,7 @@ class Tracker {
2280
2615
  * Force flush event queue
2281
2616
  */
2282
2617
  async flush() {
2618
+ await this.retryPendingIdentify();
2283
2619
  await this.queue.flush();
2284
2620
  }
2285
2621
  /**
@@ -2334,10 +2670,10 @@ class Tracker {
2334
2670
  /**
2335
2671
  * Destroy tracker and cleanup
2336
2672
  */
2337
- destroy() {
2673
+ async destroy() {
2338
2674
  logger.info('Destroying tracker');
2339
- // Flush any remaining events
2340
- this.queue.flush();
2675
+ // Flush any remaining events (await to ensure completion)
2676
+ await this.queue.flush();
2341
2677
  // Destroy plugins
2342
2678
  for (const plugin of this.plugins) {
2343
2679
  if (plugin.destroy) {
@@ -2352,20 +2688,28 @@ class Tracker {
2352
2688
  }
2353
2689
 
2354
2690
  /**
2355
- * Clianta SDK - CRM API Client
2356
- * @see SDK_VERSION in core/config.ts
2691
+ * Clianta SDK - Event Triggers Manager
2692
+ * Manages event-driven automation and email notifications
2357
2693
  */
2358
2694
  /**
2359
- * CRM API Client for managing contacts and opportunities
2695
+ * Event Triggers Manager
2696
+ * Handles event-driven automation based on CRM actions
2697
+ *
2698
+ * Similar to:
2699
+ * - Salesforce: Process Builder, Flow Automation
2700
+ * - HubSpot: Workflows, Email Sequences
2701
+ * - Pipedrive: Workflow Automation
2360
2702
  */
2361
- class CRMClient {
2703
+ class EventTriggersManager {
2362
2704
  constructor(apiEndpoint, workspaceId, authToken) {
2705
+ this.triggers = new Map();
2706
+ this.listeners = new Map();
2363
2707
  this.apiEndpoint = apiEndpoint;
2364
2708
  this.workspaceId = workspaceId;
2365
2709
  this.authToken = authToken;
2366
2710
  }
2367
2711
  /**
2368
- * Set authentication token for API requests
2712
+ * Set authentication token
2369
2713
  */
2370
2714
  setAuthToken(token) {
2371
2715
  this.authToken = token;
@@ -2410,120 +2754,942 @@ class CRMClient {
2410
2754
  }
2411
2755
  }
2412
2756
  // ============================================
2413
- // CONTACTS API
2757
+ // TRIGGER MANAGEMENT
2414
2758
  // ============================================
2415
2759
  /**
2416
- * Get all contacts with pagination
2760
+ * Get all event triggers
2417
2761
  */
2418
- async getContacts(params) {
2419
- const queryParams = new URLSearchParams();
2420
- if (params?.page)
2421
- queryParams.set('page', params.page.toString());
2422
- if (params?.limit)
2423
- queryParams.set('limit', params.limit.toString());
2424
- if (params?.search)
2425
- queryParams.set('search', params.search);
2426
- if (params?.status)
2427
- queryParams.set('status', params.status);
2428
- const query = queryParams.toString();
2429
- const endpoint = `/api/workspaces/${this.workspaceId}/contacts${query ? `?${query}` : ''}`;
2430
- return this.request(endpoint);
2762
+ async getTriggers() {
2763
+ return this.request(`/api/workspaces/${this.workspaceId}/triggers`);
2431
2764
  }
2432
2765
  /**
2433
- * Get a single contact by ID
2766
+ * Get a single trigger by ID
2434
2767
  */
2435
- async getContact(contactId) {
2436
- return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
2768
+ async getTrigger(triggerId) {
2769
+ return this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`);
2437
2770
  }
2438
2771
  /**
2439
- * Create a new contact
2772
+ * Create a new event trigger
2440
2773
  */
2441
- async createContact(contact) {
2442
- return this.request(`/api/workspaces/${this.workspaceId}/contacts`, {
2774
+ async createTrigger(trigger) {
2775
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers`, {
2443
2776
  method: 'POST',
2444
- body: JSON.stringify(contact),
2777
+ body: JSON.stringify(trigger),
2445
2778
  });
2779
+ // Cache the trigger locally if successful
2780
+ if (result.success && result.data?._id) {
2781
+ this.triggers.set(result.data._id, result.data);
2782
+ }
2783
+ return result;
2446
2784
  }
2447
2785
  /**
2448
- * Update an existing contact
2786
+ * Update an existing trigger
2449
2787
  */
2450
- async updateContact(contactId, updates) {
2451
- return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2788
+ async updateTrigger(triggerId, updates) {
2789
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2452
2790
  method: 'PUT',
2453
2791
  body: JSON.stringify(updates),
2454
2792
  });
2793
+ // Update cache if successful
2794
+ if (result.success && result.data?._id) {
2795
+ this.triggers.set(result.data._id, result.data);
2796
+ }
2797
+ return result;
2455
2798
  }
2456
2799
  /**
2457
- * Delete a contact
2800
+ * Delete a trigger
2458
2801
  */
2459
- async deleteContact(contactId) {
2460
- return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2802
+ async deleteTrigger(triggerId) {
2803
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2461
2804
  method: 'DELETE',
2462
2805
  });
2806
+ // Remove from cache if successful
2807
+ if (result.success) {
2808
+ this.triggers.delete(triggerId);
2809
+ }
2810
+ return result;
2811
+ }
2812
+ /**
2813
+ * Activate a trigger
2814
+ */
2815
+ async activateTrigger(triggerId) {
2816
+ return this.updateTrigger(triggerId, { isActive: true });
2817
+ }
2818
+ /**
2819
+ * Deactivate a trigger
2820
+ */
2821
+ async deactivateTrigger(triggerId) {
2822
+ return this.updateTrigger(triggerId, { isActive: false });
2463
2823
  }
2464
2824
  // ============================================
2465
- // OPPORTUNITIES API
2825
+ // EVENT HANDLING (CLIENT-SIDE)
2466
2826
  // ============================================
2467
2827
  /**
2468
- * Get all opportunities with pagination
2828
+ * Register a local event listener for client-side triggers
2829
+ * This allows immediate client-side reactions to events
2469
2830
  */
2470
- async getOpportunities(params) {
2471
- const queryParams = new URLSearchParams();
2472
- if (params?.page)
2473
- queryParams.set('page', params.page.toString());
2474
- if (params?.limit)
2475
- queryParams.set('limit', params.limit.toString());
2476
- if (params?.pipelineId)
2477
- queryParams.set('pipelineId', params.pipelineId);
2478
- if (params?.stageId)
2479
- queryParams.set('stageId', params.stageId);
2480
- const query = queryParams.toString();
2481
- const endpoint = `/api/workspaces/${this.workspaceId}/opportunities${query ? `?${query}` : ''}`;
2482
- return this.request(endpoint);
2831
+ on(eventType, callback) {
2832
+ if (!this.listeners.has(eventType)) {
2833
+ this.listeners.set(eventType, new Set());
2834
+ }
2835
+ this.listeners.get(eventType).add(callback);
2836
+ logger.debug(`Event listener registered: ${eventType}`);
2483
2837
  }
2484
2838
  /**
2485
- * Get a single opportunity by ID
2839
+ * Remove an event listener
2486
2840
  */
2487
- async getOpportunity(opportunityId) {
2488
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`);
2841
+ off(eventType, callback) {
2842
+ const listeners = this.listeners.get(eventType);
2843
+ if (listeners) {
2844
+ listeners.delete(callback);
2845
+ }
2489
2846
  }
2490
2847
  /**
2491
- * Create a new opportunity
2848
+ * Emit an event (client-side only)
2849
+ * This will trigger any registered local listeners
2492
2850
  */
2493
- async createOpportunity(opportunity) {
2494
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities`, {
2851
+ emit(eventType, data) {
2852
+ logger.debug(`Event emitted: ${eventType}`, data);
2853
+ const listeners = this.listeners.get(eventType);
2854
+ if (listeners) {
2855
+ listeners.forEach(callback => {
2856
+ try {
2857
+ callback(data);
2858
+ }
2859
+ catch (error) {
2860
+ logger.error(`Error in event listener for ${eventType}:`, error);
2861
+ }
2862
+ });
2863
+ }
2864
+ }
2865
+ /**
2866
+ * Check if conditions are met for a trigger
2867
+ * Supports dynamic field evaluation including custom fields and nested paths
2868
+ */
2869
+ evaluateConditions(conditions, data) {
2870
+ if (!conditions || conditions.length === 0) {
2871
+ return true; // No conditions means always fire
2872
+ }
2873
+ return conditions.every(condition => {
2874
+ // Support dot notation for nested fields (e.g., 'customFields.industry')
2875
+ const fieldValue = condition.field.includes('.')
2876
+ ? this.getNestedValue(data, condition.field)
2877
+ : data[condition.field];
2878
+ const targetValue = condition.value;
2879
+ switch (condition.operator) {
2880
+ case 'equals':
2881
+ return fieldValue === targetValue;
2882
+ case 'not_equals':
2883
+ return fieldValue !== targetValue;
2884
+ case 'contains':
2885
+ return String(fieldValue).includes(String(targetValue));
2886
+ case 'greater_than':
2887
+ return Number(fieldValue) > Number(targetValue);
2888
+ case 'less_than':
2889
+ return Number(fieldValue) < Number(targetValue);
2890
+ case 'in':
2891
+ return Array.isArray(targetValue) && targetValue.includes(fieldValue);
2892
+ case 'not_in':
2893
+ return Array.isArray(targetValue) && !targetValue.includes(fieldValue);
2894
+ default:
2895
+ return false;
2896
+ }
2897
+ });
2898
+ }
2899
+ /**
2900
+ * Execute actions for a triggered event (client-side preview)
2901
+ * Note: Actual execution happens on the backend
2902
+ */
2903
+ async executeActions(trigger, data) {
2904
+ logger.info(`Executing actions for trigger: ${trigger.name}`);
2905
+ for (const action of trigger.actions) {
2906
+ try {
2907
+ await this.executeAction(action, data);
2908
+ }
2909
+ catch (error) {
2910
+ logger.error(`Failed to execute action:`, error);
2911
+ }
2912
+ }
2913
+ }
2914
+ /**
2915
+ * Execute a single action
2916
+ */
2917
+ async executeAction(action, data) {
2918
+ switch (action.type) {
2919
+ case 'send_email':
2920
+ await this.executeSendEmail(action, data);
2921
+ break;
2922
+ case 'webhook':
2923
+ await this.executeWebhook(action, data);
2924
+ break;
2925
+ case 'create_task':
2926
+ await this.executeCreateTask(action, data);
2927
+ break;
2928
+ case 'update_contact':
2929
+ await this.executeUpdateContact(action, data);
2930
+ break;
2931
+ default:
2932
+ logger.warn(`Unknown action type:`, action);
2933
+ }
2934
+ }
2935
+ /**
2936
+ * Execute send email action (via backend API)
2937
+ */
2938
+ async executeSendEmail(action, data) {
2939
+ logger.debug('Sending email:', action);
2940
+ const payload = {
2941
+ to: this.replaceVariables(action.to, data),
2942
+ subject: action.subject ? this.replaceVariables(action.subject, data) : undefined,
2943
+ body: action.body ? this.replaceVariables(action.body, data) : undefined,
2944
+ templateId: action.templateId,
2945
+ cc: action.cc,
2946
+ bcc: action.bcc,
2947
+ from: action.from,
2948
+ delayMinutes: action.delayMinutes,
2949
+ };
2950
+ await this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
2495
2951
  method: 'POST',
2496
- body: JSON.stringify(opportunity),
2952
+ body: JSON.stringify(payload),
2497
2953
  });
2498
2954
  }
2499
2955
  /**
2500
- * Update an existing opportunity
2956
+ * Execute webhook action
2501
2957
  */
2502
- async updateOpportunity(opportunityId, updates) {
2503
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
2958
+ async executeWebhook(action, data) {
2959
+ logger.debug('Calling webhook:', action.url);
2960
+ const body = action.body ? this.replaceVariables(action.body, data) : JSON.stringify(data);
2961
+ await fetch(action.url, {
2962
+ method: action.method,
2963
+ headers: {
2964
+ 'Content-Type': 'application/json',
2965
+ ...action.headers,
2966
+ },
2967
+ body,
2968
+ });
2969
+ }
2970
+ /**
2971
+ * Execute create task action
2972
+ */
2973
+ async executeCreateTask(action, data) {
2974
+ logger.debug('Creating task:', action.title);
2975
+ const dueDate = action.dueDays
2976
+ ? new Date(Date.now() + action.dueDays * 24 * 60 * 60 * 1000).toISOString()
2977
+ : undefined;
2978
+ await this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
2979
+ method: 'POST',
2980
+ body: JSON.stringify({
2981
+ title: this.replaceVariables(action.title, data),
2982
+ description: action.description ? this.replaceVariables(action.description, data) : undefined,
2983
+ priority: action.priority,
2984
+ dueDate,
2985
+ assignedTo: action.assignedTo,
2986
+ relatedContactId: typeof data.contactId === 'string' ? data.contactId : undefined,
2987
+ }),
2988
+ });
2989
+ }
2990
+ /**
2991
+ * Execute update contact action
2992
+ */
2993
+ async executeUpdateContact(action, data) {
2994
+ const contactId = data.contactId || data._id;
2995
+ if (!contactId) {
2996
+ logger.warn('Cannot update contact: no contactId in data');
2997
+ return;
2998
+ }
2999
+ logger.debug('Updating contact:', contactId);
3000
+ await this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2504
3001
  method: 'PUT',
2505
- body: JSON.stringify(updates),
3002
+ body: JSON.stringify(action.updates),
2506
3003
  });
2507
3004
  }
2508
3005
  /**
2509
- * Delete an opportunity
3006
+ * Replace variables in a string template
3007
+ * Supports syntax like {{contact.email}}, {{opportunity.value}}
2510
3008
  */
2511
- async deleteOpportunity(opportunityId) {
2512
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
2513
- method: 'DELETE',
3009
+ replaceVariables(template, data) {
3010
+ return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
3011
+ const value = this.getNestedValue(data, path.trim());
3012
+ return value !== undefined ? String(value) : match;
2514
3013
  });
2515
3014
  }
2516
3015
  /**
2517
- * Move opportunity to a different stage
3016
+ * Get nested value from object using dot notation
3017
+ * Supports dynamic field access including custom fields
2518
3018
  */
2519
- async moveOpportunity(opportunityId, stageId) {
2520
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/move`, {
2521
- method: 'POST',
2522
- body: JSON.stringify({ stageId }),
3019
+ getNestedValue(obj, path) {
3020
+ return path.split('.').reduce((current, key) => {
3021
+ return current !== null && current !== undefined && typeof current === 'object'
3022
+ ? current[key]
3023
+ : undefined;
3024
+ }, obj);
3025
+ }
3026
+ /**
3027
+ * Extract all available field paths from a data object
3028
+ * Useful for dynamic field discovery based on platform-specific attributes
3029
+ * @param obj - The data object to extract fields from
3030
+ * @param prefix - Internal use for nested paths
3031
+ * @param maxDepth - Maximum depth to traverse (default: 3)
3032
+ * @returns Array of field paths (e.g., ['email', 'contact.firstName', 'customFields.industry'])
3033
+ */
3034
+ extractAvailableFields(obj, prefix = '', maxDepth = 3) {
3035
+ if (maxDepth <= 0)
3036
+ return [];
3037
+ const fields = [];
3038
+ for (const key in obj) {
3039
+ if (!obj.hasOwnProperty(key))
3040
+ continue;
3041
+ const value = obj[key];
3042
+ const fieldPath = prefix ? `${prefix}.${key}` : key;
3043
+ fields.push(fieldPath);
3044
+ // Recursively traverse nested objects
3045
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
3046
+ const nestedFields = this.extractAvailableFields(value, fieldPath, maxDepth - 1);
3047
+ fields.push(...nestedFields);
3048
+ }
3049
+ }
3050
+ return fields;
3051
+ }
3052
+ /**
3053
+ * Get available fields from sample data
3054
+ * Helps with dynamic field detection for platform-specific attributes
3055
+ * @param sampleData - Sample data object to analyze
3056
+ * @returns Array of available field paths
3057
+ */
3058
+ getAvailableFields(sampleData) {
3059
+ return this.extractAvailableFields(sampleData);
3060
+ }
3061
+ // ============================================
3062
+ // HELPER METHODS FOR COMMON PATTERNS
3063
+ // ============================================
3064
+ /**
3065
+ * Create a simple email trigger
3066
+ * Helper method for common use case
3067
+ */
3068
+ async createEmailTrigger(config) {
3069
+ return this.createTrigger({
3070
+ name: config.name,
3071
+ eventType: config.eventType,
3072
+ conditions: config.conditions,
3073
+ actions: [
3074
+ {
3075
+ type: 'send_email',
3076
+ to: config.to,
3077
+ subject: config.subject,
3078
+ body: config.body,
3079
+ },
3080
+ ],
3081
+ isActive: true,
3082
+ });
3083
+ }
3084
+ /**
3085
+ * Create a task creation trigger
3086
+ */
3087
+ async createTaskTrigger(config) {
3088
+ return this.createTrigger({
3089
+ name: config.name,
3090
+ eventType: config.eventType,
3091
+ conditions: config.conditions,
3092
+ actions: [
3093
+ {
3094
+ type: 'create_task',
3095
+ title: config.taskTitle,
3096
+ description: config.taskDescription,
3097
+ priority: config.priority,
3098
+ dueDays: config.dueDays,
3099
+ },
3100
+ ],
3101
+ isActive: true,
3102
+ });
3103
+ }
3104
+ /**
3105
+ * Create a webhook trigger
3106
+ */
3107
+ async createWebhookTrigger(config) {
3108
+ return this.createTrigger({
3109
+ name: config.name,
3110
+ eventType: config.eventType,
3111
+ conditions: config.conditions,
3112
+ actions: [
3113
+ {
3114
+ type: 'webhook',
3115
+ url: config.webhookUrl,
3116
+ method: config.method || 'POST',
3117
+ },
3118
+ ],
3119
+ isActive: true,
2523
3120
  });
2524
3121
  }
2525
3122
  }
2526
3123
 
3124
+ /**
3125
+ * Clianta SDK - CRM API Client
3126
+ * @see SDK_VERSION in core/config.ts
3127
+ */
3128
+ /**
3129
+ * CRM API Client for managing contacts and opportunities
3130
+ */
3131
+ class CRMClient {
3132
+ constructor(apiEndpoint, workspaceId, authToken) {
3133
+ this.apiEndpoint = apiEndpoint;
3134
+ this.workspaceId = workspaceId;
3135
+ this.authToken = authToken;
3136
+ this.triggers = new EventTriggersManager(apiEndpoint, workspaceId, authToken);
3137
+ }
3138
+ /**
3139
+ * Set authentication token for API requests
3140
+ */
3141
+ setAuthToken(token) {
3142
+ this.authToken = token;
3143
+ this.triggers.setAuthToken(token);
3144
+ }
3145
+ /**
3146
+ * Validate required parameter exists
3147
+ * @throws {Error} if value is null/undefined or empty string
3148
+ */
3149
+ validateRequired(param, value, methodName) {
3150
+ if (value === null || value === undefined || value === '') {
3151
+ throw new Error(`[CRMClient.${methodName}] ${param} is required`);
3152
+ }
3153
+ }
3154
+ /**
3155
+ * Make authenticated API request
3156
+ */
3157
+ async request(endpoint, options = {}) {
3158
+ const url = `${this.apiEndpoint}${endpoint}`;
3159
+ const headers = {
3160
+ 'Content-Type': 'application/json',
3161
+ ...(options.headers || {}),
3162
+ };
3163
+ if (this.authToken) {
3164
+ headers['Authorization'] = `Bearer ${this.authToken}`;
3165
+ }
3166
+ try {
3167
+ const response = await fetch(url, {
3168
+ ...options,
3169
+ headers,
3170
+ });
3171
+ const data = await response.json();
3172
+ if (!response.ok) {
3173
+ return {
3174
+ success: false,
3175
+ error: data.message || 'Request failed',
3176
+ status: response.status,
3177
+ };
3178
+ }
3179
+ return {
3180
+ success: true,
3181
+ data: data.data || data,
3182
+ status: response.status,
3183
+ };
3184
+ }
3185
+ catch (error) {
3186
+ return {
3187
+ success: false,
3188
+ error: error instanceof Error ? error.message : 'Network error',
3189
+ status: 0,
3190
+ };
3191
+ }
3192
+ }
3193
+ // ============================================
3194
+ // CONTACTS API
3195
+ // ============================================
3196
+ /**
3197
+ * Get all contacts with pagination
3198
+ */
3199
+ async getContacts(params) {
3200
+ const queryParams = new URLSearchParams();
3201
+ if (params?.page)
3202
+ queryParams.set('page', params.page.toString());
3203
+ if (params?.limit)
3204
+ queryParams.set('limit', params.limit.toString());
3205
+ if (params?.search)
3206
+ queryParams.set('search', params.search);
3207
+ if (params?.status)
3208
+ queryParams.set('status', params.status);
3209
+ const query = queryParams.toString();
3210
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts${query ? `?${query}` : ''}`;
3211
+ return this.request(endpoint);
3212
+ }
3213
+ /**
3214
+ * Get a single contact by ID
3215
+ */
3216
+ async getContact(contactId) {
3217
+ this.validateRequired('contactId', contactId, 'getContact');
3218
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
3219
+ }
3220
+ /**
3221
+ * Create a new contact
3222
+ */
3223
+ async createContact(contact) {
3224
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts`, {
3225
+ method: 'POST',
3226
+ body: JSON.stringify(contact),
3227
+ });
3228
+ }
3229
+ /**
3230
+ * Update an existing contact
3231
+ */
3232
+ async updateContact(contactId, updates) {
3233
+ this.validateRequired('contactId', contactId, 'updateContact');
3234
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
3235
+ method: 'PUT',
3236
+ body: JSON.stringify(updates),
3237
+ });
3238
+ }
3239
+ /**
3240
+ * Delete a contact
3241
+ */
3242
+ async deleteContact(contactId) {
3243
+ this.validateRequired('contactId', contactId, 'deleteContact');
3244
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
3245
+ method: 'DELETE',
3246
+ });
3247
+ }
3248
+ // ============================================
3249
+ // OPPORTUNITIES API
3250
+ // ============================================
3251
+ /**
3252
+ * Get all opportunities with pagination
3253
+ */
3254
+ async getOpportunities(params) {
3255
+ const queryParams = new URLSearchParams();
3256
+ if (params?.page)
3257
+ queryParams.set('page', params.page.toString());
3258
+ if (params?.limit)
3259
+ queryParams.set('limit', params.limit.toString());
3260
+ if (params?.pipelineId)
3261
+ queryParams.set('pipelineId', params.pipelineId);
3262
+ if (params?.stageId)
3263
+ queryParams.set('stageId', params.stageId);
3264
+ const query = queryParams.toString();
3265
+ const endpoint = `/api/workspaces/${this.workspaceId}/opportunities${query ? `?${query}` : ''}`;
3266
+ return this.request(endpoint);
3267
+ }
3268
+ /**
3269
+ * Get a single opportunity by ID
3270
+ */
3271
+ async getOpportunity(opportunityId) {
3272
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`);
3273
+ }
3274
+ /**
3275
+ * Create a new opportunity
3276
+ */
3277
+ async createOpportunity(opportunity) {
3278
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities`, {
3279
+ method: 'POST',
3280
+ body: JSON.stringify(opportunity),
3281
+ });
3282
+ }
3283
+ /**
3284
+ * Update an existing opportunity
3285
+ */
3286
+ async updateOpportunity(opportunityId, updates) {
3287
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
3288
+ method: 'PUT',
3289
+ body: JSON.stringify(updates),
3290
+ });
3291
+ }
3292
+ /**
3293
+ * Delete an opportunity
3294
+ */
3295
+ async deleteOpportunity(opportunityId) {
3296
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
3297
+ method: 'DELETE',
3298
+ });
3299
+ }
3300
+ /**
3301
+ * Move opportunity to a different stage
3302
+ */
3303
+ async moveOpportunity(opportunityId, stageId) {
3304
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/move`, {
3305
+ method: 'POST',
3306
+ body: JSON.stringify({ stageId }),
3307
+ });
3308
+ }
3309
+ // ============================================
3310
+ // COMPANIES API
3311
+ // ============================================
3312
+ /**
3313
+ * Get all companies with pagination
3314
+ */
3315
+ async getCompanies(params) {
3316
+ const queryParams = new URLSearchParams();
3317
+ if (params?.page)
3318
+ queryParams.set('page', params.page.toString());
3319
+ if (params?.limit)
3320
+ queryParams.set('limit', params.limit.toString());
3321
+ if (params?.search)
3322
+ queryParams.set('search', params.search);
3323
+ if (params?.status)
3324
+ queryParams.set('status', params.status);
3325
+ if (params?.industry)
3326
+ queryParams.set('industry', params.industry);
3327
+ const query = queryParams.toString();
3328
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies${query ? `?${query}` : ''}`;
3329
+ return this.request(endpoint);
3330
+ }
3331
+ /**
3332
+ * Get a single company by ID
3333
+ */
3334
+ async getCompany(companyId) {
3335
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`);
3336
+ }
3337
+ /**
3338
+ * Create a new company
3339
+ */
3340
+ async createCompany(company) {
3341
+ return this.request(`/api/workspaces/${this.workspaceId}/companies`, {
3342
+ method: 'POST',
3343
+ body: JSON.stringify(company),
3344
+ });
3345
+ }
3346
+ /**
3347
+ * Update an existing company
3348
+ */
3349
+ async updateCompany(companyId, updates) {
3350
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
3351
+ method: 'PUT',
3352
+ body: JSON.stringify(updates),
3353
+ });
3354
+ }
3355
+ /**
3356
+ * Delete a company
3357
+ */
3358
+ async deleteCompany(companyId) {
3359
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
3360
+ method: 'DELETE',
3361
+ });
3362
+ }
3363
+ /**
3364
+ * Get contacts belonging to a company
3365
+ */
3366
+ async getCompanyContacts(companyId, params) {
3367
+ const queryParams = new URLSearchParams();
3368
+ if (params?.page)
3369
+ queryParams.set('page', params.page.toString());
3370
+ if (params?.limit)
3371
+ queryParams.set('limit', params.limit.toString());
3372
+ const query = queryParams.toString();
3373
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/contacts${query ? `?${query}` : ''}`;
3374
+ return this.request(endpoint);
3375
+ }
3376
+ /**
3377
+ * Get deals/opportunities belonging to a company
3378
+ */
3379
+ async getCompanyDeals(companyId, params) {
3380
+ const queryParams = new URLSearchParams();
3381
+ if (params?.page)
3382
+ queryParams.set('page', params.page.toString());
3383
+ if (params?.limit)
3384
+ queryParams.set('limit', params.limit.toString());
3385
+ const query = queryParams.toString();
3386
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/deals${query ? `?${query}` : ''}`;
3387
+ return this.request(endpoint);
3388
+ }
3389
+ // ============================================
3390
+ // PIPELINES API
3391
+ // ============================================
3392
+ /**
3393
+ * Get all pipelines
3394
+ */
3395
+ async getPipelines() {
3396
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines`);
3397
+ }
3398
+ /**
3399
+ * Get a single pipeline by ID
3400
+ */
3401
+ async getPipeline(pipelineId) {
3402
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`);
3403
+ }
3404
+ /**
3405
+ * Create a new pipeline
3406
+ */
3407
+ async createPipeline(pipeline) {
3408
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines`, {
3409
+ method: 'POST',
3410
+ body: JSON.stringify(pipeline),
3411
+ });
3412
+ }
3413
+ /**
3414
+ * Update an existing pipeline
3415
+ */
3416
+ async updatePipeline(pipelineId, updates) {
3417
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
3418
+ method: 'PUT',
3419
+ body: JSON.stringify(updates),
3420
+ });
3421
+ }
3422
+ /**
3423
+ * Delete a pipeline
3424
+ */
3425
+ async deletePipeline(pipelineId) {
3426
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
3427
+ method: 'DELETE',
3428
+ });
3429
+ }
3430
+ // ============================================
3431
+ // TASKS API
3432
+ // ============================================
3433
+ /**
3434
+ * Get all tasks with pagination
3435
+ */
3436
+ async getTasks(params) {
3437
+ const queryParams = new URLSearchParams();
3438
+ if (params?.page)
3439
+ queryParams.set('page', params.page.toString());
3440
+ if (params?.limit)
3441
+ queryParams.set('limit', params.limit.toString());
3442
+ if (params?.status)
3443
+ queryParams.set('status', params.status);
3444
+ if (params?.priority)
3445
+ queryParams.set('priority', params.priority);
3446
+ if (params?.contactId)
3447
+ queryParams.set('contactId', params.contactId);
3448
+ if (params?.companyId)
3449
+ queryParams.set('companyId', params.companyId);
3450
+ if (params?.opportunityId)
3451
+ queryParams.set('opportunityId', params.opportunityId);
3452
+ const query = queryParams.toString();
3453
+ const endpoint = `/api/workspaces/${this.workspaceId}/tasks${query ? `?${query}` : ''}`;
3454
+ return this.request(endpoint);
3455
+ }
3456
+ /**
3457
+ * Get a single task by ID
3458
+ */
3459
+ async getTask(taskId) {
3460
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`);
3461
+ }
3462
+ /**
3463
+ * Create a new task
3464
+ */
3465
+ async createTask(task) {
3466
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
3467
+ method: 'POST',
3468
+ body: JSON.stringify(task),
3469
+ });
3470
+ }
3471
+ /**
3472
+ * Update an existing task
3473
+ */
3474
+ async updateTask(taskId, updates) {
3475
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
3476
+ method: 'PUT',
3477
+ body: JSON.stringify(updates),
3478
+ });
3479
+ }
3480
+ /**
3481
+ * Mark a task as completed
3482
+ */
3483
+ async completeTask(taskId) {
3484
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}/complete`, {
3485
+ method: 'PATCH',
3486
+ });
3487
+ }
3488
+ /**
3489
+ * Delete a task
3490
+ */
3491
+ async deleteTask(taskId) {
3492
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
3493
+ method: 'DELETE',
3494
+ });
3495
+ }
3496
+ // ============================================
3497
+ // ACTIVITIES API
3498
+ // ============================================
3499
+ /**
3500
+ * Get activities for a contact
3501
+ */
3502
+ async getContactActivities(contactId, params) {
3503
+ const queryParams = new URLSearchParams();
3504
+ if (params?.page)
3505
+ queryParams.set('page', params.page.toString());
3506
+ if (params?.limit)
3507
+ queryParams.set('limit', params.limit.toString());
3508
+ if (params?.type)
3509
+ queryParams.set('type', params.type);
3510
+ const query = queryParams.toString();
3511
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
3512
+ return this.request(endpoint);
3513
+ }
3514
+ /**
3515
+ * Get activities for an opportunity/deal
3516
+ */
3517
+ async getOpportunityActivities(opportunityId, params) {
3518
+ const queryParams = new URLSearchParams();
3519
+ if (params?.page)
3520
+ queryParams.set('page', params.page.toString());
3521
+ if (params?.limit)
3522
+ queryParams.set('limit', params.limit.toString());
3523
+ if (params?.type)
3524
+ queryParams.set('type', params.type);
3525
+ const query = queryParams.toString();
3526
+ const endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/activities${query ? `?${query}` : ''}`;
3527
+ return this.request(endpoint);
3528
+ }
3529
+ /**
3530
+ * Create a new activity
3531
+ */
3532
+ async createActivity(activity) {
3533
+ // Determine the correct endpoint based on related entity
3534
+ let endpoint;
3535
+ if (activity.opportunityId) {
3536
+ endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${activity.opportunityId}/activities`;
3537
+ }
3538
+ else if (activity.contactId) {
3539
+ endpoint = `/api/workspaces/${this.workspaceId}/contacts/${activity.contactId}/activities`;
3540
+ }
3541
+ else {
3542
+ endpoint = `/api/workspaces/${this.workspaceId}/activities`;
3543
+ }
3544
+ return this.request(endpoint, {
3545
+ method: 'POST',
3546
+ body: JSON.stringify(activity),
3547
+ });
3548
+ }
3549
+ /**
3550
+ * Update an existing activity
3551
+ */
3552
+ async updateActivity(activityId, updates) {
3553
+ return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3554
+ method: 'PATCH',
3555
+ body: JSON.stringify(updates),
3556
+ });
3557
+ }
3558
+ /**
3559
+ * Delete an activity
3560
+ */
3561
+ async deleteActivity(activityId) {
3562
+ return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3563
+ method: 'DELETE',
3564
+ });
3565
+ }
3566
+ /**
3567
+ * Log a call activity
3568
+ */
3569
+ async logCall(data) {
3570
+ return this.createActivity({
3571
+ type: 'call',
3572
+ title: `${data.direction === 'inbound' ? 'Inbound' : 'Outbound'} Call`,
3573
+ direction: data.direction,
3574
+ duration: data.duration,
3575
+ outcome: data.outcome,
3576
+ description: data.notes,
3577
+ contactId: data.contactId,
3578
+ opportunityId: data.opportunityId,
3579
+ });
3580
+ }
3581
+ /**
3582
+ * Log a meeting activity
3583
+ */
3584
+ async logMeeting(data) {
3585
+ return this.createActivity({
3586
+ type: 'meeting',
3587
+ title: data.title,
3588
+ duration: data.duration,
3589
+ outcome: data.outcome,
3590
+ description: data.notes,
3591
+ contactId: data.contactId,
3592
+ opportunityId: data.opportunityId,
3593
+ });
3594
+ }
3595
+ /**
3596
+ * Add a note to a contact or opportunity
3597
+ */
3598
+ async addNote(data) {
3599
+ return this.createActivity({
3600
+ type: 'note',
3601
+ title: 'Note',
3602
+ description: data.content,
3603
+ contactId: data.contactId,
3604
+ opportunityId: data.opportunityId,
3605
+ });
3606
+ }
3607
+ // ============================================
3608
+ // EMAIL TEMPLATES API
3609
+ // ============================================
3610
+ /**
3611
+ * Get all email templates
3612
+ */
3613
+ async getEmailTemplates(params) {
3614
+ const queryParams = new URLSearchParams();
3615
+ if (params?.page)
3616
+ queryParams.set('page', params.page.toString());
3617
+ if (params?.limit)
3618
+ queryParams.set('limit', params.limit.toString());
3619
+ const query = queryParams.toString();
3620
+ const endpoint = `/api/workspaces/${this.workspaceId}/email-templates${query ? `?${query}` : ''}`;
3621
+ return this.request(endpoint);
3622
+ }
3623
+ /**
3624
+ * Get a single email template by ID
3625
+ */
3626
+ async getEmailTemplate(templateId) {
3627
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`);
3628
+ }
3629
+ /**
3630
+ * Create a new email template
3631
+ */
3632
+ async createEmailTemplate(template) {
3633
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates`, {
3634
+ method: 'POST',
3635
+ body: JSON.stringify(template),
3636
+ });
3637
+ }
3638
+ /**
3639
+ * Update an email template
3640
+ */
3641
+ async updateEmailTemplate(templateId, updates) {
3642
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3643
+ method: 'PUT',
3644
+ body: JSON.stringify(updates),
3645
+ });
3646
+ }
3647
+ /**
3648
+ * Delete an email template
3649
+ */
3650
+ async deleteEmailTemplate(templateId) {
3651
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3652
+ method: 'DELETE',
3653
+ });
3654
+ }
3655
+ /**
3656
+ * Send an email using a template
3657
+ */
3658
+ async sendEmail(data) {
3659
+ return this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
3660
+ method: 'POST',
3661
+ body: JSON.stringify(data),
3662
+ });
3663
+ }
3664
+ // ============================================
3665
+ // EVENT TRIGGERS API (delegated to triggers manager)
3666
+ // ============================================
3667
+ /**
3668
+ * Get all event triggers
3669
+ */
3670
+ async getEventTriggers() {
3671
+ return this.triggers.getTriggers();
3672
+ }
3673
+ /**
3674
+ * Create a new event trigger
3675
+ */
3676
+ async createEventTrigger(trigger) {
3677
+ return this.triggers.createTrigger(trigger);
3678
+ }
3679
+ /**
3680
+ * Update an event trigger
3681
+ */
3682
+ async updateEventTrigger(triggerId, updates) {
3683
+ return this.triggers.updateTrigger(triggerId, updates);
3684
+ }
3685
+ /**
3686
+ * Delete an event trigger
3687
+ */
3688
+ async deleteEventTrigger(triggerId) {
3689
+ return this.triggers.deleteTrigger(triggerId);
3690
+ }
3691
+ }
3692
+
2527
3693
  /**
2528
3694
  * Clianta SDK
2529
3695
  * Professional CRM and tracking SDK for lead generation
@@ -2576,6 +3742,7 @@ if (typeof window !== 'undefined') {
2576
3742
  Tracker,
2577
3743
  CRMClient,
2578
3744
  ConsentManager,
3745
+ EventTriggersManager,
2579
3746
  };
2580
3747
  }
2581
3748
 
@@ -2640,7 +3807,7 @@ function useClianta() {
2640
3807
  return useContext(CliantaContext);
2641
3808
  }
2642
3809
  /**
2643
- * useClinataTrack - Convenience hook for tracking events
3810
+ * useCliantaTrack - Convenience hook for tracking events
2644
3811
  *
2645
3812
  * @example
2646
3813
  * const track = useCliantaTrack();