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