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