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