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