@clianta/sdk 1.2.0 → 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 +17 -0
- package/README.md +71 -1
- package/dist/clianta.cjs.js +765 -118
- package/dist/clianta.cjs.js.map +1 -1
- package/dist/clianta.esm.js +765 -119
- package/dist/clianta.esm.js.map +1 -1
- package/dist/clianta.umd.js +765 -118
- 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 +354 -3
- package/dist/react.cjs.js +764 -118
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.esm.js +764 -118
- package/dist/react.esm.js.map +1 -1
- package/dist/vue.cjs.js +3900 -0
- package/dist/vue.cjs.js.map +1 -0
- package/dist/vue.d.ts +201 -0
- package/dist/vue.esm.js +3893 -0
- package/dist/vue.esm.js.map +1 -0
- package/package.json +16 -3
package/dist/clianta.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
|
*/
|
|
@@ -12,7 +12,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
12
12
|
* @see SDK_VERSION in core/config.ts
|
|
13
13
|
*/
|
|
14
14
|
/** SDK Version */
|
|
15
|
-
const SDK_VERSION = '1.
|
|
15
|
+
const SDK_VERSION = '1.3.0';
|
|
16
16
|
/** Default API endpoint based on environment */
|
|
17
17
|
const getDefaultApiEndpoint = () => {
|
|
18
18
|
if (typeof window === 'undefined')
|
|
@@ -32,7 +32,6 @@ const DEFAULT_PLUGINS = [
|
|
|
32
32
|
'engagement',
|
|
33
33
|
'downloads',
|
|
34
34
|
'exitIntent',
|
|
35
|
-
'popupForms',
|
|
36
35
|
];
|
|
37
36
|
/** Default configuration values */
|
|
38
37
|
const DEFAULT_CONFIG = {
|
|
@@ -590,6 +589,10 @@ class EventQueue {
|
|
|
590
589
|
this.isFlushing = false;
|
|
591
590
|
/** Rate limiting: timestamps of recent events */
|
|
592
591
|
this.eventTimestamps = [];
|
|
592
|
+
/** Unload handler references for cleanup */
|
|
593
|
+
this.boundBeforeUnload = null;
|
|
594
|
+
this.boundVisibilityChange = null;
|
|
595
|
+
this.boundPageHide = null;
|
|
593
596
|
this.transport = transport;
|
|
594
597
|
this.config = {
|
|
595
598
|
batchSize: config.batchSize ?? 10,
|
|
@@ -704,13 +707,25 @@ class EventQueue {
|
|
|
704
707
|
this.persistQueue([]);
|
|
705
708
|
}
|
|
706
709
|
/**
|
|
707
|
-
* Stop the flush timer
|
|
710
|
+
* Stop the flush timer and cleanup handlers
|
|
708
711
|
*/
|
|
709
712
|
destroy() {
|
|
710
713
|
if (this.flushTimer) {
|
|
711
714
|
clearInterval(this.flushTimer);
|
|
712
715
|
this.flushTimer = null;
|
|
713
716
|
}
|
|
717
|
+
// Remove unload handlers
|
|
718
|
+
if (typeof window !== 'undefined') {
|
|
719
|
+
if (this.boundBeforeUnload) {
|
|
720
|
+
window.removeEventListener('beforeunload', this.boundBeforeUnload);
|
|
721
|
+
}
|
|
722
|
+
if (this.boundVisibilityChange) {
|
|
723
|
+
window.removeEventListener('visibilitychange', this.boundVisibilityChange);
|
|
724
|
+
}
|
|
725
|
+
if (this.boundPageHide) {
|
|
726
|
+
window.removeEventListener('pagehide', this.boundPageHide);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
714
729
|
}
|
|
715
730
|
/**
|
|
716
731
|
* Start auto-flush timer
|
|
@@ -730,19 +745,18 @@ class EventQueue {
|
|
|
730
745
|
if (typeof window === 'undefined')
|
|
731
746
|
return;
|
|
732
747
|
// Flush on page unload
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
});
|
|
748
|
+
this.boundBeforeUnload = () => this.flushSync();
|
|
749
|
+
window.addEventListener('beforeunload', this.boundBeforeUnload);
|
|
736
750
|
// Flush when page becomes hidden
|
|
737
|
-
|
|
751
|
+
this.boundVisibilityChange = () => {
|
|
738
752
|
if (document.visibilityState === 'hidden') {
|
|
739
753
|
this.flushSync();
|
|
740
754
|
}
|
|
741
|
-
}
|
|
755
|
+
};
|
|
756
|
+
window.addEventListener('visibilitychange', this.boundVisibilityChange);
|
|
742
757
|
// Flush on page hide (iOS Safari)
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
});
|
|
758
|
+
this.boundPageHide = () => this.flushSync();
|
|
759
|
+
window.addEventListener('pagehide', this.boundPageHide);
|
|
746
760
|
}
|
|
747
761
|
/**
|
|
748
762
|
* Persist queue to localStorage
|
|
@@ -885,6 +899,10 @@ class ScrollPlugin extends BasePlugin {
|
|
|
885
899
|
this.pageLoadTime = 0;
|
|
886
900
|
this.scrollTimeout = null;
|
|
887
901
|
this.boundHandler = null;
|
|
902
|
+
/** SPA navigation support */
|
|
903
|
+
this.originalPushState = null;
|
|
904
|
+
this.originalReplaceState = null;
|
|
905
|
+
this.popstateHandler = null;
|
|
888
906
|
}
|
|
889
907
|
init(tracker) {
|
|
890
908
|
super.init(tracker);
|
|
@@ -892,6 +910,8 @@ class ScrollPlugin extends BasePlugin {
|
|
|
892
910
|
if (typeof window !== 'undefined') {
|
|
893
911
|
this.boundHandler = this.handleScroll.bind(this);
|
|
894
912
|
window.addEventListener('scroll', this.boundHandler, { passive: true });
|
|
913
|
+
// Setup SPA navigation reset
|
|
914
|
+
this.setupNavigationReset();
|
|
895
915
|
}
|
|
896
916
|
}
|
|
897
917
|
destroy() {
|
|
@@ -901,8 +921,53 @@ class ScrollPlugin extends BasePlugin {
|
|
|
901
921
|
if (this.scrollTimeout) {
|
|
902
922
|
clearTimeout(this.scrollTimeout);
|
|
903
923
|
}
|
|
924
|
+
// Restore original history methods
|
|
925
|
+
if (this.originalPushState) {
|
|
926
|
+
history.pushState = this.originalPushState;
|
|
927
|
+
this.originalPushState = null;
|
|
928
|
+
}
|
|
929
|
+
if (this.originalReplaceState) {
|
|
930
|
+
history.replaceState = this.originalReplaceState;
|
|
931
|
+
this.originalReplaceState = null;
|
|
932
|
+
}
|
|
933
|
+
// Remove popstate listener
|
|
934
|
+
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
935
|
+
window.removeEventListener('popstate', this.popstateHandler);
|
|
936
|
+
this.popstateHandler = null;
|
|
937
|
+
}
|
|
904
938
|
super.destroy();
|
|
905
939
|
}
|
|
940
|
+
/**
|
|
941
|
+
* Reset scroll tracking for SPA navigation
|
|
942
|
+
*/
|
|
943
|
+
resetForNavigation() {
|
|
944
|
+
this.milestonesReached.clear();
|
|
945
|
+
this.maxScrollDepth = 0;
|
|
946
|
+
this.pageLoadTime = Date.now();
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Setup History API interception for SPA navigation
|
|
950
|
+
*/
|
|
951
|
+
setupNavigationReset() {
|
|
952
|
+
if (typeof window === 'undefined')
|
|
953
|
+
return;
|
|
954
|
+
// Store originals for cleanup
|
|
955
|
+
this.originalPushState = history.pushState;
|
|
956
|
+
this.originalReplaceState = history.replaceState;
|
|
957
|
+
// Intercept pushState and replaceState
|
|
958
|
+
const self = this;
|
|
959
|
+
history.pushState = function (...args) {
|
|
960
|
+
self.originalPushState.apply(history, args);
|
|
961
|
+
self.resetForNavigation();
|
|
962
|
+
};
|
|
963
|
+
history.replaceState = function (...args) {
|
|
964
|
+
self.originalReplaceState.apply(history, args);
|
|
965
|
+
self.resetForNavigation();
|
|
966
|
+
};
|
|
967
|
+
// Handle back/forward navigation
|
|
968
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
969
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
970
|
+
}
|
|
906
971
|
handleScroll() {
|
|
907
972
|
// Debounce scroll tracking
|
|
908
973
|
if (this.scrollTimeout) {
|
|
@@ -955,6 +1020,7 @@ class FormsPlugin extends BasePlugin {
|
|
|
955
1020
|
this.trackedForms = new WeakSet();
|
|
956
1021
|
this.formInteractions = new Set();
|
|
957
1022
|
this.observer = null;
|
|
1023
|
+
this.listeners = [];
|
|
958
1024
|
}
|
|
959
1025
|
init(tracker) {
|
|
960
1026
|
super.init(tracker);
|
|
@@ -973,8 +1039,20 @@ class FormsPlugin extends BasePlugin {
|
|
|
973
1039
|
this.observer.disconnect();
|
|
974
1040
|
this.observer = null;
|
|
975
1041
|
}
|
|
1042
|
+
// Remove all tracked event listeners
|
|
1043
|
+
for (const { element, event, handler } of this.listeners) {
|
|
1044
|
+
element.removeEventListener(event, handler);
|
|
1045
|
+
}
|
|
1046
|
+
this.listeners = [];
|
|
976
1047
|
super.destroy();
|
|
977
1048
|
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Track event listener for cleanup
|
|
1051
|
+
*/
|
|
1052
|
+
addListener(element, event, handler) {
|
|
1053
|
+
element.addEventListener(event, handler);
|
|
1054
|
+
this.listeners.push({ element, event, handler });
|
|
1055
|
+
}
|
|
978
1056
|
trackAllForms() {
|
|
979
1057
|
document.querySelectorAll('form').forEach((form) => {
|
|
980
1058
|
this.setupFormTracking(form);
|
|
@@ -1000,7 +1078,7 @@ class FormsPlugin extends BasePlugin {
|
|
|
1000
1078
|
if (!field.name || field.type === 'submit' || field.type === 'button')
|
|
1001
1079
|
return;
|
|
1002
1080
|
['focus', 'blur', 'change'].forEach((eventType) => {
|
|
1003
|
-
|
|
1081
|
+
const handler = () => {
|
|
1004
1082
|
const key = `${formId}-${field.name}-${eventType}`;
|
|
1005
1083
|
if (!this.formInteractions.has(key)) {
|
|
1006
1084
|
this.formInteractions.add(key);
|
|
@@ -1011,12 +1089,13 @@ class FormsPlugin extends BasePlugin {
|
|
|
1011
1089
|
interactionType: eventType,
|
|
1012
1090
|
});
|
|
1013
1091
|
}
|
|
1014
|
-
}
|
|
1092
|
+
};
|
|
1093
|
+
this.addListener(field, eventType, handler);
|
|
1015
1094
|
});
|
|
1016
1095
|
}
|
|
1017
1096
|
});
|
|
1018
1097
|
// Track form submission
|
|
1019
|
-
|
|
1098
|
+
const submitHandler = () => {
|
|
1020
1099
|
this.track('form_submit', 'Form Submitted', {
|
|
1021
1100
|
formId,
|
|
1022
1101
|
action: form.action,
|
|
@@ -1024,7 +1103,8 @@ class FormsPlugin extends BasePlugin {
|
|
|
1024
1103
|
});
|
|
1025
1104
|
// Auto-identify if email field found
|
|
1026
1105
|
this.autoIdentify(form);
|
|
1027
|
-
}
|
|
1106
|
+
};
|
|
1107
|
+
this.addListener(form, 'submit', submitHandler);
|
|
1028
1108
|
}
|
|
1029
1109
|
autoIdentify(form) {
|
|
1030
1110
|
const emailField = form.querySelector('input[type="email"], input[name*="email"]');
|
|
@@ -1108,6 +1188,7 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1108
1188
|
this.engagementTimeout = null;
|
|
1109
1189
|
this.boundMarkEngaged = null;
|
|
1110
1190
|
this.boundTrackTimeOnPage = null;
|
|
1191
|
+
this.boundVisibilityHandler = null;
|
|
1111
1192
|
}
|
|
1112
1193
|
init(tracker) {
|
|
1113
1194
|
super.init(tracker);
|
|
@@ -1118,12 +1199,7 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1118
1199
|
// Setup engagement detection
|
|
1119
1200
|
this.boundMarkEngaged = this.markEngaged.bind(this);
|
|
1120
1201
|
this.boundTrackTimeOnPage = this.trackTimeOnPage.bind(this);
|
|
1121
|
-
|
|
1122
|
-
document.addEventListener(event, this.boundMarkEngaged, { passive: true });
|
|
1123
|
-
});
|
|
1124
|
-
// Track time on page before unload
|
|
1125
|
-
window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
|
|
1126
|
-
window.addEventListener('visibilitychange', () => {
|
|
1202
|
+
this.boundVisibilityHandler = () => {
|
|
1127
1203
|
if (document.visibilityState === 'hidden') {
|
|
1128
1204
|
this.trackTimeOnPage();
|
|
1129
1205
|
}
|
|
@@ -1131,7 +1207,13 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1131
1207
|
// Reset engagement timer when page becomes visible again
|
|
1132
1208
|
this.engagementStartTime = Date.now();
|
|
1133
1209
|
}
|
|
1210
|
+
};
|
|
1211
|
+
['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
|
|
1212
|
+
document.addEventListener(event, this.boundMarkEngaged, { passive: true });
|
|
1134
1213
|
});
|
|
1214
|
+
// Track time on page before unload
|
|
1215
|
+
window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
|
|
1216
|
+
document.addEventListener('visibilitychange', this.boundVisibilityHandler);
|
|
1135
1217
|
}
|
|
1136
1218
|
destroy() {
|
|
1137
1219
|
if (this.boundMarkEngaged && typeof document !== 'undefined') {
|
|
@@ -1142,6 +1224,9 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1142
1224
|
if (this.boundTrackTimeOnPage && typeof window !== 'undefined') {
|
|
1143
1225
|
window.removeEventListener('beforeunload', this.boundTrackTimeOnPage);
|
|
1144
1226
|
}
|
|
1227
|
+
if (this.boundVisibilityHandler && typeof document !== 'undefined') {
|
|
1228
|
+
document.removeEventListener('visibilitychange', this.boundVisibilityHandler);
|
|
1229
|
+
}
|
|
1145
1230
|
if (this.engagementTimeout) {
|
|
1146
1231
|
clearTimeout(this.engagementTimeout);
|
|
1147
1232
|
}
|
|
@@ -1186,20 +1271,69 @@ class DownloadsPlugin extends BasePlugin {
|
|
|
1186
1271
|
this.name = 'downloads';
|
|
1187
1272
|
this.trackedDownloads = new Set();
|
|
1188
1273
|
this.boundHandler = null;
|
|
1274
|
+
/** SPA navigation support */
|
|
1275
|
+
this.originalPushState = null;
|
|
1276
|
+
this.originalReplaceState = null;
|
|
1277
|
+
this.popstateHandler = null;
|
|
1189
1278
|
}
|
|
1190
1279
|
init(tracker) {
|
|
1191
1280
|
super.init(tracker);
|
|
1192
1281
|
if (typeof document !== 'undefined') {
|
|
1193
1282
|
this.boundHandler = this.handleClick.bind(this);
|
|
1194
1283
|
document.addEventListener('click', this.boundHandler, true);
|
|
1284
|
+
// Setup SPA navigation reset
|
|
1285
|
+
this.setupNavigationReset();
|
|
1195
1286
|
}
|
|
1196
1287
|
}
|
|
1197
1288
|
destroy() {
|
|
1198
1289
|
if (this.boundHandler && typeof document !== 'undefined') {
|
|
1199
1290
|
document.removeEventListener('click', this.boundHandler, true);
|
|
1200
1291
|
}
|
|
1292
|
+
// Restore original history methods
|
|
1293
|
+
if (this.originalPushState) {
|
|
1294
|
+
history.pushState = this.originalPushState;
|
|
1295
|
+
this.originalPushState = null;
|
|
1296
|
+
}
|
|
1297
|
+
if (this.originalReplaceState) {
|
|
1298
|
+
history.replaceState = this.originalReplaceState;
|
|
1299
|
+
this.originalReplaceState = null;
|
|
1300
|
+
}
|
|
1301
|
+
// Remove popstate listener
|
|
1302
|
+
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
1303
|
+
window.removeEventListener('popstate', this.popstateHandler);
|
|
1304
|
+
this.popstateHandler = null;
|
|
1305
|
+
}
|
|
1201
1306
|
super.destroy();
|
|
1202
1307
|
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Reset download tracking for SPA navigation
|
|
1310
|
+
*/
|
|
1311
|
+
resetForNavigation() {
|
|
1312
|
+
this.trackedDownloads.clear();
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Setup History API interception for SPA navigation
|
|
1316
|
+
*/
|
|
1317
|
+
setupNavigationReset() {
|
|
1318
|
+
if (typeof window === 'undefined')
|
|
1319
|
+
return;
|
|
1320
|
+
// Store originals for cleanup
|
|
1321
|
+
this.originalPushState = history.pushState;
|
|
1322
|
+
this.originalReplaceState = history.replaceState;
|
|
1323
|
+
// Intercept pushState and replaceState
|
|
1324
|
+
const self = this;
|
|
1325
|
+
history.pushState = function (...args) {
|
|
1326
|
+
self.originalPushState.apply(history, args);
|
|
1327
|
+
self.resetForNavigation();
|
|
1328
|
+
};
|
|
1329
|
+
history.replaceState = function (...args) {
|
|
1330
|
+
self.originalReplaceState.apply(history, args);
|
|
1331
|
+
self.resetForNavigation();
|
|
1332
|
+
};
|
|
1333
|
+
// Handle back/forward navigation
|
|
1334
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1335
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
1336
|
+
}
|
|
1203
1337
|
handleClick(e) {
|
|
1204
1338
|
const link = e.target.closest('a');
|
|
1205
1339
|
if (!link || !link.href)
|
|
@@ -1325,17 +1459,34 @@ class PerformancePlugin extends BasePlugin {
|
|
|
1325
1459
|
constructor() {
|
|
1326
1460
|
super(...arguments);
|
|
1327
1461
|
this.name = 'performance';
|
|
1462
|
+
this.boundLoadHandler = null;
|
|
1463
|
+
this.observers = [];
|
|
1464
|
+
this.boundClsVisibilityHandler = null;
|
|
1328
1465
|
}
|
|
1329
1466
|
init(tracker) {
|
|
1330
1467
|
super.init(tracker);
|
|
1331
1468
|
if (typeof window !== 'undefined') {
|
|
1332
1469
|
// Track performance after page load
|
|
1333
|
-
|
|
1470
|
+
this.boundLoadHandler = () => {
|
|
1334
1471
|
// Delay to ensure all metrics are available
|
|
1335
1472
|
setTimeout(() => this.trackPerformance(), 100);
|
|
1336
|
-
}
|
|
1473
|
+
};
|
|
1474
|
+
window.addEventListener('load', this.boundLoadHandler);
|
|
1337
1475
|
}
|
|
1338
1476
|
}
|
|
1477
|
+
destroy() {
|
|
1478
|
+
if (this.boundLoadHandler && typeof window !== 'undefined') {
|
|
1479
|
+
window.removeEventListener('load', this.boundLoadHandler);
|
|
1480
|
+
}
|
|
1481
|
+
for (const observer of this.observers) {
|
|
1482
|
+
observer.disconnect();
|
|
1483
|
+
}
|
|
1484
|
+
this.observers = [];
|
|
1485
|
+
if (this.boundClsVisibilityHandler && typeof window !== 'undefined') {
|
|
1486
|
+
window.removeEventListener('visibilitychange', this.boundClsVisibilityHandler);
|
|
1487
|
+
}
|
|
1488
|
+
super.destroy();
|
|
1489
|
+
}
|
|
1339
1490
|
trackPerformance() {
|
|
1340
1491
|
if (typeof performance === 'undefined')
|
|
1341
1492
|
return;
|
|
@@ -1392,6 +1543,7 @@ class PerformancePlugin extends BasePlugin {
|
|
|
1392
1543
|
}
|
|
1393
1544
|
});
|
|
1394
1545
|
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
|
|
1546
|
+
this.observers.push(lcpObserver);
|
|
1395
1547
|
}
|
|
1396
1548
|
catch {
|
|
1397
1549
|
// LCP not supported
|
|
@@ -1409,6 +1561,7 @@ class PerformancePlugin extends BasePlugin {
|
|
|
1409
1561
|
}
|
|
1410
1562
|
});
|
|
1411
1563
|
fidObserver.observe({ type: 'first-input', buffered: true });
|
|
1564
|
+
this.observers.push(fidObserver);
|
|
1412
1565
|
}
|
|
1413
1566
|
catch {
|
|
1414
1567
|
// FID not supported
|
|
@@ -1425,15 +1578,17 @@ class PerformancePlugin extends BasePlugin {
|
|
|
1425
1578
|
});
|
|
1426
1579
|
});
|
|
1427
1580
|
clsObserver.observe({ type: 'layout-shift', buffered: true });
|
|
1581
|
+
this.observers.push(clsObserver);
|
|
1428
1582
|
// Report CLS after page is hidden
|
|
1429
|
-
|
|
1583
|
+
this.boundClsVisibilityHandler = () => {
|
|
1430
1584
|
if (document.visibilityState === 'hidden' && clsValue > 0) {
|
|
1431
1585
|
this.track('performance', 'Web Vital - CLS', {
|
|
1432
1586
|
metric: 'CLS',
|
|
1433
1587
|
value: Math.round(clsValue * 1000) / 1000,
|
|
1434
1588
|
});
|
|
1435
1589
|
}
|
|
1436
|
-
}
|
|
1590
|
+
};
|
|
1591
|
+
window.addEventListener('visibilitychange', this.boundClsVisibilityHandler, { once: true });
|
|
1437
1592
|
}
|
|
1438
1593
|
catch {
|
|
1439
1594
|
// CLS not supported
|
|
@@ -1744,7 +1899,7 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1744
1899
|
label.appendChild(requiredMark);
|
|
1745
1900
|
}
|
|
1746
1901
|
fieldWrapper.appendChild(label);
|
|
1747
|
-
// Input/Textarea
|
|
1902
|
+
// Input/Textarea/Select
|
|
1748
1903
|
if (field.type === 'textarea') {
|
|
1749
1904
|
const textarea = document.createElement('textarea');
|
|
1750
1905
|
textarea.name = field.name;
|
|
@@ -1755,6 +1910,38 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1755
1910
|
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;';
|
|
1756
1911
|
fieldWrapper.appendChild(textarea);
|
|
1757
1912
|
}
|
|
1913
|
+
else if (field.type === 'select') {
|
|
1914
|
+
const select = document.createElement('select');
|
|
1915
|
+
select.name = field.name;
|
|
1916
|
+
if (field.required)
|
|
1917
|
+
select.required = true;
|
|
1918
|
+
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;';
|
|
1919
|
+
// Add placeholder option
|
|
1920
|
+
if (field.placeholder) {
|
|
1921
|
+
const placeholderOption = document.createElement('option');
|
|
1922
|
+
placeholderOption.value = '';
|
|
1923
|
+
placeholderOption.textContent = field.placeholder;
|
|
1924
|
+
placeholderOption.disabled = true;
|
|
1925
|
+
placeholderOption.selected = true;
|
|
1926
|
+
select.appendChild(placeholderOption);
|
|
1927
|
+
}
|
|
1928
|
+
// Add options from field.options array if provided
|
|
1929
|
+
if (field.options && Array.isArray(field.options)) {
|
|
1930
|
+
field.options.forEach((opt) => {
|
|
1931
|
+
const option = document.createElement('option');
|
|
1932
|
+
if (typeof opt === 'string') {
|
|
1933
|
+
option.value = opt;
|
|
1934
|
+
option.textContent = opt;
|
|
1935
|
+
}
|
|
1936
|
+
else {
|
|
1937
|
+
option.value = opt.value;
|
|
1938
|
+
option.textContent = opt.label;
|
|
1939
|
+
}
|
|
1940
|
+
select.appendChild(option);
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
fieldWrapper.appendChild(select);
|
|
1944
|
+
}
|
|
1758
1945
|
else {
|
|
1759
1946
|
const input = document.createElement('input');
|
|
1760
1947
|
input.type = field.type;
|
|
@@ -1788,96 +1975,6 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1788
1975
|
formElement.appendChild(submitBtn);
|
|
1789
1976
|
container.appendChild(formElement);
|
|
1790
1977
|
}
|
|
1791
|
-
buildFormHTML(form) {
|
|
1792
|
-
const style = form.style || {};
|
|
1793
|
-
const primaryColor = style.primaryColor || '#10B981';
|
|
1794
|
-
const textColor = style.textColor || '#18181B';
|
|
1795
|
-
let fieldsHTML = form.fields.map(field => {
|
|
1796
|
-
const requiredMark = field.required ? '<span style="color: #EF4444;">*</span>' : '';
|
|
1797
|
-
if (field.type === 'textarea') {
|
|
1798
|
-
return `
|
|
1799
|
-
<div style="margin-bottom: 12px;">
|
|
1800
|
-
<label style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${textColor};">
|
|
1801
|
-
${field.label} ${requiredMark}
|
|
1802
|
-
</label>
|
|
1803
|
-
<textarea
|
|
1804
|
-
name="${field.name}"
|
|
1805
|
-
placeholder="${field.placeholder || ''}"
|
|
1806
|
-
${field.required ? 'required' : ''}
|
|
1807
|
-
style="width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; resize: vertical; min-height: 80px;"
|
|
1808
|
-
></textarea>
|
|
1809
|
-
</div>
|
|
1810
|
-
`;
|
|
1811
|
-
}
|
|
1812
|
-
else if (field.type === 'checkbox') {
|
|
1813
|
-
return `
|
|
1814
|
-
<div style="margin-bottom: 12px;">
|
|
1815
|
-
<label style="display: flex; align-items: center; gap: 8px; font-size: 14px; color: ${textColor}; cursor: pointer;">
|
|
1816
|
-
<input
|
|
1817
|
-
type="checkbox"
|
|
1818
|
-
name="${field.name}"
|
|
1819
|
-
${field.required ? 'required' : ''}
|
|
1820
|
-
style="width: 16px; height: 16px;"
|
|
1821
|
-
/>
|
|
1822
|
-
${field.label} ${requiredMark}
|
|
1823
|
-
</label>
|
|
1824
|
-
</div>
|
|
1825
|
-
`;
|
|
1826
|
-
}
|
|
1827
|
-
else {
|
|
1828
|
-
return `
|
|
1829
|
-
<div style="margin-bottom: 12px;">
|
|
1830
|
-
<label style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${textColor};">
|
|
1831
|
-
${field.label} ${requiredMark}
|
|
1832
|
-
</label>
|
|
1833
|
-
<input
|
|
1834
|
-
type="${field.type}"
|
|
1835
|
-
name="${field.name}"
|
|
1836
|
-
placeholder="${field.placeholder || ''}"
|
|
1837
|
-
${field.required ? 'required' : ''}
|
|
1838
|
-
style="width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box;"
|
|
1839
|
-
/>
|
|
1840
|
-
</div>
|
|
1841
|
-
`;
|
|
1842
|
-
}
|
|
1843
|
-
}).join('');
|
|
1844
|
-
return `
|
|
1845
|
-
<button id="clianta-form-close" style="
|
|
1846
|
-
position: absolute;
|
|
1847
|
-
top: 12px;
|
|
1848
|
-
right: 12px;
|
|
1849
|
-
background: none;
|
|
1850
|
-
border: none;
|
|
1851
|
-
font-size: 20px;
|
|
1852
|
-
cursor: pointer;
|
|
1853
|
-
color: #71717A;
|
|
1854
|
-
padding: 4px;
|
|
1855
|
-
">×</button>
|
|
1856
|
-
<h2 style="font-size: 20px; font-weight: 700; margin-bottom: 8px; color: ${textColor};">
|
|
1857
|
-
${form.headline || 'Stay in touch'}
|
|
1858
|
-
</h2>
|
|
1859
|
-
<p style="font-size: 14px; color: #71717A; margin-bottom: 16px;">
|
|
1860
|
-
${form.subheadline || 'Get the latest updates'}
|
|
1861
|
-
</p>
|
|
1862
|
-
<form id="clianta-form-element">
|
|
1863
|
-
${fieldsHTML}
|
|
1864
|
-
<button type="submit" style="
|
|
1865
|
-
width: 100%;
|
|
1866
|
-
padding: 10px 16px;
|
|
1867
|
-
background: ${primaryColor};
|
|
1868
|
-
color: white;
|
|
1869
|
-
border: none;
|
|
1870
|
-
border-radius: 6px;
|
|
1871
|
-
font-size: 14px;
|
|
1872
|
-
font-weight: 500;
|
|
1873
|
-
cursor: pointer;
|
|
1874
|
-
margin-top: 8px;
|
|
1875
|
-
">
|
|
1876
|
-
${form.submitButtonText || 'Subscribe'}
|
|
1877
|
-
</button>
|
|
1878
|
-
</form>
|
|
1879
|
-
`;
|
|
1880
|
-
}
|
|
1881
1978
|
setupFormEvents(form, overlay, container) {
|
|
1882
1979
|
// Close button
|
|
1883
1980
|
const closeBtn = container.querySelector('#clianta-form-close');
|
|
@@ -2278,6 +2375,8 @@ class Tracker {
|
|
|
2278
2375
|
constructor(workspaceId, userConfig = {}) {
|
|
2279
2376
|
this.plugins = [];
|
|
2280
2377
|
this.isInitialized = false;
|
|
2378
|
+
/** Pending identify retry on next flush */
|
|
2379
|
+
this.pendingIdentify = null;
|
|
2281
2380
|
if (!workspaceId) {
|
|
2282
2381
|
throw new Error('[Clianta] Workspace ID is required');
|
|
2283
2382
|
}
|
|
@@ -2407,7 +2506,7 @@ class Tracker {
|
|
|
2407
2506
|
referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
|
|
2408
2507
|
properties,
|
|
2409
2508
|
device: getDeviceInfo(),
|
|
2410
|
-
|
|
2509
|
+
...getUTMParams(),
|
|
2411
2510
|
timestamp: new Date().toISOString(),
|
|
2412
2511
|
sdkVersion: SDK_VERSION,
|
|
2413
2512
|
};
|
|
@@ -2452,11 +2551,24 @@ class Tracker {
|
|
|
2452
2551
|
});
|
|
2453
2552
|
if (result.success) {
|
|
2454
2553
|
logger.info('Visitor identified successfully');
|
|
2554
|
+
this.pendingIdentify = null;
|
|
2455
2555
|
}
|
|
2456
2556
|
else {
|
|
2457
2557
|
logger.error('Failed to identify visitor:', result.error);
|
|
2558
|
+
// Store for retry on next flush
|
|
2559
|
+
this.pendingIdentify = { email, traits };
|
|
2458
2560
|
}
|
|
2459
2561
|
}
|
|
2562
|
+
/**
|
|
2563
|
+
* Retry pending identify call
|
|
2564
|
+
*/
|
|
2565
|
+
async retryPendingIdentify() {
|
|
2566
|
+
if (!this.pendingIdentify)
|
|
2567
|
+
return;
|
|
2568
|
+
const { email, traits } = this.pendingIdentify;
|
|
2569
|
+
this.pendingIdentify = null;
|
|
2570
|
+
await this.identify(email, traits);
|
|
2571
|
+
}
|
|
2460
2572
|
/**
|
|
2461
2573
|
* Update consent state
|
|
2462
2574
|
*/
|
|
@@ -2504,6 +2616,7 @@ class Tracker {
|
|
|
2504
2616
|
* Force flush event queue
|
|
2505
2617
|
*/
|
|
2506
2618
|
async flush() {
|
|
2619
|
+
await this.retryPendingIdentify();
|
|
2507
2620
|
await this.queue.flush();
|
|
2508
2621
|
}
|
|
2509
2622
|
/**
|
|
@@ -2575,6 +2688,440 @@ class Tracker {
|
|
|
2575
2688
|
}
|
|
2576
2689
|
}
|
|
2577
2690
|
|
|
2691
|
+
/**
|
|
2692
|
+
* Clianta SDK - Event Triggers Manager
|
|
2693
|
+
* Manages event-driven automation and email notifications
|
|
2694
|
+
*/
|
|
2695
|
+
/**
|
|
2696
|
+
* Event Triggers Manager
|
|
2697
|
+
* Handles event-driven automation based on CRM actions
|
|
2698
|
+
*
|
|
2699
|
+
* Similar to:
|
|
2700
|
+
* - Salesforce: Process Builder, Flow Automation
|
|
2701
|
+
* - HubSpot: Workflows, Email Sequences
|
|
2702
|
+
* - Pipedrive: Workflow Automation
|
|
2703
|
+
*/
|
|
2704
|
+
class EventTriggersManager {
|
|
2705
|
+
constructor(apiEndpoint, workspaceId, authToken) {
|
|
2706
|
+
this.triggers = new Map();
|
|
2707
|
+
this.listeners = new Map();
|
|
2708
|
+
this.apiEndpoint = apiEndpoint;
|
|
2709
|
+
this.workspaceId = workspaceId;
|
|
2710
|
+
this.authToken = authToken;
|
|
2711
|
+
}
|
|
2712
|
+
/**
|
|
2713
|
+
* Set authentication token
|
|
2714
|
+
*/
|
|
2715
|
+
setAuthToken(token) {
|
|
2716
|
+
this.authToken = token;
|
|
2717
|
+
}
|
|
2718
|
+
/**
|
|
2719
|
+
* Make authenticated API request
|
|
2720
|
+
*/
|
|
2721
|
+
async request(endpoint, options = {}) {
|
|
2722
|
+
const url = `${this.apiEndpoint}${endpoint}`;
|
|
2723
|
+
const headers = {
|
|
2724
|
+
'Content-Type': 'application/json',
|
|
2725
|
+
...(options.headers || {}),
|
|
2726
|
+
};
|
|
2727
|
+
if (this.authToken) {
|
|
2728
|
+
headers['Authorization'] = `Bearer ${this.authToken}`;
|
|
2729
|
+
}
|
|
2730
|
+
try {
|
|
2731
|
+
const response = await fetch(url, {
|
|
2732
|
+
...options,
|
|
2733
|
+
headers,
|
|
2734
|
+
});
|
|
2735
|
+
const data = await response.json();
|
|
2736
|
+
if (!response.ok) {
|
|
2737
|
+
return {
|
|
2738
|
+
success: false,
|
|
2739
|
+
error: data.message || 'Request failed',
|
|
2740
|
+
status: response.status,
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
return {
|
|
2744
|
+
success: true,
|
|
2745
|
+
data: data.data || data,
|
|
2746
|
+
status: response.status,
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
catch (error) {
|
|
2750
|
+
return {
|
|
2751
|
+
success: false,
|
|
2752
|
+
error: error instanceof Error ? error.message : 'Network error',
|
|
2753
|
+
status: 0,
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
// ============================================
|
|
2758
|
+
// TRIGGER MANAGEMENT
|
|
2759
|
+
// ============================================
|
|
2760
|
+
/**
|
|
2761
|
+
* Get all event triggers
|
|
2762
|
+
*/
|
|
2763
|
+
async getTriggers() {
|
|
2764
|
+
return this.request(`/api/workspaces/${this.workspaceId}/triggers`);
|
|
2765
|
+
}
|
|
2766
|
+
/**
|
|
2767
|
+
* Get a single trigger by ID
|
|
2768
|
+
*/
|
|
2769
|
+
async getTrigger(triggerId) {
|
|
2770
|
+
return this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`);
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* Create a new event trigger
|
|
2774
|
+
*/
|
|
2775
|
+
async createTrigger(trigger) {
|
|
2776
|
+
const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers`, {
|
|
2777
|
+
method: 'POST',
|
|
2778
|
+
body: JSON.stringify(trigger),
|
|
2779
|
+
});
|
|
2780
|
+
// Cache the trigger locally if successful
|
|
2781
|
+
if (result.success && result.data?._id) {
|
|
2782
|
+
this.triggers.set(result.data._id, result.data);
|
|
2783
|
+
}
|
|
2784
|
+
return result;
|
|
2785
|
+
}
|
|
2786
|
+
/**
|
|
2787
|
+
* Update an existing trigger
|
|
2788
|
+
*/
|
|
2789
|
+
async updateTrigger(triggerId, updates) {
|
|
2790
|
+
const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
|
|
2791
|
+
method: 'PUT',
|
|
2792
|
+
body: JSON.stringify(updates),
|
|
2793
|
+
});
|
|
2794
|
+
// Update cache if successful
|
|
2795
|
+
if (result.success && result.data?._id) {
|
|
2796
|
+
this.triggers.set(result.data._id, result.data);
|
|
2797
|
+
}
|
|
2798
|
+
return result;
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* Delete a trigger
|
|
2802
|
+
*/
|
|
2803
|
+
async deleteTrigger(triggerId) {
|
|
2804
|
+
const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
|
|
2805
|
+
method: 'DELETE',
|
|
2806
|
+
});
|
|
2807
|
+
// Remove from cache if successful
|
|
2808
|
+
if (result.success) {
|
|
2809
|
+
this.triggers.delete(triggerId);
|
|
2810
|
+
}
|
|
2811
|
+
return result;
|
|
2812
|
+
}
|
|
2813
|
+
/**
|
|
2814
|
+
* Activate a trigger
|
|
2815
|
+
*/
|
|
2816
|
+
async activateTrigger(triggerId) {
|
|
2817
|
+
return this.updateTrigger(triggerId, { isActive: true });
|
|
2818
|
+
}
|
|
2819
|
+
/**
|
|
2820
|
+
* Deactivate a trigger
|
|
2821
|
+
*/
|
|
2822
|
+
async deactivateTrigger(triggerId) {
|
|
2823
|
+
return this.updateTrigger(triggerId, { isActive: false });
|
|
2824
|
+
}
|
|
2825
|
+
// ============================================
|
|
2826
|
+
// EVENT HANDLING (CLIENT-SIDE)
|
|
2827
|
+
// ============================================
|
|
2828
|
+
/**
|
|
2829
|
+
* Register a local event listener for client-side triggers
|
|
2830
|
+
* This allows immediate client-side reactions to events
|
|
2831
|
+
*/
|
|
2832
|
+
on(eventType, callback) {
|
|
2833
|
+
if (!this.listeners.has(eventType)) {
|
|
2834
|
+
this.listeners.set(eventType, new Set());
|
|
2835
|
+
}
|
|
2836
|
+
this.listeners.get(eventType).add(callback);
|
|
2837
|
+
logger.debug(`Event listener registered: ${eventType}`);
|
|
2838
|
+
}
|
|
2839
|
+
/**
|
|
2840
|
+
* Remove an event listener
|
|
2841
|
+
*/
|
|
2842
|
+
off(eventType, callback) {
|
|
2843
|
+
const listeners = this.listeners.get(eventType);
|
|
2844
|
+
if (listeners) {
|
|
2845
|
+
listeners.delete(callback);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
/**
|
|
2849
|
+
* Emit an event (client-side only)
|
|
2850
|
+
* This will trigger any registered local listeners
|
|
2851
|
+
*/
|
|
2852
|
+
emit(eventType, data) {
|
|
2853
|
+
logger.debug(`Event emitted: ${eventType}`, data);
|
|
2854
|
+
const listeners = this.listeners.get(eventType);
|
|
2855
|
+
if (listeners) {
|
|
2856
|
+
listeners.forEach(callback => {
|
|
2857
|
+
try {
|
|
2858
|
+
callback(data);
|
|
2859
|
+
}
|
|
2860
|
+
catch (error) {
|
|
2861
|
+
logger.error(`Error in event listener for ${eventType}:`, error);
|
|
2862
|
+
}
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
/**
|
|
2867
|
+
* Check if conditions are met for a trigger
|
|
2868
|
+
* Supports dynamic field evaluation including custom fields and nested paths
|
|
2869
|
+
*/
|
|
2870
|
+
evaluateConditions(conditions, data) {
|
|
2871
|
+
if (!conditions || conditions.length === 0) {
|
|
2872
|
+
return true; // No conditions means always fire
|
|
2873
|
+
}
|
|
2874
|
+
return conditions.every(condition => {
|
|
2875
|
+
// Support dot notation for nested fields (e.g., 'customFields.industry')
|
|
2876
|
+
const fieldValue = condition.field.includes('.')
|
|
2877
|
+
? this.getNestedValue(data, condition.field)
|
|
2878
|
+
: data[condition.field];
|
|
2879
|
+
const targetValue = condition.value;
|
|
2880
|
+
switch (condition.operator) {
|
|
2881
|
+
case 'equals':
|
|
2882
|
+
return fieldValue === targetValue;
|
|
2883
|
+
case 'not_equals':
|
|
2884
|
+
return fieldValue !== targetValue;
|
|
2885
|
+
case 'contains':
|
|
2886
|
+
return String(fieldValue).includes(String(targetValue));
|
|
2887
|
+
case 'greater_than':
|
|
2888
|
+
return Number(fieldValue) > Number(targetValue);
|
|
2889
|
+
case 'less_than':
|
|
2890
|
+
return Number(fieldValue) < Number(targetValue);
|
|
2891
|
+
case 'in':
|
|
2892
|
+
return Array.isArray(targetValue) && targetValue.includes(fieldValue);
|
|
2893
|
+
case 'not_in':
|
|
2894
|
+
return Array.isArray(targetValue) && !targetValue.includes(fieldValue);
|
|
2895
|
+
default:
|
|
2896
|
+
return false;
|
|
2897
|
+
}
|
|
2898
|
+
});
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* Execute actions for a triggered event (client-side preview)
|
|
2902
|
+
* Note: Actual execution happens on the backend
|
|
2903
|
+
*/
|
|
2904
|
+
async executeActions(trigger, data) {
|
|
2905
|
+
logger.info(`Executing actions for trigger: ${trigger.name}`);
|
|
2906
|
+
for (const action of trigger.actions) {
|
|
2907
|
+
try {
|
|
2908
|
+
await this.executeAction(action, data);
|
|
2909
|
+
}
|
|
2910
|
+
catch (error) {
|
|
2911
|
+
logger.error(`Failed to execute action:`, error);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
/**
|
|
2916
|
+
* Execute a single action
|
|
2917
|
+
*/
|
|
2918
|
+
async executeAction(action, data) {
|
|
2919
|
+
switch (action.type) {
|
|
2920
|
+
case 'send_email':
|
|
2921
|
+
await this.executeSendEmail(action, data);
|
|
2922
|
+
break;
|
|
2923
|
+
case 'webhook':
|
|
2924
|
+
await this.executeWebhook(action, data);
|
|
2925
|
+
break;
|
|
2926
|
+
case 'create_task':
|
|
2927
|
+
await this.executeCreateTask(action, data);
|
|
2928
|
+
break;
|
|
2929
|
+
case 'update_contact':
|
|
2930
|
+
await this.executeUpdateContact(action, data);
|
|
2931
|
+
break;
|
|
2932
|
+
default:
|
|
2933
|
+
logger.warn(`Unknown action type:`, action);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
/**
|
|
2937
|
+
* Execute send email action (via backend API)
|
|
2938
|
+
*/
|
|
2939
|
+
async executeSendEmail(action, data) {
|
|
2940
|
+
logger.debug('Sending email:', action);
|
|
2941
|
+
const payload = {
|
|
2942
|
+
to: this.replaceVariables(action.to, data),
|
|
2943
|
+
subject: action.subject ? this.replaceVariables(action.subject, data) : undefined,
|
|
2944
|
+
body: action.body ? this.replaceVariables(action.body, data) : undefined,
|
|
2945
|
+
templateId: action.templateId,
|
|
2946
|
+
cc: action.cc,
|
|
2947
|
+
bcc: action.bcc,
|
|
2948
|
+
from: action.from,
|
|
2949
|
+
delayMinutes: action.delayMinutes,
|
|
2950
|
+
};
|
|
2951
|
+
await this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
|
|
2952
|
+
method: 'POST',
|
|
2953
|
+
body: JSON.stringify(payload),
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* Execute webhook action
|
|
2958
|
+
*/
|
|
2959
|
+
async executeWebhook(action, data) {
|
|
2960
|
+
logger.debug('Calling webhook:', action.url);
|
|
2961
|
+
const body = action.body ? this.replaceVariables(action.body, data) : JSON.stringify(data);
|
|
2962
|
+
await fetch(action.url, {
|
|
2963
|
+
method: action.method,
|
|
2964
|
+
headers: {
|
|
2965
|
+
'Content-Type': 'application/json',
|
|
2966
|
+
...action.headers,
|
|
2967
|
+
},
|
|
2968
|
+
body,
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
/**
|
|
2972
|
+
* Execute create task action
|
|
2973
|
+
*/
|
|
2974
|
+
async executeCreateTask(action, data) {
|
|
2975
|
+
logger.debug('Creating task:', action.title);
|
|
2976
|
+
const dueDate = action.dueDays
|
|
2977
|
+
? new Date(Date.now() + action.dueDays * 24 * 60 * 60 * 1000).toISOString()
|
|
2978
|
+
: undefined;
|
|
2979
|
+
await this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
|
|
2980
|
+
method: 'POST',
|
|
2981
|
+
body: JSON.stringify({
|
|
2982
|
+
title: this.replaceVariables(action.title, data),
|
|
2983
|
+
description: action.description ? this.replaceVariables(action.description, data) : undefined,
|
|
2984
|
+
priority: action.priority,
|
|
2985
|
+
dueDate,
|
|
2986
|
+
assignedTo: action.assignedTo,
|
|
2987
|
+
relatedContactId: typeof data.contactId === 'string' ? data.contactId : undefined,
|
|
2988
|
+
}),
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
/**
|
|
2992
|
+
* Execute update contact action
|
|
2993
|
+
*/
|
|
2994
|
+
async executeUpdateContact(action, data) {
|
|
2995
|
+
const contactId = data.contactId || data._id;
|
|
2996
|
+
if (!contactId) {
|
|
2997
|
+
logger.warn('Cannot update contact: no contactId in data');
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
logger.debug('Updating contact:', contactId);
|
|
3001
|
+
await this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
|
|
3002
|
+
method: 'PUT',
|
|
3003
|
+
body: JSON.stringify(action.updates),
|
|
3004
|
+
});
|
|
3005
|
+
}
|
|
3006
|
+
/**
|
|
3007
|
+
* Replace variables in a string template
|
|
3008
|
+
* Supports syntax like {{contact.email}}, {{opportunity.value}}
|
|
3009
|
+
*/
|
|
3010
|
+
replaceVariables(template, data) {
|
|
3011
|
+
return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
|
|
3012
|
+
const value = this.getNestedValue(data, path.trim());
|
|
3013
|
+
return value !== undefined ? String(value) : match;
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
/**
|
|
3017
|
+
* Get nested value from object using dot notation
|
|
3018
|
+
* Supports dynamic field access including custom fields
|
|
3019
|
+
*/
|
|
3020
|
+
getNestedValue(obj, path) {
|
|
3021
|
+
return path.split('.').reduce((current, key) => {
|
|
3022
|
+
return current !== null && current !== undefined && typeof current === 'object'
|
|
3023
|
+
? current[key]
|
|
3024
|
+
: undefined;
|
|
3025
|
+
}, obj);
|
|
3026
|
+
}
|
|
3027
|
+
/**
|
|
3028
|
+
* Extract all available field paths from a data object
|
|
3029
|
+
* Useful for dynamic field discovery based on platform-specific attributes
|
|
3030
|
+
* @param obj - The data object to extract fields from
|
|
3031
|
+
* @param prefix - Internal use for nested paths
|
|
3032
|
+
* @param maxDepth - Maximum depth to traverse (default: 3)
|
|
3033
|
+
* @returns Array of field paths (e.g., ['email', 'contact.firstName', 'customFields.industry'])
|
|
3034
|
+
*/
|
|
3035
|
+
extractAvailableFields(obj, prefix = '', maxDepth = 3) {
|
|
3036
|
+
if (maxDepth <= 0)
|
|
3037
|
+
return [];
|
|
3038
|
+
const fields = [];
|
|
3039
|
+
for (const key in obj) {
|
|
3040
|
+
if (!obj.hasOwnProperty(key))
|
|
3041
|
+
continue;
|
|
3042
|
+
const value = obj[key];
|
|
3043
|
+
const fieldPath = prefix ? `${prefix}.${key}` : key;
|
|
3044
|
+
fields.push(fieldPath);
|
|
3045
|
+
// Recursively traverse nested objects
|
|
3046
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
3047
|
+
const nestedFields = this.extractAvailableFields(value, fieldPath, maxDepth - 1);
|
|
3048
|
+
fields.push(...nestedFields);
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
return fields;
|
|
3052
|
+
}
|
|
3053
|
+
/**
|
|
3054
|
+
* Get available fields from sample data
|
|
3055
|
+
* Helps with dynamic field detection for platform-specific attributes
|
|
3056
|
+
* @param sampleData - Sample data object to analyze
|
|
3057
|
+
* @returns Array of available field paths
|
|
3058
|
+
*/
|
|
3059
|
+
getAvailableFields(sampleData) {
|
|
3060
|
+
return this.extractAvailableFields(sampleData);
|
|
3061
|
+
}
|
|
3062
|
+
// ============================================
|
|
3063
|
+
// HELPER METHODS FOR COMMON PATTERNS
|
|
3064
|
+
// ============================================
|
|
3065
|
+
/**
|
|
3066
|
+
* Create a simple email trigger
|
|
3067
|
+
* Helper method for common use case
|
|
3068
|
+
*/
|
|
3069
|
+
async createEmailTrigger(config) {
|
|
3070
|
+
return this.createTrigger({
|
|
3071
|
+
name: config.name,
|
|
3072
|
+
eventType: config.eventType,
|
|
3073
|
+
conditions: config.conditions,
|
|
3074
|
+
actions: [
|
|
3075
|
+
{
|
|
3076
|
+
type: 'send_email',
|
|
3077
|
+
to: config.to,
|
|
3078
|
+
subject: config.subject,
|
|
3079
|
+
body: config.body,
|
|
3080
|
+
},
|
|
3081
|
+
],
|
|
3082
|
+
isActive: true,
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
3085
|
+
/**
|
|
3086
|
+
* Create a task creation trigger
|
|
3087
|
+
*/
|
|
3088
|
+
async createTaskTrigger(config) {
|
|
3089
|
+
return this.createTrigger({
|
|
3090
|
+
name: config.name,
|
|
3091
|
+
eventType: config.eventType,
|
|
3092
|
+
conditions: config.conditions,
|
|
3093
|
+
actions: [
|
|
3094
|
+
{
|
|
3095
|
+
type: 'create_task',
|
|
3096
|
+
title: config.taskTitle,
|
|
3097
|
+
description: config.taskDescription,
|
|
3098
|
+
priority: config.priority,
|
|
3099
|
+
dueDays: config.dueDays,
|
|
3100
|
+
},
|
|
3101
|
+
],
|
|
3102
|
+
isActive: true,
|
|
3103
|
+
});
|
|
3104
|
+
}
|
|
3105
|
+
/**
|
|
3106
|
+
* Create a webhook trigger
|
|
3107
|
+
*/
|
|
3108
|
+
async createWebhookTrigger(config) {
|
|
3109
|
+
return this.createTrigger({
|
|
3110
|
+
name: config.name,
|
|
3111
|
+
eventType: config.eventType,
|
|
3112
|
+
conditions: config.conditions,
|
|
3113
|
+
actions: [
|
|
3114
|
+
{
|
|
3115
|
+
type: 'webhook',
|
|
3116
|
+
url: config.webhookUrl,
|
|
3117
|
+
method: config.method || 'POST',
|
|
3118
|
+
},
|
|
3119
|
+
],
|
|
3120
|
+
isActive: true,
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
|
|
2578
3125
|
/**
|
|
2579
3126
|
* Clianta SDK - CRM API Client
|
|
2580
3127
|
* @see SDK_VERSION in core/config.ts
|
|
@@ -2587,12 +3134,23 @@ class CRMClient {
|
|
|
2587
3134
|
this.apiEndpoint = apiEndpoint;
|
|
2588
3135
|
this.workspaceId = workspaceId;
|
|
2589
3136
|
this.authToken = authToken;
|
|
3137
|
+
this.triggers = new EventTriggersManager(apiEndpoint, workspaceId, authToken);
|
|
2590
3138
|
}
|
|
2591
3139
|
/**
|
|
2592
3140
|
* Set authentication token for API requests
|
|
2593
3141
|
*/
|
|
2594
3142
|
setAuthToken(token) {
|
|
2595
3143
|
this.authToken = token;
|
|
3144
|
+
this.triggers.setAuthToken(token);
|
|
3145
|
+
}
|
|
3146
|
+
/**
|
|
3147
|
+
* Validate required parameter exists
|
|
3148
|
+
* @throws {Error} if value is null/undefined or empty string
|
|
3149
|
+
*/
|
|
3150
|
+
validateRequired(param, value, methodName) {
|
|
3151
|
+
if (value === null || value === undefined || value === '') {
|
|
3152
|
+
throw new Error(`[CRMClient.${methodName}] ${param} is required`);
|
|
3153
|
+
}
|
|
2596
3154
|
}
|
|
2597
3155
|
/**
|
|
2598
3156
|
* Make authenticated API request
|
|
@@ -2657,6 +3215,7 @@ class CRMClient {
|
|
|
2657
3215
|
* Get a single contact by ID
|
|
2658
3216
|
*/
|
|
2659
3217
|
async getContact(contactId) {
|
|
3218
|
+
this.validateRequired('contactId', contactId, 'getContact');
|
|
2660
3219
|
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
|
|
2661
3220
|
}
|
|
2662
3221
|
/**
|
|
@@ -2672,6 +3231,7 @@ class CRMClient {
|
|
|
2672
3231
|
* Update an existing contact
|
|
2673
3232
|
*/
|
|
2674
3233
|
async updateContact(contactId, updates) {
|
|
3234
|
+
this.validateRequired('contactId', contactId, 'updateContact');
|
|
2675
3235
|
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
|
|
2676
3236
|
method: 'PUT',
|
|
2677
3237
|
body: JSON.stringify(updates),
|
|
@@ -2681,6 +3241,7 @@ class CRMClient {
|
|
|
2681
3241
|
* Delete a contact
|
|
2682
3242
|
*/
|
|
2683
3243
|
async deleteContact(contactId) {
|
|
3244
|
+
this.validateRequired('contactId', contactId, 'deleteContact');
|
|
2684
3245
|
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
|
|
2685
3246
|
method: 'DELETE',
|
|
2686
3247
|
});
|
|
@@ -3044,6 +3605,90 @@ class CRMClient {
|
|
|
3044
3605
|
opportunityId: data.opportunityId,
|
|
3045
3606
|
});
|
|
3046
3607
|
}
|
|
3608
|
+
// ============================================
|
|
3609
|
+
// EMAIL TEMPLATES API
|
|
3610
|
+
// ============================================
|
|
3611
|
+
/**
|
|
3612
|
+
* Get all email templates
|
|
3613
|
+
*/
|
|
3614
|
+
async getEmailTemplates(params) {
|
|
3615
|
+
const queryParams = new URLSearchParams();
|
|
3616
|
+
if (params?.page)
|
|
3617
|
+
queryParams.set('page', params.page.toString());
|
|
3618
|
+
if (params?.limit)
|
|
3619
|
+
queryParams.set('limit', params.limit.toString());
|
|
3620
|
+
const query = queryParams.toString();
|
|
3621
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/email-templates${query ? `?${query}` : ''}`;
|
|
3622
|
+
return this.request(endpoint);
|
|
3623
|
+
}
|
|
3624
|
+
/**
|
|
3625
|
+
* Get a single email template by ID
|
|
3626
|
+
*/
|
|
3627
|
+
async getEmailTemplate(templateId) {
|
|
3628
|
+
return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`);
|
|
3629
|
+
}
|
|
3630
|
+
/**
|
|
3631
|
+
* Create a new email template
|
|
3632
|
+
*/
|
|
3633
|
+
async createEmailTemplate(template) {
|
|
3634
|
+
return this.request(`/api/workspaces/${this.workspaceId}/email-templates`, {
|
|
3635
|
+
method: 'POST',
|
|
3636
|
+
body: JSON.stringify(template),
|
|
3637
|
+
});
|
|
3638
|
+
}
|
|
3639
|
+
/**
|
|
3640
|
+
* Update an email template
|
|
3641
|
+
*/
|
|
3642
|
+
async updateEmailTemplate(templateId, updates) {
|
|
3643
|
+
return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
|
|
3644
|
+
method: 'PUT',
|
|
3645
|
+
body: JSON.stringify(updates),
|
|
3646
|
+
});
|
|
3647
|
+
}
|
|
3648
|
+
/**
|
|
3649
|
+
* Delete an email template
|
|
3650
|
+
*/
|
|
3651
|
+
async deleteEmailTemplate(templateId) {
|
|
3652
|
+
return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
|
|
3653
|
+
method: 'DELETE',
|
|
3654
|
+
});
|
|
3655
|
+
}
|
|
3656
|
+
/**
|
|
3657
|
+
* Send an email using a template
|
|
3658
|
+
*/
|
|
3659
|
+
async sendEmail(data) {
|
|
3660
|
+
return this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
|
|
3661
|
+
method: 'POST',
|
|
3662
|
+
body: JSON.stringify(data),
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
// ============================================
|
|
3666
|
+
// EVENT TRIGGERS API (delegated to triggers manager)
|
|
3667
|
+
// ============================================
|
|
3668
|
+
/**
|
|
3669
|
+
* Get all event triggers
|
|
3670
|
+
*/
|
|
3671
|
+
async getEventTriggers() {
|
|
3672
|
+
return this.triggers.getTriggers();
|
|
3673
|
+
}
|
|
3674
|
+
/**
|
|
3675
|
+
* Create a new event trigger
|
|
3676
|
+
*/
|
|
3677
|
+
async createEventTrigger(trigger) {
|
|
3678
|
+
return this.triggers.createTrigger(trigger);
|
|
3679
|
+
}
|
|
3680
|
+
/**
|
|
3681
|
+
* Update an event trigger
|
|
3682
|
+
*/
|
|
3683
|
+
async updateEventTrigger(triggerId, updates) {
|
|
3684
|
+
return this.triggers.updateTrigger(triggerId, updates);
|
|
3685
|
+
}
|
|
3686
|
+
/**
|
|
3687
|
+
* Delete an event trigger
|
|
3688
|
+
*/
|
|
3689
|
+
async deleteEventTrigger(triggerId) {
|
|
3690
|
+
return this.triggers.deleteTrigger(triggerId);
|
|
3691
|
+
}
|
|
3047
3692
|
}
|
|
3048
3693
|
|
|
3049
3694
|
/**
|
|
@@ -3098,11 +3743,13 @@ if (typeof window !== 'undefined') {
|
|
|
3098
3743
|
Tracker,
|
|
3099
3744
|
CRMClient,
|
|
3100
3745
|
ConsentManager,
|
|
3746
|
+
EventTriggersManager,
|
|
3101
3747
|
};
|
|
3102
3748
|
}
|
|
3103
3749
|
|
|
3104
3750
|
exports.CRMClient = CRMClient;
|
|
3105
3751
|
exports.ConsentManager = ConsentManager;
|
|
3752
|
+
exports.EventTriggersManager = EventTriggersManager;
|
|
3106
3753
|
exports.SDK_VERSION = SDK_VERSION;
|
|
3107
3754
|
exports.Tracker = Tracker;
|
|
3108
3755
|
exports.clianta = clianta;
|