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