@active-reach/web-sdk 1.9.0 → 1.11.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/dist/aegis.min.js +1 -1
- package/dist/aegis.min.js.map +1 -1
- package/dist/{analytics-Cc4-QQBf.mjs → analytics-6PR9ERDS.mjs} +190 -4
- package/dist/analytics-6PR9ERDS.mjs.map +1 -0
- package/dist/core/analytics.d.ts +1 -0
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/governance/index.d.ts +2 -0
- package/dist/governance/index.d.ts.map +1 -1
- package/dist/governance/trait-governor.d.ts +63 -0
- package/dist/governance/trait-governor.d.ts.map +1 -0
- package/dist/inapp/AegisInAppManager.d.ts +69 -0
- package/dist/inapp/AegisInAppManager.d.ts.map +1 -1
- package/dist/index.js +566 -60
- package/dist/index.js.map +1 -1
- package/dist/react.js +1 -1
- package/dist/state/intent_ledger.d.ts +136 -0
- package/dist/state/intent_ledger.d.ts.map +1 -0
- package/dist/triggers/IntentRuleEvaluator.d.ts +167 -0
- package/dist/triggers/IntentRuleEvaluator.d.ts.map +1 -0
- package/dist/triggers/IntentSnapshotCollector.d.ts +83 -0
- package/dist/triggers/IntentSnapshotCollector.d.ts.map +1 -0
- package/dist/triggers/SelectorBinder.d.ts +118 -0
- package/dist/triggers/SelectorBinder.d.ts.map +1 -0
- package/dist/triggers/TriggerEngine.d.ts +177 -0
- package/dist/triggers/TriggerEngine.d.ts.map +1 -1
- package/dist/triggers/index.d.ts +7 -1
- package/dist/triggers/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/analytics-Cc4-QQBf.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as Storage, l as logger, A as Aegis } from "./analytics-
|
|
2
|
-
import { B, E, N, R, m } from "./analytics-
|
|
1
|
+
import { S as Storage, l as logger, A as Aegis } from "./analytics-6PR9ERDS.mjs";
|
|
2
|
+
import { B, E, N, R, m } from "./analytics-6PR9ERDS.mjs";
|
|
3
3
|
import { AegisWebPush } from "./push/AegisWebPush.js";
|
|
4
4
|
function debounce(func, wait) {
|
|
5
5
|
let timeoutId = null;
|
|
@@ -755,6 +755,7 @@ const _AegisInAppManager = class _AegisInAppManager {
|
|
|
755
755
|
this.ready = new Promise((resolve) => {
|
|
756
756
|
this.readyResolve = resolve;
|
|
757
757
|
});
|
|
758
|
+
this.filledSlots = /* @__PURE__ */ new WeakSet();
|
|
758
759
|
this.writeKey = config.writeKey;
|
|
759
760
|
this.apiHost = config.apiHost || "https://api.aegis.ai";
|
|
760
761
|
this.userId = config.userId ?? readAnonIdFromStorage$1();
|
|
@@ -1099,6 +1100,8 @@ const _AegisInAppManager = class _AegisInAppManager {
|
|
|
1099
1100
|
this.emit("campaigns-loaded", this.campaigns);
|
|
1100
1101
|
this.readyResolve();
|
|
1101
1102
|
this.tryDisplayNextCampaign();
|
|
1103
|
+
this.renderIntoSlots();
|
|
1104
|
+
this.renderTokenAnchors();
|
|
1102
1105
|
} catch (error) {
|
|
1103
1106
|
this.emitError(error, { stage: "refresh-campaigns" });
|
|
1104
1107
|
this.log(`Error refreshing campaigns: ${error}`, "error");
|
|
@@ -1147,6 +1150,344 @@ const _AegisInAppManager = class _AegisInAppManager {
|
|
|
1147
1150
|
this.displayCampaign(campaign);
|
|
1148
1151
|
}
|
|
1149
1152
|
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Scan the page for `[data-aegis-slot]` anchors and render any
|
|
1155
|
+
* campaign whose `widget_category` matches the slot key AND whose
|
|
1156
|
+
* `delivery_modes` includes `'embedded_card'`.
|
|
1157
|
+
*
|
|
1158
|
+
* Runs after every refresh-campaigns success — the WeakSet guard
|
|
1159
|
+
* keeps it idempotent across re-fetches. The slot key is the campaign's
|
|
1160
|
+
* `widget_category` (the wide grouping discriminator from F3), so a
|
|
1161
|
+
* `<div data-aegis-slot="feedback">` will accept the first active
|
|
1162
|
+
* campaign with `widget_category='feedback'` + embedded_card mode.
|
|
1163
|
+
*/
|
|
1164
|
+
renderIntoSlots() {
|
|
1165
|
+
if (typeof document === "undefined") return;
|
|
1166
|
+
const slots = document.querySelectorAll("[data-aegis-slot]");
|
|
1167
|
+
if (slots.length === 0) return;
|
|
1168
|
+
const eligibleByCategory = /* @__PURE__ */ new Map();
|
|
1169
|
+
for (const c of this.campaigns) {
|
|
1170
|
+
const modes = c.delivery_modes;
|
|
1171
|
+
const category = c.widget_category;
|
|
1172
|
+
if (!modes || !modes.includes("embedded_card")) continue;
|
|
1173
|
+
if (!category) continue;
|
|
1174
|
+
if (!eligibleByCategory.has(category)) {
|
|
1175
|
+
eligibleByCategory.set(category, c);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if (eligibleByCategory.size === 0) return;
|
|
1179
|
+
slots.forEach((slot) => {
|
|
1180
|
+
if (this.filledSlots.has(slot)) return;
|
|
1181
|
+
const key = slot.getAttribute("data-aegis-slot");
|
|
1182
|
+
if (!key) return;
|
|
1183
|
+
const campaign = eligibleByCategory.get(key);
|
|
1184
|
+
if (!campaign) return;
|
|
1185
|
+
this.renderCampaignIntoSlot(campaign, slot);
|
|
1186
|
+
this.filledSlots.add(slot);
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* U7 — render server-injected token-bound campaigns.
|
|
1191
|
+
*
|
|
1192
|
+
* The standalone-page route at `apps/cashier-portal/src/app/s/[slug]/
|
|
1193
|
+
* [workspace]/[page_key]/page.tsx` (and the `/c/[token]` Aegis-hosted
|
|
1194
|
+
* fallback) verify the URL token server-side via the F16 internal
|
|
1195
|
+
* endpoint, then embed the resolved campaign payload as JSON inside
|
|
1196
|
+
* a `<div data-aegis-token-data="...">` anchor. This SDK method
|
|
1197
|
+
* scans for those anchors, parses the payload, and dispatches to
|
|
1198
|
+
* the SAME `renderCampaignIntoSlot` used by the [data-aegis-slot]
|
|
1199
|
+
* path — so widget rendering code is single-sourced.
|
|
1200
|
+
*
|
|
1201
|
+
* Why server-injected (not client-fetched): keeps INTERNAL_API_SECRET
|
|
1202
|
+
* server-side, avoids an extra browser round-trip for verify, gives
|
|
1203
|
+
* Next.js SSR-friendly initial HTML for SEO/no-JS users (rendered
|
|
1204
|
+
* via the page handler's React tree alongside the SDK anchor).
|
|
1205
|
+
*
|
|
1206
|
+
* Idempotency: filledSlots is the same WeakSet used by
|
|
1207
|
+
* renderIntoSlots — re-running on SSE/poll refresh is a no-op for
|
|
1208
|
+
* already-filled anchors.
|
|
1209
|
+
*/
|
|
1210
|
+
renderTokenAnchors() {
|
|
1211
|
+
if (typeof document === "undefined") return;
|
|
1212
|
+
const anchors = document.querySelectorAll("[data-aegis-token-data]");
|
|
1213
|
+
if (anchors.length === 0) return;
|
|
1214
|
+
anchors.forEach((anchor) => {
|
|
1215
|
+
if (this.filledSlots.has(anchor)) return;
|
|
1216
|
+
const raw = anchor.getAttribute("data-aegis-token-data");
|
|
1217
|
+
if (!raw) return;
|
|
1218
|
+
let payload = null;
|
|
1219
|
+
try {
|
|
1220
|
+
payload = JSON.parse(raw);
|
|
1221
|
+
} catch (e) {
|
|
1222
|
+
this.log(
|
|
1223
|
+
`data-aegis-token-data: invalid JSON, skipping anchor (${e})`,
|
|
1224
|
+
"warn"
|
|
1225
|
+
);
|
|
1226
|
+
this.filledSlots.add(anchor);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (!payload || !payload.campaign || !payload.campaign.id) {
|
|
1230
|
+
this.log(
|
|
1231
|
+
"data-aegis-token-data: missing `campaign` in payload, skipping",
|
|
1232
|
+
"warn"
|
|
1233
|
+
);
|
|
1234
|
+
this.filledSlots.add(anchor);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
this.renderCampaignIntoSlot(
|
|
1238
|
+
payload.campaign,
|
|
1239
|
+
anchor,
|
|
1240
|
+
{ submitUrl: payload.submit_url }
|
|
1241
|
+
);
|
|
1242
|
+
this.filledSlots.add(anchor);
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Slot-mode counterpart to `displayCampaign`. Reuses the existing
|
|
1247
|
+
* lifecycle hooks (`campaign-will-show`, `displayedCampaigns`,
|
|
1248
|
+
* `campaign-shown`, impression tracking) so analytics + frequency
|
|
1249
|
+
* caps work the same way as overlay renders.
|
|
1250
|
+
*
|
|
1251
|
+
* Only the sub_types that make UX sense embedded are handled; the
|
|
1252
|
+
* rest (modal, full_screen, half_interstitial, alert, pip) silently
|
|
1253
|
+
* skip — those types only make sense as fullscreen overlays.
|
|
1254
|
+
*/
|
|
1255
|
+
renderCampaignIntoSlot(campaign, target, options) {
|
|
1256
|
+
const proceed = this.emit("campaign-will-show", campaign);
|
|
1257
|
+
if (!proceed) {
|
|
1258
|
+
this.log(
|
|
1259
|
+
`slot campaign ${campaign.id} suppressed by campaign-will-show handler`
|
|
1260
|
+
);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
this.displayedCampaigns.add(campaign.id);
|
|
1264
|
+
this.addAnimationStyles();
|
|
1265
|
+
const ic = campaign.interactive_config || {};
|
|
1266
|
+
const bg = this.sanitizeColor(campaign.background_color || "#4169e1");
|
|
1267
|
+
const text = this.sanitizeColor(campaign.text_color || "#ffffff");
|
|
1268
|
+
const submitUrl = options == null ? void 0 : options.submitUrl;
|
|
1269
|
+
let rendered = false;
|
|
1270
|
+
switch (campaign.sub_type) {
|
|
1271
|
+
case "star_rating":
|
|
1272
|
+
rendered = this.renderStarRatingSlot(
|
|
1273
|
+
campaign,
|
|
1274
|
+
ic,
|
|
1275
|
+
bg,
|
|
1276
|
+
text,
|
|
1277
|
+
target,
|
|
1278
|
+
submitUrl
|
|
1279
|
+
);
|
|
1280
|
+
break;
|
|
1281
|
+
case "nps_survey":
|
|
1282
|
+
rendered = this.renderNPSSurveySlot(
|
|
1283
|
+
campaign,
|
|
1284
|
+
ic,
|
|
1285
|
+
bg,
|
|
1286
|
+
text,
|
|
1287
|
+
target,
|
|
1288
|
+
submitUrl
|
|
1289
|
+
);
|
|
1290
|
+
break;
|
|
1291
|
+
// Future sub_types (quick_poll, countdown_offer, quiz, sticky_bar,
|
|
1292
|
+
// progress_bar, carousel_cards, product_recommendation) get added
|
|
1293
|
+
// here as merchants need them embedded. Until then, fall through
|
|
1294
|
+
// to the warn below — campaigns will still render as overlays via
|
|
1295
|
+
// the parallel `tryDisplayNextCampaign` path.
|
|
1296
|
+
default:
|
|
1297
|
+
this.log(
|
|
1298
|
+
`slot mode not yet supported for sub_type: ${campaign.sub_type ?? campaign.type}`,
|
|
1299
|
+
"warn"
|
|
1300
|
+
);
|
|
1301
|
+
this.displayedCampaigns.delete(campaign.id);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
if (!rendered) return;
|
|
1305
|
+
this.trackEvent(campaign.id, "impression");
|
|
1306
|
+
this.emit("campaign-shown", campaign);
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Slot-mode wrappers. They build a small card container (no overlay
|
|
1310
|
+
* positioning, no close button) and append the SHARED body element
|
|
1311
|
+
* the overlay path also uses. Body construction lives in the
|
|
1312
|
+
* `_buildXxxBody` helpers below so visual output stays identical
|
|
1313
|
+
* between overlay and slot modes — adding a renderer feature in
|
|
1314
|
+
* one place updates both.
|
|
1315
|
+
*/
|
|
1316
|
+
renderStarRatingSlot(campaign, ic, bg, text, target, submitUrl) {
|
|
1317
|
+
const card = this._wrapInSlotCard(
|
|
1318
|
+
"aegis-in-app-rating-card",
|
|
1319
|
+
campaign.id,
|
|
1320
|
+
bg,
|
|
1321
|
+
text
|
|
1322
|
+
);
|
|
1323
|
+
card.appendChild(
|
|
1324
|
+
this._buildStarRatingBody(campaign, ic, text, "slot", submitUrl)
|
|
1325
|
+
);
|
|
1326
|
+
target.appendChild(card);
|
|
1327
|
+
return true;
|
|
1328
|
+
}
|
|
1329
|
+
renderNPSSurveySlot(campaign, ic, bg, text, target, submitUrl) {
|
|
1330
|
+
const card = this._wrapInSlotCard(
|
|
1331
|
+
"aegis-in-app-nps-card",
|
|
1332
|
+
campaign.id,
|
|
1333
|
+
bg,
|
|
1334
|
+
text
|
|
1335
|
+
);
|
|
1336
|
+
card.appendChild(
|
|
1337
|
+
this._buildNPSSurveyBody(campaign, ic, text, "slot", submitUrl)
|
|
1338
|
+
);
|
|
1339
|
+
target.appendChild(card);
|
|
1340
|
+
return true;
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Token-bound submission helper. POSTs the structured response to
|
|
1344
|
+
* the per-token submit URL the page handler embedded. Best-effort
|
|
1345
|
+
* — failure is logged but doesn't throw (the customer already
|
|
1346
|
+
* clicked; we can't undo their interaction).
|
|
1347
|
+
*/
|
|
1348
|
+
_submitTokenResponse(submitUrl, payload) {
|
|
1349
|
+
if (!submitUrl) return;
|
|
1350
|
+
fetch(submitUrl, {
|
|
1351
|
+
method: "POST",
|
|
1352
|
+
headers: { "Content-Type": "application/json" },
|
|
1353
|
+
body: JSON.stringify({ response: payload }),
|
|
1354
|
+
keepalive: true
|
|
1355
|
+
}).catch((err) => {
|
|
1356
|
+
this.log(`token submit failed: ${err}`, "warn");
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
// --- Shared body builders (used by both overlay + slot paths) ----------
|
|
1360
|
+
//
|
|
1361
|
+
// Sizing: `'overlay'` matches the legacy fullscreen-modal scale (24px
|
|
1362
|
+
// padding, 18px title, 32px stars). `'slot'` is the embedded card
|
|
1363
|
+
// scale (20px padding, 16px title, 28px stars) — proportional, doesn't
|
|
1364
|
+
// dwarf merchant page chrome. Adding a new variant means a new tuple
|
|
1365
|
+
// here, not a new renderer.
|
|
1366
|
+
_wrapInSlotCard(className, campaignId, bg, text) {
|
|
1367
|
+
const card = document.createElement("div");
|
|
1368
|
+
card.className = className;
|
|
1369
|
+
card.setAttribute("data-campaign-id", campaignId);
|
|
1370
|
+
card.style.cssText = `
|
|
1371
|
+
width: 100%; border-radius: 12px; overflow: hidden;
|
|
1372
|
+
background: ${bg}; color: ${text};
|
|
1373
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
|
1374
|
+
`;
|
|
1375
|
+
return card;
|
|
1376
|
+
}
|
|
1377
|
+
_buildStarRatingBody(campaign, ic, text, variant, submitUrl) {
|
|
1378
|
+
const isOverlay = variant === "overlay";
|
|
1379
|
+
const padding = isOverlay ? "24px" : "20px";
|
|
1380
|
+
const titleSize = isOverlay ? "18px" : "16px";
|
|
1381
|
+
const titleWeight = isOverlay ? "700" : "600";
|
|
1382
|
+
const titleMargin = isOverlay ? "16px" : "12px";
|
|
1383
|
+
const starSize = isOverlay ? "32px" : "28px";
|
|
1384
|
+
const starsMarginBottom = isOverlay ? "16px" : "0";
|
|
1385
|
+
const hoverScale = isOverlay ? "1.2" : "1.15";
|
|
1386
|
+
const body = document.createElement("div");
|
|
1387
|
+
body.style.cssText = `padding: ${padding}; text-align: center;`;
|
|
1388
|
+
const title = document.createElement("div");
|
|
1389
|
+
title.style.cssText = `font-size: ${titleSize}; font-weight: ${titleWeight}; margin-bottom: ${titleMargin};`;
|
|
1390
|
+
title.textContent = campaign.title || "Rate your experience";
|
|
1391
|
+
body.appendChild(title);
|
|
1392
|
+
const stars = document.createElement("div");
|
|
1393
|
+
stars.style.cssText = `display: flex; gap: 8px; justify-content: center;` + (starsMarginBottom !== "0" ? ` margin-bottom: ${starsMarginBottom};` : "");
|
|
1394
|
+
const maxStars = ic.rating_scale || 5;
|
|
1395
|
+
for (let i = 1; i <= maxStars; i++) {
|
|
1396
|
+
const star = document.createElement("span");
|
|
1397
|
+
star.style.cssText = `font-size: ${starSize}; cursor: pointer; transition: transform 0.1s; user-select: none;` + (text ? ` color: ${text};` : "");
|
|
1398
|
+
star.textContent = "☆";
|
|
1399
|
+
const value = i;
|
|
1400
|
+
star.addEventListener("click", () => {
|
|
1401
|
+
this.trackEvent(campaign.id, "clicked");
|
|
1402
|
+
if (submitUrl) {
|
|
1403
|
+
this._submitTokenResponse(submitUrl, {
|
|
1404
|
+
sub_type: "star_rating",
|
|
1405
|
+
value
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
star.addEventListener("mouseenter", () => {
|
|
1410
|
+
star.style.transform = `scale(${hoverScale})`;
|
|
1411
|
+
});
|
|
1412
|
+
star.addEventListener("mouseleave", () => {
|
|
1413
|
+
star.style.transform = "scale(1)";
|
|
1414
|
+
});
|
|
1415
|
+
stars.appendChild(star);
|
|
1416
|
+
}
|
|
1417
|
+
body.appendChild(stars);
|
|
1418
|
+
return body;
|
|
1419
|
+
}
|
|
1420
|
+
_buildNPSSurveyBody(campaign, ic, text, variant, submitUrl) {
|
|
1421
|
+
const isOverlay = variant === "overlay";
|
|
1422
|
+
const padding = isOverlay ? "24px" : "20px";
|
|
1423
|
+
const titleAlign = isOverlay ? "center" : "center";
|
|
1424
|
+
const body = document.createElement("div");
|
|
1425
|
+
body.style.cssText = `padding: ${padding};` + (isOverlay ? " text-align: center;" : "");
|
|
1426
|
+
const title = document.createElement("div");
|
|
1427
|
+
title.style.cssText = `font-size: 16px; font-weight: ${isOverlay ? "700" : "600"}; margin-bottom: ${isOverlay ? "16px" : "12px"}; text-align: ${titleAlign};`;
|
|
1428
|
+
title.textContent = ic.nps_question || campaign.title || "How likely are you to recommend us?";
|
|
1429
|
+
body.appendChild(title);
|
|
1430
|
+
if (isOverlay) {
|
|
1431
|
+
const scale = document.createElement("div");
|
|
1432
|
+
scale.style.cssText = "display: flex; gap: 4px; justify-content: center; flex-wrap: wrap; margin-bottom: 12px;";
|
|
1433
|
+
for (let i = 0; i <= 10; i++) {
|
|
1434
|
+
const btn = document.createElement("span");
|
|
1435
|
+
btn.style.cssText = `
|
|
1436
|
+
width: 28px; height: 28px; border-radius: 6px; display: flex;
|
|
1437
|
+
align-items: center; justify-content: center; font-size: 11px;
|
|
1438
|
+
font-weight: 600; cursor: pointer; background: ${text}33; color: ${text};
|
|
1439
|
+
transition: transform 0.1s;
|
|
1440
|
+
`;
|
|
1441
|
+
btn.textContent = String(i);
|
|
1442
|
+
const value = i;
|
|
1443
|
+
btn.addEventListener("click", () => {
|
|
1444
|
+
this.trackEvent(campaign.id, "clicked");
|
|
1445
|
+
if (submitUrl) {
|
|
1446
|
+
this._submitTokenResponse(submitUrl, {
|
|
1447
|
+
sub_type: "nps_survey",
|
|
1448
|
+
value
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
scale.appendChild(btn);
|
|
1453
|
+
}
|
|
1454
|
+
body.appendChild(scale);
|
|
1455
|
+
const labels = document.createElement("div");
|
|
1456
|
+
labels.style.cssText = "display: flex; justify-content: space-between; font-size: 11px; opacity: 0.6; margin-bottom: 16px;";
|
|
1457
|
+
const notLikely = document.createElement("span");
|
|
1458
|
+
notLikely.textContent = "Not likely";
|
|
1459
|
+
const veryLikely = document.createElement("span");
|
|
1460
|
+
veryLikely.textContent = "Very likely";
|
|
1461
|
+
labels.appendChild(notLikely);
|
|
1462
|
+
labels.appendChild(veryLikely);
|
|
1463
|
+
body.appendChild(labels);
|
|
1464
|
+
return body;
|
|
1465
|
+
}
|
|
1466
|
+
const grid = document.createElement("div");
|
|
1467
|
+
grid.style.cssText = "display: grid; grid-template-columns: repeat(11, 1fr); gap: 4px;";
|
|
1468
|
+
for (let n = 0; n <= 10; n++) {
|
|
1469
|
+
const btn = document.createElement("button");
|
|
1470
|
+
btn.style.cssText = `
|
|
1471
|
+
padding: 8px 0; border-radius: 6px; border: 1px solid ${text}33;
|
|
1472
|
+
background: transparent; color: ${text}; font-size: 13px; font-weight: 600;
|
|
1473
|
+
cursor: pointer; transition: background 0.15s;
|
|
1474
|
+
`;
|
|
1475
|
+
btn.textContent = String(n);
|
|
1476
|
+
const value = n;
|
|
1477
|
+
btn.addEventListener("click", () => {
|
|
1478
|
+
this.trackEvent(campaign.id, "clicked");
|
|
1479
|
+
if (submitUrl) {
|
|
1480
|
+
this._submitTokenResponse(submitUrl, {
|
|
1481
|
+
sub_type: "nps_survey",
|
|
1482
|
+
value
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
});
|
|
1486
|
+
grid.appendChild(btn);
|
|
1487
|
+
}
|
|
1488
|
+
body.appendChild(grid);
|
|
1489
|
+
return body;
|
|
1490
|
+
}
|
|
1150
1491
|
/**
|
|
1151
1492
|
* Evaluate the currently armed campaigns against a client-side event
|
|
1152
1493
|
* and render any that match their `client_trigger`.
|
|
@@ -1338,38 +1679,7 @@ const _AegisInAppManager = class _AegisInAppManager {
|
|
|
1338
1679
|
background: ${bg}; color: ${text}; animation: aegisScaleIn 0.3s ease;
|
|
1339
1680
|
box-shadow: 0 20px 60px rgba(0,0,0,0.15);
|
|
1340
1681
|
`;
|
|
1341
|
-
const body =
|
|
1342
|
-
body.style.cssText = "padding: 24px; text-align: center;";
|
|
1343
|
-
const question = document.createElement("div");
|
|
1344
|
-
question.style.cssText = "font-size: 16px; font-weight: 700; margin-bottom: 16px;";
|
|
1345
|
-
question.textContent = ic.nps_question || "How likely are you to recommend us?";
|
|
1346
|
-
body.appendChild(question);
|
|
1347
|
-
const scale = document.createElement("div");
|
|
1348
|
-
scale.style.cssText = "display: flex; gap: 4px; justify-content: center; flex-wrap: wrap; margin-bottom: 12px;";
|
|
1349
|
-
for (let i = 0; i <= 10; i++) {
|
|
1350
|
-
const btn = document.createElement("span");
|
|
1351
|
-
btn.style.cssText = `
|
|
1352
|
-
width: 28px; height: 28px; border-radius: 6px; display: flex;
|
|
1353
|
-
align-items: center; justify-content: center; font-size: 11px;
|
|
1354
|
-
font-weight: 600; cursor: pointer; background: ${text}33; color: ${text};
|
|
1355
|
-
transition: transform 0.1s;
|
|
1356
|
-
`;
|
|
1357
|
-
btn.textContent = String(i);
|
|
1358
|
-
btn.addEventListener("click", () => {
|
|
1359
|
-
this.trackEvent(campaign.id, "clicked");
|
|
1360
|
-
});
|
|
1361
|
-
scale.appendChild(btn);
|
|
1362
|
-
}
|
|
1363
|
-
body.appendChild(scale);
|
|
1364
|
-
const labels = document.createElement("div");
|
|
1365
|
-
labels.style.cssText = "display: flex; justify-content: space-between; font-size: 11px; opacity: 0.6; margin-bottom: 16px;";
|
|
1366
|
-
const notLikely = document.createElement("span");
|
|
1367
|
-
notLikely.textContent = "Not likely";
|
|
1368
|
-
const veryLikely = document.createElement("span");
|
|
1369
|
-
veryLikely.textContent = "Very likely";
|
|
1370
|
-
labels.appendChild(notLikely);
|
|
1371
|
-
labels.appendChild(veryLikely);
|
|
1372
|
-
body.appendChild(labels);
|
|
1682
|
+
const body = this._buildNPSSurveyBody(campaign, ic, text, "overlay");
|
|
1373
1683
|
this.addCloseButton(body, overlay, campaign.id);
|
|
1374
1684
|
modal.appendChild(body);
|
|
1375
1685
|
overlay.appendChild(modal);
|
|
@@ -1450,31 +1760,7 @@ const _AegisInAppManager = class _AegisInAppManager {
|
|
|
1450
1760
|
background: ${bg}; color: ${text}; animation: aegisScaleIn 0.3s ease;
|
|
1451
1761
|
box-shadow: 0 20px 60px rgba(0,0,0,0.15);
|
|
1452
1762
|
`;
|
|
1453
|
-
const body =
|
|
1454
|
-
body.style.cssText = "padding: 24px; text-align: center;";
|
|
1455
|
-
const title = document.createElement("div");
|
|
1456
|
-
title.style.cssText = "font-size: 18px; font-weight: 700; margin-bottom: 16px;";
|
|
1457
|
-
title.textContent = campaign.title || "Rate your experience";
|
|
1458
|
-
body.appendChild(title);
|
|
1459
|
-
const stars = document.createElement("div");
|
|
1460
|
-
stars.style.cssText = "display: flex; gap: 8px; justify-content: center; margin-bottom: 16px;";
|
|
1461
|
-
const maxStars = ic.rating_scale || 5;
|
|
1462
|
-
for (let i = 1; i <= maxStars; i++) {
|
|
1463
|
-
const star = document.createElement("span");
|
|
1464
|
-
star.style.cssText = "font-size: 32px; cursor: pointer; transition: transform 0.1s;";
|
|
1465
|
-
star.textContent = "☆";
|
|
1466
|
-
star.addEventListener("click", () => {
|
|
1467
|
-
this.trackEvent(campaign.id, "clicked");
|
|
1468
|
-
});
|
|
1469
|
-
star.addEventListener("mouseenter", () => {
|
|
1470
|
-
star.style.transform = "scale(1.2)";
|
|
1471
|
-
});
|
|
1472
|
-
star.addEventListener("mouseleave", () => {
|
|
1473
|
-
star.style.transform = "scale(1)";
|
|
1474
|
-
});
|
|
1475
|
-
stars.appendChild(star);
|
|
1476
|
-
}
|
|
1477
|
-
body.appendChild(stars);
|
|
1763
|
+
const body = this._buildStarRatingBody(campaign, ic, text, "overlay");
|
|
1478
1764
|
this.addCloseButton(body, overlay, campaign.id);
|
|
1479
1765
|
modal.appendChild(body);
|
|
1480
1766
|
overlay.appendChild(modal);
|
|
@@ -2526,7 +2812,7 @@ const _AegisInAppManager = class _AegisInAppManager {
|
|
|
2526
2812
|
this.disconnectSSE();
|
|
2527
2813
|
if (typeof document !== "undefined") {
|
|
2528
2814
|
document.querySelectorAll(
|
|
2529
|
-
".aegis-in-app-banner, .aegis-in-app-modal-overlay, .aegis-in-app-fullscreen-overlay, .aegis-in-app-half-interstitial-overlay, .aegis-in-app-alert-overlay, .aegis-in-app-pip, .aegis-in-app-nps-overlay, .aegis-in-app-countdown-overlay, .aegis-in-app-rating-overlay, .aegis-in-app-poll-overlay, .aegis-in-app-quiz-overlay"
|
|
2815
|
+
".aegis-in-app-banner, .aegis-in-app-modal-overlay, .aegis-in-app-fullscreen-overlay, .aegis-in-app-half-interstitial-overlay, .aegis-in-app-alert-overlay, .aegis-in-app-pip, .aegis-in-app-nps-overlay, .aegis-in-app-countdown-overlay, .aegis-in-app-rating-overlay, .aegis-in-app-poll-overlay, .aegis-in-app-quiz-overlay, .aegis-in-app-rating-card, .aegis-in-app-nps-card"
|
|
2530
2816
|
).forEach((el) => {
|
|
2531
2817
|
if (el.parentNode) {
|
|
2532
2818
|
el.parentNode.removeChild(el);
|
|
@@ -3233,6 +3519,28 @@ class TriggerEngine {
|
|
|
3233
3519
|
this.visibilityChangeEnabled = false;
|
|
3234
3520
|
this.backButtonEnabled = false;
|
|
3235
3521
|
this.backButtonFired = false;
|
|
3522
|
+
this.rageClickEnabled = false;
|
|
3523
|
+
this.rageClickConfig = { threshold: 3, windowMs: 1e3 };
|
|
3524
|
+
this.rageClickBuffer = /* @__PURE__ */ new Map();
|
|
3525
|
+
this.rageClickFiredInBurst = /* @__PURE__ */ new Set();
|
|
3526
|
+
this.hoverEnabled = false;
|
|
3527
|
+
this.hoverStartAt = /* @__PURE__ */ new Map();
|
|
3528
|
+
this.hoverLastMs = /* @__PURE__ */ new Map();
|
|
3529
|
+
this.hoverThresholds = /* @__PURE__ */ new Map([
|
|
3530
|
+
["price", [1500]],
|
|
3531
|
+
["cta", [1500]]
|
|
3532
|
+
]);
|
|
3533
|
+
this.hoverThresholdsFired = /* @__PURE__ */ new Map();
|
|
3534
|
+
this.hoverThresholdTimers = /* @__PURE__ */ new Map();
|
|
3535
|
+
this.mouseVelEnabled = false;
|
|
3536
|
+
this.mouseVelConfig = {
|
|
3537
|
+
threshold: 1.5,
|
|
3538
|
+
windowMs: 400,
|
|
3539
|
+
cooldownMs: 5e3
|
|
3540
|
+
};
|
|
3541
|
+
this.mouseSamples = [];
|
|
3542
|
+
this.mouseVelLast = 0;
|
|
3543
|
+
this.mouseVelLastFiredAt = 0;
|
|
3236
3544
|
this.handleScroll = () => {
|
|
3237
3545
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
3238
3546
|
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
|
|
@@ -3377,6 +3685,204 @@ class TriggerEngine {
|
|
|
3377
3685
|
registerBackButton() {
|
|
3378
3686
|
this.backButtonEnabled = true;
|
|
3379
3687
|
}
|
|
3688
|
+
/**
|
|
3689
|
+
* Enable rage-click detection (P2 Task 1).
|
|
3690
|
+
*
|
|
3691
|
+
* After calling this, feed click events via `noteClick(intent)`.
|
|
3692
|
+
* SelectorBinder (P1 Task 5) is the upstream source; the wiring lives
|
|
3693
|
+
* in IntentSnapshotCollector (P2 Task 5). Calling `noteClick` without
|
|
3694
|
+
* a prior `registerRageClick()` is a no-op so legacy callers don't
|
|
3695
|
+
* accidentally enable the feature.
|
|
3696
|
+
*/
|
|
3697
|
+
registerRageClick(config) {
|
|
3698
|
+
this.rageClickEnabled = true;
|
|
3699
|
+
if (config) {
|
|
3700
|
+
this.rageClickConfig = {
|
|
3701
|
+
threshold: config.threshold ?? this.rageClickConfig.threshold,
|
|
3702
|
+
windowMs: config.windowMs ?? this.rageClickConfig.windowMs
|
|
3703
|
+
};
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
/**
|
|
3707
|
+
* Feed a click event for rage-click detection. Returns the current
|
|
3708
|
+
* count within-window for the intent — useful for the snapshot
|
|
3709
|
+
* collector (P2 Task 5) which updates IntentRuleEvaluator with
|
|
3710
|
+
* `rage_click_count` continuously, not just at the burst threshold.
|
|
3711
|
+
*
|
|
3712
|
+
* Idempotent w.r.t. `registerRageClick` — when not enabled, this
|
|
3713
|
+
* returns 0 without buffering.
|
|
3714
|
+
*/
|
|
3715
|
+
noteClick(intent, timestamp = Date.now()) {
|
|
3716
|
+
if (!this.rageClickEnabled) return 0;
|
|
3717
|
+
const buf = this.rageClickBuffer.get(intent) ?? [];
|
|
3718
|
+
const cutoff = timestamp - this.rageClickConfig.windowMs;
|
|
3719
|
+
let trimStart = 0;
|
|
3720
|
+
while (trimStart < buf.length && buf[trimStart] < cutoff) trimStart++;
|
|
3721
|
+
const recent = trimStart === 0 ? buf : buf.slice(trimStart);
|
|
3722
|
+
recent.push(timestamp);
|
|
3723
|
+
this.rageClickBuffer.set(intent, recent);
|
|
3724
|
+
if (recent.length < this.rageClickConfig.threshold) {
|
|
3725
|
+
this.rageClickFiredInBurst.delete(intent);
|
|
3726
|
+
}
|
|
3727
|
+
if (recent.length >= this.rageClickConfig.threshold && !this.rageClickFiredInBurst.has(intent)) {
|
|
3728
|
+
this.rageClickFiredInBurst.add(intent);
|
|
3729
|
+
this.emit("rage_click", {
|
|
3730
|
+
intent,
|
|
3731
|
+
count: recent.length,
|
|
3732
|
+
window_ms: this.rageClickConfig.windowMs
|
|
3733
|
+
});
|
|
3734
|
+
}
|
|
3735
|
+
return recent.length;
|
|
3736
|
+
}
|
|
3737
|
+
/** Snapshot the current rage_click count for an intent. The
|
|
3738
|
+
* IntentSnapshotCollector (P2 Task 5) calls this when building the
|
|
3739
|
+
* `rage_click_count` signal value for the IntentRuleEvaluator. */
|
|
3740
|
+
getRageClickCount(intent) {
|
|
3741
|
+
if (!this.rageClickEnabled) return 0;
|
|
3742
|
+
const buf = this.rageClickBuffer.get(intent);
|
|
3743
|
+
if (!buf) return 0;
|
|
3744
|
+
const cutoff = Date.now() - this.rageClickConfig.windowMs;
|
|
3745
|
+
let live = 0;
|
|
3746
|
+
for (const ts of buf) {
|
|
3747
|
+
if (ts >= cutoff) live++;
|
|
3748
|
+
}
|
|
3749
|
+
return live;
|
|
3750
|
+
}
|
|
3751
|
+
/**
|
|
3752
|
+
* Enable hover-dwell tracking (P2 Task 2). After registration, feed
|
|
3753
|
+
* hover events via `noteHoverStart(intent, ts)` / `noteHoverEnd`.
|
|
3754
|
+
* SelectorBinder (P1 Task 5) is the upstream source; wiring lands in
|
|
3755
|
+
* P2 Task 5.
|
|
3756
|
+
*/
|
|
3757
|
+
registerHoverDwell(config) {
|
|
3758
|
+
this.hoverEnabled = true;
|
|
3759
|
+
if (config == null ? void 0 : config.thresholdsByIntent) {
|
|
3760
|
+
for (const [intent, thresholds] of Object.entries(config.thresholdsByIntent)) {
|
|
3761
|
+
if (thresholds) this.hoverThresholds.set(intent, thresholds);
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
/** Record the start of a hover on the named intent. Schedules
|
|
3766
|
+
* per-threshold timers that emit `<intent>_hover_dwell_<ms>` when
|
|
3767
|
+
* the active hover passes each registered threshold. */
|
|
3768
|
+
noteHoverStart(intent, timestamp = Date.now()) {
|
|
3769
|
+
if (!this.hoverEnabled) return;
|
|
3770
|
+
if (this.hoverStartAt.has(intent)) {
|
|
3771
|
+
this.noteHoverEnd(intent, timestamp);
|
|
3772
|
+
}
|
|
3773
|
+
this.hoverStartAt.set(intent, timestamp);
|
|
3774
|
+
this.hoverThresholdsFired.set(intent, /* @__PURE__ */ new Set());
|
|
3775
|
+
const thresholds = this.hoverThresholds.get(intent);
|
|
3776
|
+
if (thresholds) {
|
|
3777
|
+
const timers = [];
|
|
3778
|
+
for (const ms of thresholds) {
|
|
3779
|
+
const timerId = setTimeout(() => {
|
|
3780
|
+
if (!this.hoverStartAt.has(intent)) return;
|
|
3781
|
+
const firedSet = this.hoverThresholdsFired.get(intent);
|
|
3782
|
+
if (firedSet == null ? void 0 : firedSet.has(ms)) return;
|
|
3783
|
+
firedSet == null ? void 0 : firedSet.add(ms);
|
|
3784
|
+
this.emit(`${intent}_hover_dwell_${ms}`, {
|
|
3785
|
+
intent,
|
|
3786
|
+
threshold_ms: ms,
|
|
3787
|
+
actual_ms: Date.now() - timestamp
|
|
3788
|
+
});
|
|
3789
|
+
}, ms);
|
|
3790
|
+
timers.push(timerId);
|
|
3791
|
+
}
|
|
3792
|
+
this.hoverThresholdTimers.set(intent, timers);
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
/** Record the end of a hover on the named intent. Updates the sticky
|
|
3796
|
+
* `lastHoverMs` so `getHoverMs` keeps returning the last-known
|
|
3797
|
+
* duration until the next hover_start. */
|
|
3798
|
+
noteHoverEnd(intent, timestamp = Date.now()) {
|
|
3799
|
+
if (!this.hoverEnabled) return;
|
|
3800
|
+
const startAt = this.hoverStartAt.get(intent);
|
|
3801
|
+
if (startAt === void 0) return;
|
|
3802
|
+
const dwell = Math.max(0, timestamp - startAt);
|
|
3803
|
+
this.hoverLastMs.set(intent, dwell);
|
|
3804
|
+
this.hoverStartAt.delete(intent);
|
|
3805
|
+
const pending = this.hoverThresholdTimers.get(intent);
|
|
3806
|
+
if (pending) {
|
|
3807
|
+
for (const id of pending) clearTimeout(id);
|
|
3808
|
+
this.hoverThresholdTimers.delete(intent);
|
|
3809
|
+
}
|
|
3810
|
+
this.hoverThresholdsFired.delete(intent);
|
|
3811
|
+
}
|
|
3812
|
+
/**
|
|
3813
|
+
* Enable mouse-velocity-to-top tracking (P2 Task 3). After
|
|
3814
|
+
* registration, feed positions via `noteMousePosition(x, y, ts?)`.
|
|
3815
|
+
* IntentSnapshotCollector (P2 Task 5) wires window 'mousemove' to
|
|
3816
|
+
* this method, with throttling to ~100ms per the plan.
|
|
3817
|
+
*/
|
|
3818
|
+
registerMouseVelocityToTop(config) {
|
|
3819
|
+
this.mouseVelEnabled = true;
|
|
3820
|
+
if (config) {
|
|
3821
|
+
this.mouseVelConfig = {
|
|
3822
|
+
threshold: config.threshold ?? this.mouseVelConfig.threshold,
|
|
3823
|
+
windowMs: config.windowMs ?? this.mouseVelConfig.windowMs,
|
|
3824
|
+
cooldownMs: config.cooldownMs ?? this.mouseVelConfig.cooldownMs
|
|
3825
|
+
};
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
/** Feed a mouse-position sample. Recomputes the rolling-window
|
|
3829
|
+
* velocity-to-top and fires `mouse_velocity_to_top` when the value
|
|
3830
|
+
* crosses the configured threshold (subject to cooldown). */
|
|
3831
|
+
noteMousePosition(x, y, timestamp = Date.now()) {
|
|
3832
|
+
if (!this.mouseVelEnabled) return 0;
|
|
3833
|
+
const cutoff = timestamp - this.mouseVelConfig.windowMs;
|
|
3834
|
+
let trimStart = 0;
|
|
3835
|
+
while (trimStart < this.mouseSamples.length && this.mouseSamples[trimStart].ts < cutoff) {
|
|
3836
|
+
trimStart++;
|
|
3837
|
+
}
|
|
3838
|
+
if (trimStart > 0) this.mouseSamples = this.mouseSamples.slice(trimStart);
|
|
3839
|
+
this.mouseSamples.push({ x, y, ts: timestamp });
|
|
3840
|
+
if (this.mouseSamples.length < 2) {
|
|
3841
|
+
this.mouseVelLast = 0;
|
|
3842
|
+
return 0;
|
|
3843
|
+
}
|
|
3844
|
+
const oldest = this.mouseSamples[0];
|
|
3845
|
+
const newest = this.mouseSamples[this.mouseSamples.length - 1];
|
|
3846
|
+
const dt = newest.ts - oldest.ts;
|
|
3847
|
+
if (dt <= 0) {
|
|
3848
|
+
this.mouseVelLast = 0;
|
|
3849
|
+
return 0;
|
|
3850
|
+
}
|
|
3851
|
+
const velocity = -(newest.y - oldest.y) / dt;
|
|
3852
|
+
this.mouseVelLast = velocity;
|
|
3853
|
+
const cooldownOk = this.mouseVelLastFiredAt === 0 || timestamp - this.mouseVelLastFiredAt >= this.mouseVelConfig.cooldownMs;
|
|
3854
|
+
if (velocity >= this.mouseVelConfig.threshold && cooldownOk) {
|
|
3855
|
+
this.mouseVelLastFiredAt = timestamp;
|
|
3856
|
+
this.emit("mouse_velocity_to_top", {
|
|
3857
|
+
velocity,
|
|
3858
|
+
threshold: this.mouseVelConfig.threshold,
|
|
3859
|
+
window_ms: this.mouseVelConfig.windowMs,
|
|
3860
|
+
samples: this.mouseSamples.length
|
|
3861
|
+
});
|
|
3862
|
+
}
|
|
3863
|
+
return velocity;
|
|
3864
|
+
}
|
|
3865
|
+
/** Snapshot the current velocity-to-top in px/ms (positive = up).
|
|
3866
|
+
* Returns 0 when not enabled or insufficient samples. */
|
|
3867
|
+
getMouseVelocityToTop() {
|
|
3868
|
+
return this.mouseVelEnabled ? this.mouseVelLast : 0;
|
|
3869
|
+
}
|
|
3870
|
+
/**
|
|
3871
|
+
* Snapshot the hover dwell for an intent.
|
|
3872
|
+
* - Currently hovering: returns `Date.now() - hover_start_at`.
|
|
3873
|
+
* - Hovered before, not currently: returns the most recent hover's
|
|
3874
|
+
* duration (sticky — the rule evaluator queries this AFTER
|
|
3875
|
+
* mouseleave to see "did Priya hover the price ≥ 1.5s?").
|
|
3876
|
+
* - Never hovered: returns 0.
|
|
3877
|
+
*/
|
|
3878
|
+
getHoverMs(intent) {
|
|
3879
|
+
if (!this.hoverEnabled) return 0;
|
|
3880
|
+
const startAt = this.hoverStartAt.get(intent);
|
|
3881
|
+
if (startAt !== void 0) {
|
|
3882
|
+
return Math.max(0, Date.now() - startAt);
|
|
3883
|
+
}
|
|
3884
|
+
return this.hoverLastMs.get(intent) ?? 0;
|
|
3885
|
+
}
|
|
3380
3886
|
start() {
|
|
3381
3887
|
if (this.isStarted) {
|
|
3382
3888
|
return;
|