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