@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/react.cjs.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
|
*/
|
|
@@ -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.
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
});
|
|
749
|
+
this.boundBeforeUnload = () => this.flushSync();
|
|
750
|
+
window.addEventListener('beforeunload', this.boundBeforeUnload);
|
|
709
751
|
// Flush when page becomes hidden
|
|
710
|
-
|
|
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
|
-
|
|
717
|
-
|
|
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
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
847
|
+
history.replaceState = function (...args) {
|
|
848
|
+
self.originalReplaceState.apply(history, args);
|
|
849
|
+
self.trackPageView();
|
|
803
850
|
};
|
|
804
851
|
// Handle back/forward navigation
|
|
805
|
-
|
|
806
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1292
|
-
if (
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
1581
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
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
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
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
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
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
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 -
|
|
2358
|
-
*
|
|
2693
|
+
* Clianta SDK - Event Triggers Manager
|
|
2694
|
+
* Manages event-driven automation and email notifications
|
|
2359
2695
|
*/
|
|
2360
2696
|
/**
|
|
2361
|
-
*
|
|
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
|
|
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
|
|
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
|
-
//
|
|
2759
|
+
// TRIGGER MANAGEMENT
|
|
2416
2760
|
// ============================================
|
|
2417
2761
|
/**
|
|
2418
|
-
* Get all
|
|
2762
|
+
* Get all event triggers
|
|
2419
2763
|
*/
|
|
2420
|
-
async
|
|
2421
|
-
|
|
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
|
|
2768
|
+
* Get a single trigger by ID
|
|
2436
2769
|
*/
|
|
2437
|
-
async
|
|
2438
|
-
return this.request(`/api/workspaces/${this.workspaceId}/
|
|
2770
|
+
async getTrigger(triggerId) {
|
|
2771
|
+
return this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`);
|
|
2439
2772
|
}
|
|
2440
2773
|
/**
|
|
2441
|
-
* Create a new
|
|
2774
|
+
* Create a new event trigger
|
|
2442
2775
|
*/
|
|
2443
|
-
async
|
|
2444
|
-
|
|
2776
|
+
async createTrigger(trigger) {
|
|
2777
|
+
const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers`, {
|
|
2445
2778
|
method: 'POST',
|
|
2446
|
-
body: JSON.stringify(
|
|
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
|
|
2788
|
+
* Update an existing trigger
|
|
2451
2789
|
*/
|
|
2452
|
-
async
|
|
2453
|
-
|
|
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
|
|
2802
|
+
* Delete a trigger
|
|
2460
2803
|
*/
|
|
2461
|
-
async
|
|
2462
|
-
|
|
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
|
-
//
|
|
2827
|
+
// EVENT HANDLING (CLIENT-SIDE)
|
|
2468
2828
|
// ============================================
|
|
2469
2829
|
/**
|
|
2470
|
-
*
|
|
2830
|
+
* Register a local event listener for client-side triggers
|
|
2831
|
+
* This allows immediate client-side reactions to events
|
|
2471
2832
|
*/
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
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
|
-
*
|
|
2841
|
+
* Remove an event listener
|
|
2488
2842
|
*/
|
|
2489
|
-
|
|
2490
|
-
|
|
2843
|
+
off(eventType, callback) {
|
|
2844
|
+
const listeners = this.listeners.get(eventType);
|
|
2845
|
+
if (listeners) {
|
|
2846
|
+
listeners.delete(callback);
|
|
2847
|
+
}
|
|
2491
2848
|
}
|
|
2492
2849
|
/**
|
|
2493
|
-
*
|
|
2850
|
+
* Emit an event (client-side only)
|
|
2851
|
+
* This will trigger any registered local listeners
|
|
2494
2852
|
*/
|
|
2495
|
-
|
|
2496
|
-
|
|
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(
|
|
2954
|
+
body: JSON.stringify(payload),
|
|
2499
2955
|
});
|
|
2500
2956
|
}
|
|
2501
2957
|
/**
|
|
2502
|
-
*
|
|
2958
|
+
* Execute webhook action
|
|
2503
2959
|
*/
|
|
2504
|
-
async
|
|
2505
|
-
|
|
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
|
-
*
|
|
3008
|
+
* Replace variables in a string template
|
|
3009
|
+
* Supports syntax like {{contact.email}}, {{opportunity.value}}
|
|
2512
3010
|
*/
|
|
2513
|
-
|
|
2514
|
-
return
|
|
2515
|
-
|
|
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
|
-
*
|
|
3018
|
+
* Get nested value from object using dot notation
|
|
3019
|
+
* Supports dynamic field access including custom fields
|
|
2520
3020
|
*/
|
|
2521
|
-
|
|
2522
|
-
return
|
|
2523
|
-
|
|
2524
|
-
|
|
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
|
-
*
|
|
3812
|
+
* useCliantaTrack - Convenience hook for tracking events
|
|
2646
3813
|
*
|
|
2647
3814
|
* @example
|
|
2648
3815
|
* const track = useCliantaTrack();
|