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