@active-reach/web-sdk 1.4.0 → 1.6.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-D9BAnJAu.mjs → analytics-C00PJUSy.mjs} +7 -2
- package/dist/{analytics-D9BAnJAu.mjs.map → analytics-C00PJUSy.mjs.map} +1 -1
- package/dist/core/prefetch-bundle-client.d.ts +150 -0
- package/dist/core/prefetch-bundle-client.d.ts.map +1 -0
- package/dist/governance/name-governor.d.ts +10 -0
- package/dist/governance/name-governor.d.ts.map +1 -1
- package/dist/inapp/AegisInAppManager.d.ts +26 -1
- package/dist/inapp/AegisInAppManager.d.ts.map +1 -1
- package/dist/inapp/renderers/carousel-cards.d.ts +15 -0
- package/dist/inapp/renderers/carousel-cards.d.ts.map +1 -0
- package/dist/inapp/renderers/coachmark-tour.d.ts +24 -0
- package/dist/inapp/renderers/coachmark-tour.d.ts.map +1 -0
- package/dist/inapp/renderers/index.d.ts +12 -0
- package/dist/inapp/renderers/index.d.ts.map +1 -0
- package/dist/inapp/renderers/product-recommendation.d.ts +23 -0
- package/dist/inapp/renderers/product-recommendation.d.ts.map +1 -0
- package/dist/inapp/renderers/progress-bar.d.ts +24 -0
- package/dist/inapp/renderers/progress-bar.d.ts.map +1 -0
- package/dist/inapp/renderers/sticky-bar.d.ts +14 -0
- package/dist/inapp/renderers/sticky-bar.d.ts.map +1 -0
- package/dist/inapp/renderers/types.d.ts +27 -0
- package/dist/inapp/renderers/types.d.ts.map +1 -0
- package/dist/inbox/AegisInbox.d.ts +103 -0
- package/dist/inbox/AegisInbox.d.ts.map +1 -0
- package/dist/inbox/index.d.ts +3 -0
- package/dist/inbox/index.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1382 -3
- package/dist/index.js.map +1 -1
- package/dist/react.js +1 -1
- package/dist/runtime/AegisMessageRuntime.d.ts +88 -0
- package/dist/runtime/AegisMessageRuntime.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { l as logger, A as Aegis } from "./analytics-
|
|
2
|
-
import { B, E, N, R, m } from "./analytics-
|
|
1
|
+
import { l as logger, A as Aegis } from "./analytics-C00PJUSy.mjs";
|
|
2
|
+
import { B, E, N, R, m } from "./analytics-C00PJUSy.mjs";
|
|
3
3
|
import { AegisWebPush } from "./push/AegisWebPush.js";
|
|
4
4
|
function debounce(func, wait) {
|
|
5
5
|
let timeoutId = null;
|
|
@@ -25,6 +25,715 @@ function throttle(func, limit) {
|
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
const MAX_CARDS_RENDERED = 10;
|
|
29
|
+
function renderCarouselCards(ctx) {
|
|
30
|
+
const { campaign, trackEvent, sanitizeUrl, sanitizeColor, log, addAnimationStyles } = ctx;
|
|
31
|
+
const ic = campaign.interactive_config || {};
|
|
32
|
+
const rawCards = Array.isArray(ic.cards) ? ic.cards : [];
|
|
33
|
+
const cards = rawCards.slice(0, MAX_CARDS_RENDERED);
|
|
34
|
+
if (cards.length === 0) {
|
|
35
|
+
log("carousel_cards rendered with zero cards — skipping", "warn");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const autoplay = Number(ic.autoplay_ms) || 0;
|
|
39
|
+
const loop = ic.loop !== false;
|
|
40
|
+
addAnimationStyles();
|
|
41
|
+
const bg = sanitizeColor(campaign.background_color || "#ffffff");
|
|
42
|
+
const fg = sanitizeColor(campaign.text_color || "#0f172a");
|
|
43
|
+
const overlay = document.createElement("div");
|
|
44
|
+
overlay.className = "aegis-in-app-carousel-overlay";
|
|
45
|
+
overlay.setAttribute("data-campaign-id", campaign.id);
|
|
46
|
+
overlay.style.cssText = `
|
|
47
|
+
position: fixed; left: 0; right: 0; bottom: 0;
|
|
48
|
+
z-index: 99999; padding: 12px 12px 20px;
|
|
49
|
+
background: rgba(0,0,0,0.12); backdrop-filter: blur(8px);
|
|
50
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
51
|
+
animation: aegisSlideInFromBottom 0.3s ease-out;
|
|
52
|
+
`;
|
|
53
|
+
const card = document.createElement("div");
|
|
54
|
+
card.style.cssText = `
|
|
55
|
+
background: ${bg}; color: ${fg}; border-radius: 16px;
|
|
56
|
+
box-shadow: 0 8px 20px rgba(0,0,0,0.08); padding: 16px;
|
|
57
|
+
max-width: 520px; margin: 0 auto; position: relative;
|
|
58
|
+
`;
|
|
59
|
+
const header = document.createElement("div");
|
|
60
|
+
header.style.cssText = "display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 12px;";
|
|
61
|
+
const headerText = document.createElement("div");
|
|
62
|
+
const title = document.createElement("div");
|
|
63
|
+
title.textContent = campaign.title;
|
|
64
|
+
title.style.cssText = "font-weight: 700; font-size: 15px; margin-bottom: 2px;";
|
|
65
|
+
const body = document.createElement("div");
|
|
66
|
+
body.textContent = campaign.body;
|
|
67
|
+
body.style.cssText = "font-size: 13px; opacity: 0.75;";
|
|
68
|
+
headerText.appendChild(title);
|
|
69
|
+
headerText.appendChild(body);
|
|
70
|
+
header.appendChild(headerText);
|
|
71
|
+
const closeBtn = document.createElement("button");
|
|
72
|
+
closeBtn.textContent = "✕";
|
|
73
|
+
closeBtn.setAttribute("aria-label", "Close");
|
|
74
|
+
closeBtn.style.cssText = `
|
|
75
|
+
background: transparent; border: none; color: inherit;
|
|
76
|
+
font-size: 16px; cursor: pointer; opacity: 0.6; padding: 2px 6px;
|
|
77
|
+
`;
|
|
78
|
+
closeBtn.addEventListener("click", () => {
|
|
79
|
+
trackEvent(campaign.id, "dismissed");
|
|
80
|
+
overlay.remove();
|
|
81
|
+
});
|
|
82
|
+
header.appendChild(closeBtn);
|
|
83
|
+
card.appendChild(header);
|
|
84
|
+
const track = document.createElement("div");
|
|
85
|
+
track.style.cssText = `
|
|
86
|
+
display: flex; gap: 10px; overflow-x: auto; scroll-snap-type: x mandatory;
|
|
87
|
+
scrollbar-width: none; -ms-overflow-style: none; padding-bottom: 4px;
|
|
88
|
+
`;
|
|
89
|
+
track.style.msOverflowStyle = "none";
|
|
90
|
+
track.addEventListener("wheel", (e) => {
|
|
91
|
+
if (Math.abs(e.deltaX) < Math.abs(e.deltaY)) return;
|
|
92
|
+
track.scrollLeft += e.deltaX;
|
|
93
|
+
});
|
|
94
|
+
cards.forEach((c, i) => {
|
|
95
|
+
const tile = document.createElement("div");
|
|
96
|
+
tile.setAttribute("data-card-index", String(i));
|
|
97
|
+
tile.style.cssText = `
|
|
98
|
+
flex: 0 0 auto; width: 140px; scroll-snap-align: start;
|
|
99
|
+
background: ${fg}0a; border-radius: 12px; padding: 10px;
|
|
100
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
101
|
+
cursor: ${c.cta_url ? "pointer" : "default"};
|
|
102
|
+
`;
|
|
103
|
+
if (c.image_url) {
|
|
104
|
+
const img = document.createElement("img");
|
|
105
|
+
const safe = sanitizeUrl(c.image_url);
|
|
106
|
+
if (safe) {
|
|
107
|
+
img.src = safe;
|
|
108
|
+
img.alt = "";
|
|
109
|
+
img.loading = "lazy";
|
|
110
|
+
img.style.cssText = "width: 100%; height: 96px; border-radius: 8px; object-fit: cover;";
|
|
111
|
+
tile.appendChild(img);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (c.title) {
|
|
115
|
+
const t = document.createElement("div");
|
|
116
|
+
t.textContent = c.title;
|
|
117
|
+
t.style.cssText = "font-weight: 600; font-size: 13px; line-height: 1.3;";
|
|
118
|
+
tile.appendChild(t);
|
|
119
|
+
}
|
|
120
|
+
if (c.body) {
|
|
121
|
+
const b = document.createElement("div");
|
|
122
|
+
b.textContent = c.body;
|
|
123
|
+
b.style.cssText = "font-size: 11.5px; opacity: 0.72; line-height: 1.3;";
|
|
124
|
+
tile.appendChild(b);
|
|
125
|
+
}
|
|
126
|
+
if (c.cta_text && c.cta_url) {
|
|
127
|
+
const cta = document.createElement("button");
|
|
128
|
+
cta.textContent = c.cta_text;
|
|
129
|
+
cta.style.cssText = `
|
|
130
|
+
margin-top: auto; background: ${fg}; color: ${bg};
|
|
131
|
+
border: none; padding: 6px 10px; border-radius: 999px;
|
|
132
|
+
font-size: 12px; font-weight: 600; cursor: pointer;
|
|
133
|
+
`;
|
|
134
|
+
const goto2 = (e) => {
|
|
135
|
+
e.stopPropagation();
|
|
136
|
+
trackEvent(campaign.id, "clicked");
|
|
137
|
+
const safe = sanitizeUrl(c.cta_url);
|
|
138
|
+
if (safe) window.location.href = safe;
|
|
139
|
+
};
|
|
140
|
+
cta.addEventListener("click", goto2);
|
|
141
|
+
tile.appendChild(cta);
|
|
142
|
+
tile.addEventListener("click", goto2);
|
|
143
|
+
}
|
|
144
|
+
track.appendChild(tile);
|
|
145
|
+
});
|
|
146
|
+
card.appendChild(track);
|
|
147
|
+
const dots = document.createElement("div");
|
|
148
|
+
dots.style.cssText = "display: flex; justify-content: center; gap: 6px; margin-top: 10px;";
|
|
149
|
+
const dotEls = [];
|
|
150
|
+
cards.forEach(() => {
|
|
151
|
+
const dot = document.createElement("span");
|
|
152
|
+
dot.style.cssText = `
|
|
153
|
+
width: 6px; height: 6px; border-radius: 999px; background: ${fg};
|
|
154
|
+
opacity: 0.25; transition: opacity 0.2s;
|
|
155
|
+
`;
|
|
156
|
+
dotEls.push(dot);
|
|
157
|
+
dots.appendChild(dot);
|
|
158
|
+
});
|
|
159
|
+
card.appendChild(dots);
|
|
160
|
+
const setActive = (idx) => {
|
|
161
|
+
dotEls.forEach((d, i) => d.style.opacity = i === idx ? "0.9" : "0.25");
|
|
162
|
+
};
|
|
163
|
+
setActive(0);
|
|
164
|
+
let activeIdx = 0;
|
|
165
|
+
const tiles = track.querySelectorAll("[data-card-index]");
|
|
166
|
+
const goto = (idx) => {
|
|
167
|
+
activeIdx = (idx % cards.length + cards.length) % cards.length;
|
|
168
|
+
const el = tiles[activeIdx];
|
|
169
|
+
if (el) {
|
|
170
|
+
el.scrollIntoView({ behavior: "smooth", inline: "start", block: "nearest" });
|
|
171
|
+
setActive(activeIdx);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
let autoplayTimer = null;
|
|
175
|
+
if (autoplay > 0 && cards.length > 1) {
|
|
176
|
+
autoplayTimer = setInterval(() => {
|
|
177
|
+
const next = activeIdx + 1;
|
|
178
|
+
if (next >= cards.length && !loop) {
|
|
179
|
+
if (autoplayTimer) clearInterval(autoplayTimer);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
goto(next);
|
|
183
|
+
}, autoplay);
|
|
184
|
+
}
|
|
185
|
+
overlay.addEventListener("remove", () => {
|
|
186
|
+
if (autoplayTimer) clearInterval(autoplayTimer);
|
|
187
|
+
});
|
|
188
|
+
track.addEventListener("scroll", () => {
|
|
189
|
+
const approx = Math.round(track.scrollLeft / 150);
|
|
190
|
+
if (approx !== activeIdx && approx >= 0 && approx < cards.length) {
|
|
191
|
+
activeIdx = approx;
|
|
192
|
+
setActive(activeIdx);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
overlay.appendChild(card);
|
|
196
|
+
document.body.appendChild(overlay);
|
|
197
|
+
}
|
|
198
|
+
const DISMISS_STORAGE_PREFIX = "aegis_sticky_dismissed:";
|
|
199
|
+
function renderStickyBar(ctx) {
|
|
200
|
+
const { campaign, trackEvent, sanitizeUrl, sanitizeColor, log, addAnimationStyles } = ctx;
|
|
201
|
+
const ic = campaign.interactive_config || {};
|
|
202
|
+
const position = ic.sticky_position === "top" ? "top" : "bottom";
|
|
203
|
+
const dismissible = ic.sticky_dismissible !== false;
|
|
204
|
+
const autoHide = Number(ic.sticky_auto_hide_ms) || 0;
|
|
205
|
+
try {
|
|
206
|
+
if (typeof localStorage !== "undefined" && localStorage.getItem(DISMISS_STORAGE_PREFIX + campaign.id)) {
|
|
207
|
+
log(`sticky_bar ${campaign.id} suppressed — user dismissed earlier`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
addAnimationStyles();
|
|
213
|
+
const bg = sanitizeColor(
|
|
214
|
+
ic.sticky_bg_color || campaign.background_color || "#4169e1"
|
|
215
|
+
);
|
|
216
|
+
const fg = sanitizeColor(campaign.text_color || "#ffffff");
|
|
217
|
+
const bar = document.createElement("div");
|
|
218
|
+
bar.className = "aegis-in-app-sticky-bar";
|
|
219
|
+
bar.setAttribute("data-campaign-id", campaign.id);
|
|
220
|
+
const positionCss = position === "top" ? "top: 0; left: 0; right: 0; animation: aegisSlideDown 0.3s ease-out;" : "bottom: 0; left: 0; right: 0; animation: aegisSlideInFromBottom 0.3s ease-out;";
|
|
221
|
+
bar.style.cssText = `
|
|
222
|
+
position: fixed; ${positionCss}
|
|
223
|
+
background: ${bg}; color: ${fg};
|
|
224
|
+
padding: 10px 14px; z-index: 999998;
|
|
225
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
226
|
+
display: flex; align-items: center; gap: 12px; justify-content: center;
|
|
227
|
+
box-shadow: 0 ${position === "top" ? "2px" : "-2px"} 8px rgba(0,0,0,0.08);
|
|
228
|
+
`;
|
|
229
|
+
const label = document.createElement("div");
|
|
230
|
+
label.style.cssText = "flex: 0 1 auto; font-size: 13px; font-weight: 500; text-align: center;";
|
|
231
|
+
const strong = document.createElement("strong");
|
|
232
|
+
strong.textContent = campaign.title;
|
|
233
|
+
strong.style.cssText = "margin-right: 6px; font-weight: 700;";
|
|
234
|
+
label.appendChild(strong);
|
|
235
|
+
label.appendChild(document.createTextNode(campaign.body));
|
|
236
|
+
bar.appendChild(label);
|
|
237
|
+
if (campaign.action_url && campaign.button_text) {
|
|
238
|
+
const cta = document.createElement("button");
|
|
239
|
+
cta.textContent = campaign.button_text;
|
|
240
|
+
cta.style.cssText = `
|
|
241
|
+
background: ${fg}; color: ${bg};
|
|
242
|
+
border: none; padding: 6px 14px; border-radius: 999px;
|
|
243
|
+
font-size: 12px; font-weight: 700; cursor: pointer; white-space: nowrap;
|
|
244
|
+
`;
|
|
245
|
+
cta.addEventListener("click", () => {
|
|
246
|
+
trackEvent(campaign.id, "clicked");
|
|
247
|
+
const safe = sanitizeUrl(campaign.action_url);
|
|
248
|
+
if (safe) window.location.href = safe;
|
|
249
|
+
});
|
|
250
|
+
bar.appendChild(cta);
|
|
251
|
+
}
|
|
252
|
+
let autoHideTimer = null;
|
|
253
|
+
const remove = (persist) => {
|
|
254
|
+
if (autoHideTimer) clearTimeout(autoHideTimer);
|
|
255
|
+
if (persist) {
|
|
256
|
+
try {
|
|
257
|
+
if (typeof localStorage !== "undefined") {
|
|
258
|
+
localStorage.setItem(DISMISS_STORAGE_PREFIX + campaign.id, "1");
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
bar.remove();
|
|
264
|
+
};
|
|
265
|
+
if (dismissible) {
|
|
266
|
+
const close = document.createElement("button");
|
|
267
|
+
close.textContent = "✕";
|
|
268
|
+
close.setAttribute("aria-label", "Dismiss");
|
|
269
|
+
close.style.cssText = `
|
|
270
|
+
background: transparent; border: none; color: inherit;
|
|
271
|
+
font-size: 16px; cursor: pointer; padding: 0 4px; opacity: 0.75;
|
|
272
|
+
`;
|
|
273
|
+
close.addEventListener("click", () => {
|
|
274
|
+
trackEvent(campaign.id, "dismissed");
|
|
275
|
+
remove(true);
|
|
276
|
+
});
|
|
277
|
+
bar.appendChild(close);
|
|
278
|
+
}
|
|
279
|
+
if (autoHide > 0) {
|
|
280
|
+
autoHideTimer = setTimeout(() => remove(false), autoHide);
|
|
281
|
+
}
|
|
282
|
+
document.body.appendChild(bar);
|
|
283
|
+
}
|
|
284
|
+
function renderProgressBar(ctx) {
|
|
285
|
+
const { campaign, trackEvent, sanitizeColor, log, addAnimationStyles } = ctx;
|
|
286
|
+
const ic = campaign.interactive_config || {};
|
|
287
|
+
const goalType = ic.progress_goal_type || "cart_total";
|
|
288
|
+
const threshold = Number(ic.progress_threshold);
|
|
289
|
+
if (!(threshold > 0)) {
|
|
290
|
+
log("progress_bar requires a positive progress_threshold — skipping", "warn");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const rewardText = ic.progress_reward_text || campaign.body || "Unlocked!";
|
|
294
|
+
const source = ic.progress_source === "sse" ? "sse" : "client";
|
|
295
|
+
addAnimationStyles();
|
|
296
|
+
const bg = sanitizeColor(campaign.background_color || "#f8fafc");
|
|
297
|
+
const fill = sanitizeColor(campaign.text_color || "#4169e1");
|
|
298
|
+
const fg = "#0f172a";
|
|
299
|
+
const bar = document.createElement("div");
|
|
300
|
+
bar.className = "aegis-in-app-progress-bar";
|
|
301
|
+
bar.setAttribute("data-campaign-id", campaign.id);
|
|
302
|
+
bar.style.cssText = `
|
|
303
|
+
position: fixed; left: 12px; right: 12px; bottom: 12px;
|
|
304
|
+
max-width: 540px; margin: 0 auto;
|
|
305
|
+
background: ${bg}; color: ${fg}; border-radius: 12px;
|
|
306
|
+
padding: 10px 14px; z-index: 999997;
|
|
307
|
+
box-shadow: 0 8px 20px rgba(0,0,0,0.08);
|
|
308
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
309
|
+
animation: aegisSlideInFromBottom 0.3s ease-out;
|
|
310
|
+
`;
|
|
311
|
+
const label = document.createElement("div");
|
|
312
|
+
label.style.cssText = "display: flex; justify-content: space-between; font-size: 13px; font-weight: 600; margin-bottom: 6px;";
|
|
313
|
+
const leading = document.createElement("span");
|
|
314
|
+
leading.textContent = campaign.title;
|
|
315
|
+
label.appendChild(leading);
|
|
316
|
+
const numeric = document.createElement("span");
|
|
317
|
+
numeric.style.cssText = "opacity: 0.7; font-weight: 500;";
|
|
318
|
+
label.appendChild(numeric);
|
|
319
|
+
bar.appendChild(label);
|
|
320
|
+
const shell = document.createElement("div");
|
|
321
|
+
shell.style.cssText = `
|
|
322
|
+
height: 8px; border-radius: 999px; background: ${fill}22;
|
|
323
|
+
overflow: hidden;
|
|
324
|
+
`;
|
|
325
|
+
const fillEl = document.createElement("div");
|
|
326
|
+
fillEl.style.cssText = `
|
|
327
|
+
height: 100%; border-radius: 999px; background: ${fill};
|
|
328
|
+
width: 0%; transition: width 0.4s cubic-bezier(.4,0,.2,1);
|
|
329
|
+
`;
|
|
330
|
+
shell.appendChild(fillEl);
|
|
331
|
+
bar.appendChild(shell);
|
|
332
|
+
const footnote = document.createElement("div");
|
|
333
|
+
footnote.style.cssText = "font-size: 11.5px; opacity: 0.65; margin-top: 6px;";
|
|
334
|
+
bar.appendChild(footnote);
|
|
335
|
+
const readCurrentValue = () => {
|
|
336
|
+
var _a, _b;
|
|
337
|
+
if (source === "sse") {
|
|
338
|
+
const hook = window.__aegisProgressSSE;
|
|
339
|
+
return hook && typeof hook[campaign.id] === "number" ? hook[campaign.id] : 0;
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
if (goalType === "cart_total" || goalType === "items_in_cart") {
|
|
343
|
+
const win = window;
|
|
344
|
+
if ((_a = win.Shopify) == null ? void 0 : _a.checkout) {
|
|
345
|
+
const v = parseFloat(String(win.Shopify.checkout.total_price || 0));
|
|
346
|
+
return goalType === "cart_total" ? v : 0;
|
|
347
|
+
}
|
|
348
|
+
if (win.aegis_cart) {
|
|
349
|
+
if (goalType === "items_in_cart") {
|
|
350
|
+
return Array.isArray(win.aegis_cart.cart_items) ? win.aegis_cart.cart_items.length : 0;
|
|
351
|
+
}
|
|
352
|
+
return Number(win.aegis_cart.cart_total || 0);
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
const cacheStr = (_b = window.localStorage) == null ? void 0 : _b.getItem("mage-cache-storage");
|
|
356
|
+
if (cacheStr) {
|
|
357
|
+
const cache = JSON.parse(cacheStr);
|
|
358
|
+
const cart = cache.cart;
|
|
359
|
+
if (cart) {
|
|
360
|
+
return goalType === "cart_total" ? Number(cart.subtotalAmount || 0) : Array.isArray(cart.items) ? cart.items.length : 0;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return 0;
|
|
367
|
+
} catch {
|
|
368
|
+
return 0;
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
let lastFiredUnlock = false;
|
|
372
|
+
const update = () => {
|
|
373
|
+
const cur = readCurrentValue();
|
|
374
|
+
const pct = Math.max(0, Math.min(100, cur / threshold * 100));
|
|
375
|
+
fillEl.style.width = `${pct}%`;
|
|
376
|
+
numeric.textContent = `${cur.toFixed(0)} / ${threshold.toFixed(0)}`;
|
|
377
|
+
if (pct >= 100) {
|
|
378
|
+
footnote.textContent = rewardText;
|
|
379
|
+
if (!lastFiredUnlock) {
|
|
380
|
+
lastFiredUnlock = true;
|
|
381
|
+
trackEvent(campaign.id, "clicked");
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
const remaining = Math.max(0, threshold - cur);
|
|
385
|
+
footnote.textContent = `Add ${remaining.toFixed(0)} more to unlock: ${rewardText}`;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
update();
|
|
389
|
+
const pollTimer = setInterval(update, 1e3);
|
|
390
|
+
const cleanup = () => clearInterval(pollTimer);
|
|
391
|
+
window.addEventListener("beforeunload", cleanup, { once: true });
|
|
392
|
+
bar.addEventListener("remove", cleanup);
|
|
393
|
+
document.body.appendChild(bar);
|
|
394
|
+
}
|
|
395
|
+
const RESUME_PREFIX = "aegis_coachmark_progress:";
|
|
396
|
+
function readResumeIdx(resumeKey) {
|
|
397
|
+
try {
|
|
398
|
+
if (typeof localStorage === "undefined") return 0;
|
|
399
|
+
const raw = localStorage.getItem(RESUME_PREFIX + resumeKey);
|
|
400
|
+
const n = raw ? parseInt(raw, 10) : 0;
|
|
401
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
402
|
+
} catch {
|
|
403
|
+
return 0;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function writeResumeIdx(resumeKey, idx) {
|
|
407
|
+
try {
|
|
408
|
+
if (typeof localStorage !== "undefined") {
|
|
409
|
+
localStorage.setItem(RESUME_PREFIX + resumeKey, String(idx));
|
|
410
|
+
}
|
|
411
|
+
} catch {
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function clearResume(resumeKey) {
|
|
415
|
+
try {
|
|
416
|
+
if (typeof localStorage !== "undefined") {
|
|
417
|
+
localStorage.removeItem(RESUME_PREFIX + resumeKey);
|
|
418
|
+
}
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function renderCoachmarkTour(ctx) {
|
|
423
|
+
const { campaign, trackEvent, sanitizeColor, log, addAnimationStyles } = ctx;
|
|
424
|
+
const ic = campaign.interactive_config || {};
|
|
425
|
+
const resumeKey = ic.resume_key;
|
|
426
|
+
if (!resumeKey) {
|
|
427
|
+
log("coachmark_tour requires interactive_config.resume_key — skipping", "warn");
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const steps = Array.isArray(ic.steps) ? ic.steps : [];
|
|
431
|
+
if (steps.length === 0) {
|
|
432
|
+
log("coachmark_tour has no steps — skipping", "warn");
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const allowSkip = ic.allow_skip !== false;
|
|
436
|
+
const showDots = ic.show_progress_dots !== false;
|
|
437
|
+
addAnimationStyles();
|
|
438
|
+
const bg = sanitizeColor(campaign.background_color || "#0f172a");
|
|
439
|
+
const fg = sanitizeColor(campaign.text_color || "#ffffff");
|
|
440
|
+
let current = readResumeIdx(resumeKey);
|
|
441
|
+
if (current >= steps.length) {
|
|
442
|
+
log(`coachmark_tour ${resumeKey} already complete — not re-showing`);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
trackEvent(campaign.id, "impression");
|
|
446
|
+
let pointerEl = null;
|
|
447
|
+
let tipEl = null;
|
|
448
|
+
let highlightEl = null;
|
|
449
|
+
const cleanupOne = () => {
|
|
450
|
+
pointerEl == null ? void 0 : pointerEl.remove();
|
|
451
|
+
tipEl == null ? void 0 : tipEl.remove();
|
|
452
|
+
highlightEl == null ? void 0 : highlightEl.remove();
|
|
453
|
+
pointerEl = null;
|
|
454
|
+
tipEl = null;
|
|
455
|
+
highlightEl = null;
|
|
456
|
+
};
|
|
457
|
+
const finish = (opts) => {
|
|
458
|
+
cleanupOne();
|
|
459
|
+
if (opts.skipped) {
|
|
460
|
+
trackEvent(campaign.id, "dismissed");
|
|
461
|
+
} else {
|
|
462
|
+
trackEvent(campaign.id, "clicked");
|
|
463
|
+
}
|
|
464
|
+
writeResumeIdx(resumeKey, steps.length);
|
|
465
|
+
};
|
|
466
|
+
const showStep = (idx) => {
|
|
467
|
+
cleanupOne();
|
|
468
|
+
const step = steps[idx];
|
|
469
|
+
if (!step) {
|
|
470
|
+
finish({ skipped: false });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
writeResumeIdx(resumeKey, idx);
|
|
474
|
+
const selector = step.anchor_web;
|
|
475
|
+
let anchor = null;
|
|
476
|
+
if (selector) {
|
|
477
|
+
try {
|
|
478
|
+
anchor = document.querySelector(selector);
|
|
479
|
+
} catch {
|
|
480
|
+
anchor = null;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (!anchor) {
|
|
484
|
+
log(`coachmark step ${idx} — selector '${selector}' not found; advancing`, "warn");
|
|
485
|
+
showStep(idx + 1);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const rect = anchor.getBoundingClientRect();
|
|
489
|
+
highlightEl = document.createElement("div");
|
|
490
|
+
highlightEl.style.cssText = `
|
|
491
|
+
position: fixed;
|
|
492
|
+
top: ${rect.top - 6}px; left: ${rect.left - 6}px;
|
|
493
|
+
width: ${rect.width + 12}px; height: ${rect.height + 12}px;
|
|
494
|
+
border-radius: 10px; z-index: 999998;
|
|
495
|
+
box-shadow: 0 0 0 2px ${fg}, 0 0 0 9999px rgba(0,0,0,0.4);
|
|
496
|
+
pointer-events: none;
|
|
497
|
+
transition: all 0.2s ease;
|
|
498
|
+
`;
|
|
499
|
+
document.body.appendChild(highlightEl);
|
|
500
|
+
const placement = step.placement || "bottom";
|
|
501
|
+
tipEl = document.createElement("div");
|
|
502
|
+
tipEl.setAttribute("data-campaign-id", campaign.id);
|
|
503
|
+
tipEl.style.cssText = `
|
|
504
|
+
position: fixed; z-index: 999999;
|
|
505
|
+
background: ${bg}; color: ${fg};
|
|
506
|
+
padding: 12px 14px; border-radius: 12px;
|
|
507
|
+
max-width: 260px;
|
|
508
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.25);
|
|
509
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
510
|
+
animation: aegisFadeIn 0.2s ease;
|
|
511
|
+
`;
|
|
512
|
+
const titleEl = document.createElement("div");
|
|
513
|
+
titleEl.textContent = step.title;
|
|
514
|
+
titleEl.style.cssText = "font-weight: 700; font-size: 13px; margin-bottom: 4px;";
|
|
515
|
+
tipEl.appendChild(titleEl);
|
|
516
|
+
const bodyEl = document.createElement("div");
|
|
517
|
+
bodyEl.textContent = step.body;
|
|
518
|
+
bodyEl.style.cssText = "font-size: 12.5px; line-height: 1.4; opacity: 0.9;";
|
|
519
|
+
tipEl.appendChild(bodyEl);
|
|
520
|
+
const controls = document.createElement("div");
|
|
521
|
+
controls.style.cssText = "display: flex; align-items: center; justify-content: space-between; margin-top: 10px; gap: 8px;";
|
|
522
|
+
if (showDots) {
|
|
523
|
+
const dots = document.createElement("div");
|
|
524
|
+
dots.style.cssText = "display: flex; gap: 4px;";
|
|
525
|
+
steps.forEach((_, i) => {
|
|
526
|
+
const d = document.createElement("span");
|
|
527
|
+
d.style.cssText = `
|
|
528
|
+
width: 5px; height: 5px; border-radius: 999px;
|
|
529
|
+
background: ${fg}; opacity: ${i === idx ? "0.95" : "0.3"};
|
|
530
|
+
`;
|
|
531
|
+
dots.appendChild(d);
|
|
532
|
+
});
|
|
533
|
+
controls.appendChild(dots);
|
|
534
|
+
} else {
|
|
535
|
+
controls.appendChild(document.createElement("span"));
|
|
536
|
+
}
|
|
537
|
+
const buttons = document.createElement("div");
|
|
538
|
+
buttons.style.cssText = "display: flex; gap: 6px;";
|
|
539
|
+
if (allowSkip && idx < steps.length - 1) {
|
|
540
|
+
const skip = document.createElement("button");
|
|
541
|
+
skip.textContent = "Skip";
|
|
542
|
+
skip.style.cssText = `
|
|
543
|
+
background: transparent; color: inherit; opacity: 0.7;
|
|
544
|
+
border: none; font-size: 12px; cursor: pointer; padding: 6px 8px;
|
|
545
|
+
`;
|
|
546
|
+
skip.addEventListener("click", () => finish({ skipped: true }));
|
|
547
|
+
buttons.appendChild(skip);
|
|
548
|
+
}
|
|
549
|
+
const next = document.createElement("button");
|
|
550
|
+
const isLast = idx === steps.length - 1;
|
|
551
|
+
next.textContent = step.cta_text || (isLast ? "Done" : "Next");
|
|
552
|
+
next.style.cssText = `
|
|
553
|
+
background: ${fg}; color: ${bg};
|
|
554
|
+
border: none; padding: 6px 12px; border-radius: 999px;
|
|
555
|
+
font-size: 12px; font-weight: 700; cursor: pointer;
|
|
556
|
+
`;
|
|
557
|
+
next.addEventListener("click", () => {
|
|
558
|
+
if (isLast) {
|
|
559
|
+
finish({ skipped: false });
|
|
560
|
+
} else {
|
|
561
|
+
showStep(idx + 1);
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
buttons.appendChild(next);
|
|
565
|
+
controls.appendChild(buttons);
|
|
566
|
+
tipEl.appendChild(controls);
|
|
567
|
+
document.body.appendChild(tipEl);
|
|
568
|
+
const tipRect = tipEl.getBoundingClientRect();
|
|
569
|
+
const margin = 12;
|
|
570
|
+
let top = rect.bottom + margin;
|
|
571
|
+
let left = rect.left;
|
|
572
|
+
if (placement === "top") {
|
|
573
|
+
top = rect.top - tipRect.height - margin;
|
|
574
|
+
} else if (placement === "left") {
|
|
575
|
+
top = rect.top;
|
|
576
|
+
left = rect.left - tipRect.width - margin;
|
|
577
|
+
} else if (placement === "right") {
|
|
578
|
+
top = rect.top;
|
|
579
|
+
left = rect.right + margin;
|
|
580
|
+
}
|
|
581
|
+
top = Math.max(8, Math.min(window.innerHeight - tipRect.height - 8, top));
|
|
582
|
+
left = Math.max(8, Math.min(window.innerWidth - tipRect.width - 8, left));
|
|
583
|
+
tipEl.style.top = `${top}px`;
|
|
584
|
+
tipEl.style.left = `${left}px`;
|
|
585
|
+
};
|
|
586
|
+
showStep(current);
|
|
587
|
+
window.aegisResetTour = (key) => clearResume(key);
|
|
588
|
+
}
|
|
589
|
+
const MAX_PRODUCTS = 24;
|
|
590
|
+
function renderProductRecommendation(ctx) {
|
|
591
|
+
const { campaign, trackEvent, sanitizeUrl, sanitizeColor, log, addAnimationStyles } = ctx;
|
|
592
|
+
const ic = campaign.interactive_config || {};
|
|
593
|
+
const rawCards = Array.isArray(ic.cards) ? ic.cards : [];
|
|
594
|
+
const cards = rawCards.slice(0, MAX_PRODUCTS);
|
|
595
|
+
if (cards.length === 0) {
|
|
596
|
+
log("product_recommendation rendered with zero products — skipping", "warn");
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const layout = ic.rec_layout || "grid";
|
|
600
|
+
const ctaDefault = ic.rec_cta_text || "Shop now";
|
|
601
|
+
addAnimationStyles();
|
|
602
|
+
const bg = sanitizeColor(campaign.background_color || "#ffffff");
|
|
603
|
+
const fg = sanitizeColor(campaign.text_color || "#0f172a");
|
|
604
|
+
const accent = sanitizeColor("#4169e1");
|
|
605
|
+
const overlay = document.createElement("div");
|
|
606
|
+
overlay.className = "aegis-in-app-product-rec";
|
|
607
|
+
overlay.setAttribute("data-campaign-id", campaign.id);
|
|
608
|
+
overlay.style.cssText = `
|
|
609
|
+
position: fixed; inset: 0;
|
|
610
|
+
background: rgba(15,23,42,0.55); backdrop-filter: blur(4px);
|
|
611
|
+
display: flex; align-items: flex-end; justify-content: center;
|
|
612
|
+
z-index: 99999; animation: aegisFadeIn 0.25s ease;
|
|
613
|
+
`;
|
|
614
|
+
const sheet = document.createElement("div");
|
|
615
|
+
sheet.style.cssText = `
|
|
616
|
+
background: ${bg}; color: ${fg};
|
|
617
|
+
max-width: 560px; width: 100%; max-height: 80vh; overflow-y: auto;
|
|
618
|
+
border-radius: 20px 20px 0 0;
|
|
619
|
+
padding: 18px 16px 22px;
|
|
620
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
621
|
+
animation: aegisSlideInFromBottom 0.3s ease-out;
|
|
622
|
+
box-shadow: 0 -12px 30px rgba(0,0,0,0.12);
|
|
623
|
+
`;
|
|
624
|
+
const handle = document.createElement("div");
|
|
625
|
+
handle.style.cssText = `
|
|
626
|
+
width: 36px; height: 4px; border-radius: 999px; background: ${fg}1f;
|
|
627
|
+
margin: 0 auto 12px;
|
|
628
|
+
`;
|
|
629
|
+
sheet.appendChild(handle);
|
|
630
|
+
const header = document.createElement("div");
|
|
631
|
+
header.style.cssText = "display: flex; justify-content: space-between; align-items: flex-start; gap: 12px;";
|
|
632
|
+
const headerText = document.createElement("div");
|
|
633
|
+
const title = document.createElement("div");
|
|
634
|
+
title.textContent = campaign.title;
|
|
635
|
+
title.style.cssText = "font-weight: 700; font-size: 16px; margin-bottom: 2px;";
|
|
636
|
+
const body = document.createElement("div");
|
|
637
|
+
body.textContent = campaign.body;
|
|
638
|
+
body.style.cssText = "font-size: 13px; opacity: 0.7; line-height: 1.4;";
|
|
639
|
+
headerText.appendChild(title);
|
|
640
|
+
headerText.appendChild(body);
|
|
641
|
+
header.appendChild(headerText);
|
|
642
|
+
const close = document.createElement("button");
|
|
643
|
+
close.textContent = "✕";
|
|
644
|
+
close.setAttribute("aria-label", "Close");
|
|
645
|
+
close.style.cssText = `
|
|
646
|
+
background: transparent; border: none; color: inherit;
|
|
647
|
+
font-size: 18px; cursor: pointer; padding: 4px 8px; opacity: 0.7;
|
|
648
|
+
`;
|
|
649
|
+
close.addEventListener("click", () => {
|
|
650
|
+
trackEvent(campaign.id, "dismissed");
|
|
651
|
+
overlay.remove();
|
|
652
|
+
});
|
|
653
|
+
header.appendChild(close);
|
|
654
|
+
sheet.appendChild(header);
|
|
655
|
+
const grid = document.createElement("div");
|
|
656
|
+
if (layout === "row" || layout === "carousel") {
|
|
657
|
+
grid.style.cssText = `
|
|
658
|
+
display: flex; gap: 10px; overflow-x: auto;
|
|
659
|
+
scroll-snap-type: x mandatory; padding: 14px 0 4px;
|
|
660
|
+
`;
|
|
661
|
+
} else {
|
|
662
|
+
grid.style.cssText = `
|
|
663
|
+
display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;
|
|
664
|
+
padding-top: 14px;
|
|
665
|
+
`;
|
|
666
|
+
}
|
|
667
|
+
cards.forEach((c) => {
|
|
668
|
+
const tile = document.createElement("div");
|
|
669
|
+
const isRow = layout === "row" || layout === "carousel";
|
|
670
|
+
tile.style.cssText = `
|
|
671
|
+
background: ${fg}08; border-radius: 12px;
|
|
672
|
+
padding: 10px; display: flex; flex-direction: column; gap: 6px;
|
|
673
|
+
cursor: ${c.cta_url ? "pointer" : "default"};
|
|
674
|
+
${isRow ? "flex: 0 0 150px; scroll-snap-align: start;" : ""}
|
|
675
|
+
transition: transform 0.15s ease;
|
|
676
|
+
`;
|
|
677
|
+
if (c.image_url) {
|
|
678
|
+
const img = document.createElement("img");
|
|
679
|
+
const safe = sanitizeUrl(c.image_url);
|
|
680
|
+
if (safe) {
|
|
681
|
+
img.src = safe;
|
|
682
|
+
img.alt = "";
|
|
683
|
+
img.loading = "lazy";
|
|
684
|
+
img.style.cssText = "width: 100%; aspect-ratio: 1 / 1; border-radius: 8px; object-fit: cover;";
|
|
685
|
+
tile.appendChild(img);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (c.title) {
|
|
689
|
+
const t = document.createElement("div");
|
|
690
|
+
t.textContent = c.title;
|
|
691
|
+
t.style.cssText = "font-weight: 600; font-size: 13px; line-height: 1.3;";
|
|
692
|
+
tile.appendChild(t);
|
|
693
|
+
}
|
|
694
|
+
const price = c.metadata && typeof c.metadata === "object" ? c.metadata.price : void 0;
|
|
695
|
+
if (price !== void 0) {
|
|
696
|
+
const p = document.createElement("div");
|
|
697
|
+
p.textContent = String(price);
|
|
698
|
+
p.style.cssText = `color: ${accent}; font-weight: 700; font-size: 13px;`;
|
|
699
|
+
tile.appendChild(p);
|
|
700
|
+
} else if (c.body) {
|
|
701
|
+
const b = document.createElement("div");
|
|
702
|
+
b.textContent = c.body;
|
|
703
|
+
b.style.cssText = "font-size: 11.5px; opacity: 0.72; line-height: 1.3;";
|
|
704
|
+
tile.appendChild(b);
|
|
705
|
+
}
|
|
706
|
+
const cta = document.createElement("button");
|
|
707
|
+
cta.textContent = c.cta_text || ctaDefault;
|
|
708
|
+
cta.style.cssText = `
|
|
709
|
+
margin-top: auto;
|
|
710
|
+
background: ${accent}; color: #fff;
|
|
711
|
+
border: none; padding: 7px 10px; border-radius: 999px;
|
|
712
|
+
font-size: 12px; font-weight: 600; cursor: pointer;
|
|
713
|
+
`;
|
|
714
|
+
const go = (e) => {
|
|
715
|
+
e.stopPropagation();
|
|
716
|
+
trackEvent(campaign.id, "clicked");
|
|
717
|
+
if (c.cta_url) {
|
|
718
|
+
const safe = sanitizeUrl(c.cta_url);
|
|
719
|
+
if (safe) window.location.href = safe;
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
cta.addEventListener("click", go);
|
|
723
|
+
tile.appendChild(cta);
|
|
724
|
+
if (c.cta_url) tile.addEventListener("click", go);
|
|
725
|
+
grid.appendChild(tile);
|
|
726
|
+
});
|
|
727
|
+
sheet.appendChild(grid);
|
|
728
|
+
overlay.appendChild(sheet);
|
|
729
|
+
overlay.addEventListener("click", (e) => {
|
|
730
|
+
if (e.target === overlay) {
|
|
731
|
+
trackEvent(campaign.id, "dismissed");
|
|
732
|
+
overlay.remove();
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
document.body.appendChild(overlay);
|
|
736
|
+
}
|
|
28
737
|
class AegisInAppManager {
|
|
29
738
|
constructor(config) {
|
|
30
739
|
this.campaigns = [];
|
|
@@ -213,11 +922,60 @@ class AegisInAppManager {
|
|
|
213
922
|
return assignments[campaignId] ?? void 0;
|
|
214
923
|
}
|
|
215
924
|
tryDisplayNextCampaign() {
|
|
216
|
-
const campaign = this.campaigns.find(
|
|
925
|
+
const campaign = this.campaigns.find(
|
|
926
|
+
(c) => !this.displayedCampaigns.has(c.id) && !c.client_trigger
|
|
927
|
+
);
|
|
217
928
|
if (campaign) {
|
|
218
929
|
this.displayCampaign(campaign);
|
|
219
930
|
}
|
|
220
931
|
}
|
|
932
|
+
/**
|
|
933
|
+
* Evaluate the currently armed campaigns against a client-side event
|
|
934
|
+
* and render any that match their `client_trigger`.
|
|
935
|
+
*
|
|
936
|
+
* Called by the host app (e.g., the EcommerceTracker on product_viewed),
|
|
937
|
+
* or by future TriggerEngine bridges. Safe to call repeatedly for the
|
|
938
|
+
* same event — dedup happens via `displayedCampaigns`.
|
|
939
|
+
*
|
|
940
|
+
* Supported trigger types (align with the cell-plane server-side
|
|
941
|
+
* `display_rules.trigger_type`):
|
|
942
|
+
* - `custom_event` : fire when eventName matches config.event
|
|
943
|
+
* - `product_match` : fire on `product_viewed` when eventData.product_id
|
|
944
|
+
* matches config.product_id (string or string[])
|
|
945
|
+
* - `delay` : client-side setTimeout — evaluated at armeng
|
|
946
|
+
* time (kicked off from `displayCampaign`), not here.
|
|
947
|
+
*/
|
|
948
|
+
onClientEvent(eventName, eventData = {}) {
|
|
949
|
+
for (const c of this.campaigns) {
|
|
950
|
+
if (this.displayedCampaigns.has(c.id)) continue;
|
|
951
|
+
if (!c.client_trigger) continue;
|
|
952
|
+
if (this.matchesClientTrigger(c.client_trigger, eventName, eventData)) {
|
|
953
|
+
this.displayCampaign(c);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
matchesClientTrigger(trigger, eventName, eventData) {
|
|
958
|
+
var _a;
|
|
959
|
+
const cfg = trigger.config || {};
|
|
960
|
+
switch (trigger.type) {
|
|
961
|
+
case "custom_event":
|
|
962
|
+
return typeof cfg.event === "string" && cfg.event === eventName;
|
|
963
|
+
case "product_match": {
|
|
964
|
+
if (eventName !== "product_viewed" && eventName !== "product_view") {
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
const wantedRaw = cfg.product_id;
|
|
968
|
+
const wanted = Array.isArray(wantedRaw) ? wantedRaw : typeof wantedRaw === "string" ? [wantedRaw] : [];
|
|
969
|
+
if (wanted.length === 0) return false;
|
|
970
|
+
const actual = String(
|
|
971
|
+
eventData.product_id ?? eventData.productId ?? ((_a = eventData.product) == null ? void 0 : _a.id) ?? ""
|
|
972
|
+
);
|
|
973
|
+
return wanted.includes(actual);
|
|
974
|
+
}
|
|
975
|
+
default:
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
221
979
|
displayCampaign(campaign) {
|
|
222
980
|
this.displayedCampaigns.add(campaign.id);
|
|
223
981
|
const interactiveSubTypes = /* @__PURE__ */ new Set([
|
|
@@ -256,9 +1014,49 @@ class AegisInAppManager {
|
|
|
256
1014
|
case "tooltip":
|
|
257
1015
|
this.renderTooltip(campaign);
|
|
258
1016
|
break;
|
|
1017
|
+
// Preload-first display types (2026-04-22). These renderers own their
|
|
1018
|
+
// own impression tracking so we don't double-count alongside the
|
|
1019
|
+
// trailing `this.trackEvent(... 'impression')` below — return early.
|
|
1020
|
+
case "carousel_cards":
|
|
1021
|
+
renderCarouselCards(this.buildRenderContext(campaign));
|
|
1022
|
+
this.trackEvent(campaign.id, "impression");
|
|
1023
|
+
return;
|
|
1024
|
+
case "sticky_bar":
|
|
1025
|
+
renderStickyBar(this.buildRenderContext(campaign));
|
|
1026
|
+
this.trackEvent(campaign.id, "impression");
|
|
1027
|
+
return;
|
|
1028
|
+
case "progress_bar":
|
|
1029
|
+
renderProgressBar(this.buildRenderContext(campaign));
|
|
1030
|
+
this.trackEvent(campaign.id, "impression");
|
|
1031
|
+
return;
|
|
1032
|
+
case "coachmark_tour":
|
|
1033
|
+
renderCoachmarkTour(this.buildRenderContext(campaign));
|
|
1034
|
+
return;
|
|
1035
|
+
case "product_recommendation":
|
|
1036
|
+
renderProductRecommendation(this.buildRenderContext(campaign));
|
|
1037
|
+
this.trackEvent(campaign.id, "impression");
|
|
1038
|
+
return;
|
|
259
1039
|
}
|
|
260
1040
|
this.trackEvent(campaign.id, "impression");
|
|
261
1041
|
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Build the shared context passed into the preload-first renderers.
|
|
1044
|
+
* Matches the interface in `./renderers/types.ts`. Kept as a private
|
|
1045
|
+
* method (rather than inlined at each call-site) so future renderer
|
|
1046
|
+
* additions stay consistent and easy to audit.
|
|
1047
|
+
*/
|
|
1048
|
+
buildRenderContext(campaign) {
|
|
1049
|
+
return {
|
|
1050
|
+
campaign,
|
|
1051
|
+
trackEvent: (id, evt) => {
|
|
1052
|
+
void this.trackEvent(id, evt);
|
|
1053
|
+
},
|
|
1054
|
+
sanitizeUrl: (url) => this.sanitizeUrl(url),
|
|
1055
|
+
sanitizeColor: (color) => this.sanitizeColor(color),
|
|
1056
|
+
log: (msg, level) => this.log(msg, level),
|
|
1057
|
+
addAnimationStyles: () => this.addAnimationStyles()
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
262
1060
|
/**
|
|
263
1061
|
* Renders interactive sub-type campaigns (spin wheel, NPS, quiz, etc.)
|
|
264
1062
|
* using DOM-safe rendering. These sub-types use the campaign's
|
|
@@ -1341,6 +2139,14 @@ class AegisInAppManager {
|
|
|
1341
2139
|
from { transform: scale(0.9); opacity: 0; }
|
|
1342
2140
|
to { transform: scale(1); opacity: 1; }
|
|
1343
2141
|
}
|
|
2142
|
+
|
|
2143
|
+
/* Bottom-anchored slide IN (opposite of aegisSlideUp which slides OUT).
|
|
2144
|
+
Used by the preload-first renderers: sticky_bar (bottom),
|
|
2145
|
+
progress_bar, carousel_cards, product_recommendation. */
|
|
2146
|
+
@keyframes aegisSlideInFromBottom {
|
|
2147
|
+
from { transform: translateY(100%); opacity: 0; }
|
|
2148
|
+
to { transform: translateY(0); opacity: 1; }
|
|
2149
|
+
}
|
|
1344
2150
|
`;
|
|
1345
2151
|
document.head.appendChild(style);
|
|
1346
2152
|
}
|
|
@@ -1401,6 +2207,10 @@ class AegisInAppManager {
|
|
|
1401
2207
|
event_type: eventType,
|
|
1402
2208
|
user_id: this.userId,
|
|
1403
2209
|
contact_id: this.contactId,
|
|
2210
|
+
// Property ID so the cell-plane's in_app.rendered emitter can
|
|
2211
|
+
// fan the event out to the property-scoped inbox_writer. Without
|
|
2212
|
+
// this, inbox materialization cannot target the right property.
|
|
2213
|
+
property_id: this.propertyId,
|
|
1404
2214
|
platform: "web",
|
|
1405
2215
|
variant_id: this.getVariantId(campaignId) ?? void 0
|
|
1406
2216
|
},
|
|
@@ -4351,6 +5161,572 @@ class SdkConfigPoller {
|
|
|
4351
5161
|
}
|
|
4352
5162
|
}
|
|
4353
5163
|
}
|
|
5164
|
+
const DEFAULT_POLL_MS = 3e5;
|
|
5165
|
+
class PrefetchBundleClient {
|
|
5166
|
+
constructor(options) {
|
|
5167
|
+
this.currentETag = null;
|
|
5168
|
+
this.currentBundle = null;
|
|
5169
|
+
this.pollTimer = null;
|
|
5170
|
+
this.listeners = [];
|
|
5171
|
+
this.inflightRefetch = null;
|
|
5172
|
+
this.apiHost = options.apiHost;
|
|
5173
|
+
this.writeKey = options.writeKey;
|
|
5174
|
+
this.organizationId = options.organizationId;
|
|
5175
|
+
this.contactId = options.contactId;
|
|
5176
|
+
this.userId = options.userId;
|
|
5177
|
+
this.propertyId = options.propertyId;
|
|
5178
|
+
this.abAssignments = options.abAssignments ?? {};
|
|
5179
|
+
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_MS;
|
|
5180
|
+
this.contextProvider = options.contextProvider ?? (() => ({}));
|
|
5181
|
+
}
|
|
5182
|
+
/**
|
|
5183
|
+
* Initial fetch + start the background ETag poll. Resolves to the bundle
|
|
5184
|
+
* (or `null` if the first fetch failed — callers should fall back to an
|
|
5185
|
+
* empty campaign list, not abort SDK init).
|
|
5186
|
+
*/
|
|
5187
|
+
async start() {
|
|
5188
|
+
const bundle = await this.fetch();
|
|
5189
|
+
this.pollTimer = setInterval(() => {
|
|
5190
|
+
this.fetch().catch((err) => logger.warn("prefetch-bundle poll failed:", err));
|
|
5191
|
+
}, this.pollIntervalMs);
|
|
5192
|
+
return bundle;
|
|
5193
|
+
}
|
|
5194
|
+
stop() {
|
|
5195
|
+
if (this.pollTimer) {
|
|
5196
|
+
clearInterval(this.pollTimer);
|
|
5197
|
+
this.pollTimer = null;
|
|
5198
|
+
}
|
|
5199
|
+
}
|
|
5200
|
+
getBundle() {
|
|
5201
|
+
return this.currentBundle;
|
|
5202
|
+
}
|
|
5203
|
+
getCampaigns() {
|
|
5204
|
+
var _a;
|
|
5205
|
+
return ((_a = this.currentBundle) == null ? void 0 : _a.campaigns) ?? [];
|
|
5206
|
+
}
|
|
5207
|
+
getInbox() {
|
|
5208
|
+
var _a;
|
|
5209
|
+
return ((_a = this.currentBundle) == null ? void 0 : _a.inbox) ?? { unread_count: 0, page: [], cursor: null };
|
|
5210
|
+
}
|
|
5211
|
+
/** Force a refresh (dedupes concurrent calls). Exposed so inbox mutations
|
|
5212
|
+
* and campaign-activation SSE messages can trigger a refetch on demand. */
|
|
5213
|
+
async refresh() {
|
|
5214
|
+
if (this.inflightRefetch) return this.inflightRefetch;
|
|
5215
|
+
this.inflightRefetch = this.fetch();
|
|
5216
|
+
try {
|
|
5217
|
+
return await this.inflightRefetch;
|
|
5218
|
+
} finally {
|
|
5219
|
+
this.inflightRefetch = null;
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5222
|
+
/** Subscribe to bundle changes (emits on first fetch + on any content
|
|
5223
|
+
* change — ETag 304 does NOT fire the listener). Returns an unsubscribe. */
|
|
5224
|
+
onChange(listener) {
|
|
5225
|
+
this.listeners.push(listener);
|
|
5226
|
+
return () => {
|
|
5227
|
+
const idx = this.listeners.indexOf(listener);
|
|
5228
|
+
if (idx >= 0) this.listeners.splice(idx, 1);
|
|
5229
|
+
};
|
|
5230
|
+
}
|
|
5231
|
+
/**
|
|
5232
|
+
* Update the identity tuple — called by AegisInAppManager when the host
|
|
5233
|
+
* app resolves a contactId post-init (e.g., after login). Triggers an
|
|
5234
|
+
* immediate refresh since the eligible campaigns + inbox will differ.
|
|
5235
|
+
*/
|
|
5236
|
+
updateIdentity(partial) {
|
|
5237
|
+
let changed = false;
|
|
5238
|
+
if (partial.contactId !== void 0 && partial.contactId !== this.contactId) {
|
|
5239
|
+
this.contactId = partial.contactId;
|
|
5240
|
+
changed = true;
|
|
5241
|
+
}
|
|
5242
|
+
if (partial.userId !== void 0 && partial.userId !== this.userId) {
|
|
5243
|
+
this.userId = partial.userId;
|
|
5244
|
+
changed = true;
|
|
5245
|
+
}
|
|
5246
|
+
if (partial.organizationId !== void 0 && partial.organizationId !== this.organizationId) {
|
|
5247
|
+
this.organizationId = partial.organizationId;
|
|
5248
|
+
changed = true;
|
|
5249
|
+
}
|
|
5250
|
+
if (partial.propertyId !== void 0 && partial.propertyId !== this.propertyId) {
|
|
5251
|
+
this.propertyId = partial.propertyId;
|
|
5252
|
+
changed = true;
|
|
5253
|
+
}
|
|
5254
|
+
if (changed) {
|
|
5255
|
+
this.currentETag = null;
|
|
5256
|
+
this.refresh().catch(
|
|
5257
|
+
(err) => logger.warn("prefetch-bundle identity-refresh failed:", err)
|
|
5258
|
+
);
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
/** Update cached AB assignments. Called by downstream renderers after
|
|
5262
|
+
* they display a variant. Persisted to localStorage by higher layers. */
|
|
5263
|
+
setAbAssignments(assignments) {
|
|
5264
|
+
this.abAssignments = { ...assignments };
|
|
5265
|
+
}
|
|
5266
|
+
// ── internals ──────────────────────────────────────────────────────────
|
|
5267
|
+
async fetch() {
|
|
5268
|
+
const ctx = this.contextProvider();
|
|
5269
|
+
const qs = new URLSearchParams();
|
|
5270
|
+
if (ctx.device_type) qs.set("device_type", ctx.device_type);
|
|
5271
|
+
if (ctx.page_url) qs.set("page_url", ctx.page_url);
|
|
5272
|
+
if (ctx.geo) qs.set("geo", ctx.geo);
|
|
5273
|
+
qs.set("is_new_user", ctx.is_new_user ? "true" : "false");
|
|
5274
|
+
const url = `${this.apiHost}/v1/sdk/prefetch-bundle?${qs.toString()}`;
|
|
5275
|
+
const headers = {
|
|
5276
|
+
"X-Aegis-Write-Key": this.writeKey,
|
|
5277
|
+
Accept: "application/json"
|
|
5278
|
+
};
|
|
5279
|
+
if (this.organizationId) headers["X-Organization-ID"] = this.organizationId;
|
|
5280
|
+
if (this.contactId) headers["X-Contact-ID"] = this.contactId;
|
|
5281
|
+
if (this.userId) headers["X-User-ID"] = this.userId;
|
|
5282
|
+
if (this.propertyId) headers["X-Property-Id"] = this.propertyId;
|
|
5283
|
+
if (this.currentETag) headers["If-None-Match"] = this.currentETag;
|
|
5284
|
+
if (Object.keys(this.abAssignments).length > 0) {
|
|
5285
|
+
try {
|
|
5286
|
+
headers["X-AB-Assignments"] = btoa(JSON.stringify(this.abAssignments));
|
|
5287
|
+
} catch {
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
5290
|
+
let response;
|
|
5291
|
+
try {
|
|
5292
|
+
response = await fetch(url, { method: "GET", headers });
|
|
5293
|
+
} catch (err) {
|
|
5294
|
+
logger.warn("prefetch-bundle fetch network error:", err);
|
|
5295
|
+
return this.currentBundle;
|
|
5296
|
+
}
|
|
5297
|
+
if (response.status === 304) {
|
|
5298
|
+
return this.currentBundle;
|
|
5299
|
+
}
|
|
5300
|
+
if (!response.ok) {
|
|
5301
|
+
logger.warn(`prefetch-bundle fetch failed: HTTP ${response.status}`);
|
|
5302
|
+
return this.currentBundle;
|
|
5303
|
+
}
|
|
5304
|
+
const etag = response.headers.get("ETag");
|
|
5305
|
+
let bundle;
|
|
5306
|
+
try {
|
|
5307
|
+
bundle = await response.json();
|
|
5308
|
+
} catch (err) {
|
|
5309
|
+
logger.warn("prefetch-bundle invalid JSON:", err);
|
|
5310
|
+
return this.currentBundle;
|
|
5311
|
+
}
|
|
5312
|
+
if (etag) this.currentETag = etag;
|
|
5313
|
+
this.currentBundle = bundle;
|
|
5314
|
+
for (const l of this.listeners) {
|
|
5315
|
+
try {
|
|
5316
|
+
l(bundle);
|
|
5317
|
+
} catch (err) {
|
|
5318
|
+
logger.warn("prefetch-bundle listener threw:", err);
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5321
|
+
return bundle;
|
|
5322
|
+
}
|
|
5323
|
+
}
|
|
5324
|
+
const DEFAULT_UNREAD_POLL = 6e4;
|
|
5325
|
+
const IDB_NAME = "aegis-inbox";
|
|
5326
|
+
const IDB_STORE = "messages";
|
|
5327
|
+
const IDB_VERSION = 1;
|
|
5328
|
+
class AegisInbox {
|
|
5329
|
+
constructor(config) {
|
|
5330
|
+
this.entries = [];
|
|
5331
|
+
this.unreadCount = 0;
|
|
5332
|
+
this.cursor = null;
|
|
5333
|
+
this.listeners = [];
|
|
5334
|
+
this.pollTimer = null;
|
|
5335
|
+
this.bundleUnsub = null;
|
|
5336
|
+
this.apiHost = config.apiHost;
|
|
5337
|
+
this.writeKey = config.writeKey;
|
|
5338
|
+
this.organizationId = config.organizationId;
|
|
5339
|
+
this.contactId = config.contactId;
|
|
5340
|
+
this.propertyId = config.propertyId;
|
|
5341
|
+
this.bundleClient = config.bundleClient;
|
|
5342
|
+
this.unreadPollIntervalMs = config.unreadPollIntervalMs ?? DEFAULT_UNREAD_POLL;
|
|
5343
|
+
}
|
|
5344
|
+
/** Hydrate from bundle (if available), restore the IDB cache (if any),
|
|
5345
|
+
* then start the background unread-count poll. */
|
|
5346
|
+
async initialize() {
|
|
5347
|
+
if (!this.contactId || !this.propertyId) {
|
|
5348
|
+
logger.warn("AegisInbox: no contact/property yet — staying dormant");
|
|
5349
|
+
return;
|
|
5350
|
+
}
|
|
5351
|
+
const cached = await this.readCache();
|
|
5352
|
+
if (cached.length > 0) {
|
|
5353
|
+
this.entries = cached;
|
|
5354
|
+
this.recomputeUnread();
|
|
5355
|
+
this.emit();
|
|
5356
|
+
}
|
|
5357
|
+
if (this.bundleClient) {
|
|
5358
|
+
const inbox = this.bundleClient.getInbox();
|
|
5359
|
+
this.seedFromBundle(inbox.page, inbox.unread_count);
|
|
5360
|
+
this.bundleUnsub = this.bundleClient.onChange((bundle) => {
|
|
5361
|
+
this.seedFromBundle(bundle.inbox.page, bundle.inbox.unread_count);
|
|
5362
|
+
});
|
|
5363
|
+
} else {
|
|
5364
|
+
await this.refreshList();
|
|
5365
|
+
}
|
|
5366
|
+
this.pollTimer = setInterval(() => {
|
|
5367
|
+
this.refreshUnreadCount().catch(
|
|
5368
|
+
(err) => logger.warn("inbox unread-count poll failed:", err)
|
|
5369
|
+
);
|
|
5370
|
+
}, this.unreadPollIntervalMs);
|
|
5371
|
+
}
|
|
5372
|
+
stop() {
|
|
5373
|
+
if (this.pollTimer) {
|
|
5374
|
+
clearInterval(this.pollTimer);
|
|
5375
|
+
this.pollTimer = null;
|
|
5376
|
+
}
|
|
5377
|
+
if (this.bundleUnsub) {
|
|
5378
|
+
this.bundleUnsub();
|
|
5379
|
+
this.bundleUnsub = null;
|
|
5380
|
+
}
|
|
5381
|
+
}
|
|
5382
|
+
/** Update identity after login / property switch. Clears the local
|
|
5383
|
+
* inbox since it's intrinsically contact+property scoped. */
|
|
5384
|
+
setIdentity(partial) {
|
|
5385
|
+
let changed = false;
|
|
5386
|
+
if (partial.contactId !== void 0 && partial.contactId !== this.contactId) {
|
|
5387
|
+
this.contactId = partial.contactId;
|
|
5388
|
+
changed = true;
|
|
5389
|
+
}
|
|
5390
|
+
if (partial.propertyId !== void 0 && partial.propertyId !== this.propertyId) {
|
|
5391
|
+
this.propertyId = partial.propertyId;
|
|
5392
|
+
changed = true;
|
|
5393
|
+
}
|
|
5394
|
+
if (partial.organizationId !== void 0 && partial.organizationId !== this.organizationId) {
|
|
5395
|
+
this.organizationId = partial.organizationId;
|
|
5396
|
+
changed = true;
|
|
5397
|
+
}
|
|
5398
|
+
if (changed) {
|
|
5399
|
+
this.entries = [];
|
|
5400
|
+
this.unreadCount = 0;
|
|
5401
|
+
this.cursor = null;
|
|
5402
|
+
this.emit();
|
|
5403
|
+
void this.clearCache();
|
|
5404
|
+
void this.refreshList();
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
// ── Read-side API ────────────────────────────────────────────────────
|
|
5408
|
+
getEntries() {
|
|
5409
|
+
return this.entries;
|
|
5410
|
+
}
|
|
5411
|
+
getUnreadCount() {
|
|
5412
|
+
return this.unreadCount;
|
|
5413
|
+
}
|
|
5414
|
+
/** Pagination cursor (ISO-8601 timestamp of the oldest entry currently
|
|
5415
|
+
* held). Pass it back via `loadMore()` to fetch older history. */
|
|
5416
|
+
getCursor() {
|
|
5417
|
+
return this.cursor;
|
|
5418
|
+
}
|
|
5419
|
+
/** Append one more page of older entries using the current cursor. */
|
|
5420
|
+
async loadMore() {
|
|
5421
|
+
if (!this.contactId || !this.propertyId || !this.cursor) return;
|
|
5422
|
+
try {
|
|
5423
|
+
const url = `${this.apiHost}/v1/in-app/inbox?limit=20&cursor=${encodeURIComponent(this.cursor)}`;
|
|
5424
|
+
const resp = await fetch(url, { headers: this.headers() });
|
|
5425
|
+
if (!resp.ok) return;
|
|
5426
|
+
const body = await resp.json();
|
|
5427
|
+
const byId = new Set(this.entries.map((e) => e.id));
|
|
5428
|
+
for (const e of body.entries) {
|
|
5429
|
+
if (!byId.has(e.id)) this.entries.push(e);
|
|
5430
|
+
}
|
|
5431
|
+
this.cursor = body.cursor;
|
|
5432
|
+
void this.writeCache();
|
|
5433
|
+
this.emit();
|
|
5434
|
+
} catch (err) {
|
|
5435
|
+
logger.warn("inbox loadMore failed:", err);
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
5438
|
+
onChange(l) {
|
|
5439
|
+
this.listeners.push(l);
|
|
5440
|
+
return () => {
|
|
5441
|
+
const i = this.listeners.indexOf(l);
|
|
5442
|
+
if (i >= 0) this.listeners.splice(i, 1);
|
|
5443
|
+
};
|
|
5444
|
+
}
|
|
5445
|
+
async refreshList() {
|
|
5446
|
+
if (!this.contactId || !this.propertyId) return;
|
|
5447
|
+
try {
|
|
5448
|
+
const resp = await fetch(
|
|
5449
|
+
`${this.apiHost}/v1/in-app/inbox?limit=20`,
|
|
5450
|
+
{ headers: this.headers() }
|
|
5451
|
+
);
|
|
5452
|
+
if (!resp.ok) {
|
|
5453
|
+
logger.warn(`inbox list fetch failed: HTTP ${resp.status}`);
|
|
5454
|
+
return;
|
|
5455
|
+
}
|
|
5456
|
+
const body = await resp.json();
|
|
5457
|
+
this.entries = body.entries;
|
|
5458
|
+
this.unreadCount = body.unread_count;
|
|
5459
|
+
this.cursor = body.cursor;
|
|
5460
|
+
void this.writeCache();
|
|
5461
|
+
this.emit();
|
|
5462
|
+
} catch (err) {
|
|
5463
|
+
logger.warn("inbox list fetch error:", err);
|
|
5464
|
+
}
|
|
5465
|
+
}
|
|
5466
|
+
async refreshUnreadCount() {
|
|
5467
|
+
if (!this.contactId || !this.propertyId) return;
|
|
5468
|
+
try {
|
|
5469
|
+
const resp = await fetch(
|
|
5470
|
+
`${this.apiHost}/v1/in-app/inbox/unread-count`,
|
|
5471
|
+
{ headers: this.headers() }
|
|
5472
|
+
);
|
|
5473
|
+
if (!resp.ok) return;
|
|
5474
|
+
const body = await resp.json();
|
|
5475
|
+
if (body.unread_count !== this.unreadCount) {
|
|
5476
|
+
this.unreadCount = body.unread_count;
|
|
5477
|
+
this.emit();
|
|
5478
|
+
if (this.unreadCount > 0) {
|
|
5479
|
+
void this.refreshList();
|
|
5480
|
+
}
|
|
5481
|
+
}
|
|
5482
|
+
} catch {
|
|
5483
|
+
}
|
|
5484
|
+
}
|
|
5485
|
+
// ── Mutations (optimistic + beacon) ──────────────────────────────────
|
|
5486
|
+
async markRead(messageId) {
|
|
5487
|
+
const entry = this.entries.find((e) => e.id === messageId);
|
|
5488
|
+
if (!entry || entry.read) return;
|
|
5489
|
+
entry.read = true;
|
|
5490
|
+
entry.read_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
5491
|
+
this.recomputeUnread();
|
|
5492
|
+
void this.writeCache();
|
|
5493
|
+
this.emit();
|
|
5494
|
+
await this.postBeacon(`/v1/in-app/inbox/${messageId}/read`);
|
|
5495
|
+
}
|
|
5496
|
+
async dismiss(messageId) {
|
|
5497
|
+
const idx = this.entries.findIndex((e) => e.id === messageId);
|
|
5498
|
+
if (idx < 0) return;
|
|
5499
|
+
const [entry] = this.entries.splice(idx, 1);
|
|
5500
|
+
if (entry && !entry.read) {
|
|
5501
|
+
this.recomputeUnread();
|
|
5502
|
+
}
|
|
5503
|
+
void this.writeCache();
|
|
5504
|
+
this.emit();
|
|
5505
|
+
await this.postBeacon(`/v1/in-app/inbox/${messageId}/dismiss`);
|
|
5506
|
+
}
|
|
5507
|
+
async markAllRead() {
|
|
5508
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5509
|
+
for (const e of this.entries) {
|
|
5510
|
+
if (!e.read) {
|
|
5511
|
+
e.read = true;
|
|
5512
|
+
e.read_at = now;
|
|
5513
|
+
}
|
|
5514
|
+
}
|
|
5515
|
+
this.unreadCount = 0;
|
|
5516
|
+
void this.writeCache();
|
|
5517
|
+
this.emit();
|
|
5518
|
+
await this.postBeacon("/v1/in-app/inbox/read-all");
|
|
5519
|
+
}
|
|
5520
|
+
// ── Internals ────────────────────────────────────────────────────────
|
|
5521
|
+
seedFromBundle(page, unreadCount) {
|
|
5522
|
+
const byId = new Map(this.entries.map((e) => [e.id, e]));
|
|
5523
|
+
const merged = page.map((incoming) => {
|
|
5524
|
+
const existing = byId.get(incoming.id);
|
|
5525
|
+
if (existing && existing.read && !incoming.read) {
|
|
5526
|
+
return { ...incoming, read: true, read_at: existing.read_at };
|
|
5527
|
+
}
|
|
5528
|
+
return incoming;
|
|
5529
|
+
});
|
|
5530
|
+
for (const [id, e] of byId) {
|
|
5531
|
+
if (!merged.find((m2) => m2.id === id)) merged.push(e);
|
|
5532
|
+
}
|
|
5533
|
+
merged.sort((a, b) => (b.created_at || "").localeCompare(a.created_at || ""));
|
|
5534
|
+
this.entries = merged;
|
|
5535
|
+
this.unreadCount = unreadCount;
|
|
5536
|
+
void this.writeCache();
|
|
5537
|
+
this.emit();
|
|
5538
|
+
}
|
|
5539
|
+
recomputeUnread() {
|
|
5540
|
+
this.unreadCount = this.entries.filter((e) => !e.read).length;
|
|
5541
|
+
}
|
|
5542
|
+
emit() {
|
|
5543
|
+
const snapshot = { entries: [...this.entries], unreadCount: this.unreadCount };
|
|
5544
|
+
for (const l of this.listeners) {
|
|
5545
|
+
try {
|
|
5546
|
+
l(snapshot);
|
|
5547
|
+
} catch (err) {
|
|
5548
|
+
logger.warn("inbox listener threw:", err);
|
|
5549
|
+
}
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
5552
|
+
headers() {
|
|
5553
|
+
const h = {
|
|
5554
|
+
"X-Aegis-Write-Key": this.writeKey,
|
|
5555
|
+
Accept: "application/json"
|
|
5556
|
+
};
|
|
5557
|
+
if (this.organizationId) h["X-Organization-ID"] = this.organizationId;
|
|
5558
|
+
if (this.contactId) h["X-Contact-ID"] = this.contactId;
|
|
5559
|
+
if (this.propertyId) h["X-Property-Id"] = this.propertyId;
|
|
5560
|
+
return h;
|
|
5561
|
+
}
|
|
5562
|
+
/** Best-effort beacon-style POST. Uses `keepalive: true` so it survives
|
|
5563
|
+
* a navigation away from the current page (e.g., user taps the CTA
|
|
5564
|
+
* which takes them to another URL right after dismiss). */
|
|
5565
|
+
async postBeacon(path) {
|
|
5566
|
+
if (!this.contactId || !this.propertyId) return;
|
|
5567
|
+
try {
|
|
5568
|
+
await fetch(`${this.apiHost}${path}`, {
|
|
5569
|
+
method: "POST",
|
|
5570
|
+
headers: this.headers(),
|
|
5571
|
+
keepalive: true
|
|
5572
|
+
});
|
|
5573
|
+
} catch (err) {
|
|
5574
|
+
logger.warn(`inbox mutation POST ${path} failed:`, err);
|
|
5575
|
+
}
|
|
5576
|
+
}
|
|
5577
|
+
// ── IndexedDB cache (non-critical) ───────────────────────────────────
|
|
5578
|
+
async openDb() {
|
|
5579
|
+
if (typeof indexedDB === "undefined") return null;
|
|
5580
|
+
return new Promise((resolve) => {
|
|
5581
|
+
try {
|
|
5582
|
+
const req = indexedDB.open(IDB_NAME, IDB_VERSION);
|
|
5583
|
+
req.onerror = () => resolve(null);
|
|
5584
|
+
req.onupgradeneeded = () => {
|
|
5585
|
+
const db = req.result;
|
|
5586
|
+
if (!db.objectStoreNames.contains(IDB_STORE)) {
|
|
5587
|
+
db.createObjectStore(IDB_STORE);
|
|
5588
|
+
}
|
|
5589
|
+
};
|
|
5590
|
+
req.onsuccess = () => resolve(req.result);
|
|
5591
|
+
} catch {
|
|
5592
|
+
resolve(null);
|
|
5593
|
+
}
|
|
5594
|
+
});
|
|
5595
|
+
}
|
|
5596
|
+
cacheKey() {
|
|
5597
|
+
if (!this.contactId || !this.propertyId) return null;
|
|
5598
|
+
return `${this.organizationId || "default"}:${this.propertyId}:${this.contactId}`;
|
|
5599
|
+
}
|
|
5600
|
+
async writeCache() {
|
|
5601
|
+
const key = this.cacheKey();
|
|
5602
|
+
if (!key) return;
|
|
5603
|
+
const db = await this.openDb();
|
|
5604
|
+
if (!db) return;
|
|
5605
|
+
try {
|
|
5606
|
+
const tx = db.transaction(IDB_STORE, "readwrite");
|
|
5607
|
+
tx.objectStore(IDB_STORE).put(this.entries, key);
|
|
5608
|
+
await new Promise((res, rej) => {
|
|
5609
|
+
tx.oncomplete = () => res();
|
|
5610
|
+
tx.onerror = () => rej(tx.error);
|
|
5611
|
+
});
|
|
5612
|
+
} catch {
|
|
5613
|
+
} finally {
|
|
5614
|
+
db.close();
|
|
5615
|
+
}
|
|
5616
|
+
}
|
|
5617
|
+
async readCache() {
|
|
5618
|
+
const key = this.cacheKey();
|
|
5619
|
+
if (!key) return [];
|
|
5620
|
+
const db = await this.openDb();
|
|
5621
|
+
if (!db) return [];
|
|
5622
|
+
try {
|
|
5623
|
+
const tx = db.transaction(IDB_STORE, "readonly");
|
|
5624
|
+
const req = tx.objectStore(IDB_STORE).get(key);
|
|
5625
|
+
return await new Promise((res) => {
|
|
5626
|
+
req.onsuccess = () => res(req.result || []);
|
|
5627
|
+
req.onerror = () => res([]);
|
|
5628
|
+
});
|
|
5629
|
+
} catch {
|
|
5630
|
+
return [];
|
|
5631
|
+
} finally {
|
|
5632
|
+
db.close();
|
|
5633
|
+
}
|
|
5634
|
+
}
|
|
5635
|
+
async clearCache() {
|
|
5636
|
+
const key = this.cacheKey();
|
|
5637
|
+
if (!key) return;
|
|
5638
|
+
const db = await this.openDb();
|
|
5639
|
+
if (!db) return;
|
|
5640
|
+
try {
|
|
5641
|
+
const tx = db.transaction(IDB_STORE, "readwrite");
|
|
5642
|
+
tx.objectStore(IDB_STORE).delete(key);
|
|
5643
|
+
await new Promise((res) => {
|
|
5644
|
+
tx.oncomplete = () => res();
|
|
5645
|
+
tx.onerror = () => res();
|
|
5646
|
+
});
|
|
5647
|
+
} catch {
|
|
5648
|
+
} finally {
|
|
5649
|
+
db.close();
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
}
|
|
5653
|
+
class AegisMessageRuntime {
|
|
5654
|
+
constructor(config) {
|
|
5655
|
+
this.initialized = false;
|
|
5656
|
+
this.inApp = new AegisInAppManager({
|
|
5657
|
+
writeKey: config.writeKey,
|
|
5658
|
+
apiHost: config.apiHost,
|
|
5659
|
+
userId: config.userId,
|
|
5660
|
+
contactId: config.contactId,
|
|
5661
|
+
organizationId: config.organizationId,
|
|
5662
|
+
propertyId: config.propertyId,
|
|
5663
|
+
debugMode: config.debugMode,
|
|
5664
|
+
enableSSE: config.enableSSE
|
|
5665
|
+
});
|
|
5666
|
+
this.widgets = new AegisWidgetManager({
|
|
5667
|
+
writeKey: config.writeKey,
|
|
5668
|
+
apiHost: config.apiHost,
|
|
5669
|
+
userId: config.userId,
|
|
5670
|
+
contactId: config.contactId,
|
|
5671
|
+
organizationId: config.organizationId,
|
|
5672
|
+
debugMode: config.debugMode,
|
|
5673
|
+
triggerEngine: config.triggerEngine,
|
|
5674
|
+
enablePrefetch: config.enableWidgetPrefetch !== false,
|
|
5675
|
+
sourcePlatform: config.sourcePlatform
|
|
5676
|
+
});
|
|
5677
|
+
}
|
|
5678
|
+
/**
|
|
5679
|
+
* Boots both managers in parallel. Safe to call multiple times — the
|
|
5680
|
+
* second + subsequent calls are no-ops.
|
|
5681
|
+
*/
|
|
5682
|
+
async initialize() {
|
|
5683
|
+
if (this.initialized) return;
|
|
5684
|
+
this.initialized = true;
|
|
5685
|
+
await Promise.all([
|
|
5686
|
+
this.inApp.initialize(),
|
|
5687
|
+
this.widgets.initialize()
|
|
5688
|
+
]);
|
|
5689
|
+
}
|
|
5690
|
+
/**
|
|
5691
|
+
* Both managers carry contactId. Update both so the server-side
|
|
5692
|
+
* targeting pipeline sees the identity for campaign eligibility AND
|
|
5693
|
+
* the widget prefetch includes per-contact segment configs.
|
|
5694
|
+
*/
|
|
5695
|
+
updateContactId(contactId) {
|
|
5696
|
+
var _a, _b;
|
|
5697
|
+
(_b = (_a = this.inApp).updateContactId) == null ? void 0 : _b.call(_a, contactId);
|
|
5698
|
+
}
|
|
5699
|
+
/**
|
|
5700
|
+
* Forward a client-side event (product_viewed, cart_idle_90s, etc.)
|
|
5701
|
+
* to the in-app manager's client-trigger evaluator. The WidgetManager
|
|
5702
|
+
* has its own TriggerEngine wiring (exit-intent, scroll-velocity) so
|
|
5703
|
+
* we don't forward there — events it cares about arrive through that
|
|
5704
|
+
* channel already.
|
|
5705
|
+
*/
|
|
5706
|
+
onClientEvent(eventName, eventData = {}) {
|
|
5707
|
+
var _a, _b;
|
|
5708
|
+
(_b = (_a = this.inApp).onClientEvent) == null ? void 0 : _b.call(_a, eventName, eventData);
|
|
5709
|
+
}
|
|
5710
|
+
/**
|
|
5711
|
+
* Tear down both managers. Used by React component unmounts + during
|
|
5712
|
+
* identity switches where we want a full reset.
|
|
5713
|
+
*/
|
|
5714
|
+
destroy() {
|
|
5715
|
+
var _a, _b, _c, _d;
|
|
5716
|
+
(_b = (_a = this.inApp).destroy) == null ? void 0 : _b.call(_a);
|
|
5717
|
+
(_d = (_c = this.widgets).destroy) == null ? void 0 : _d.call(_c);
|
|
5718
|
+
this.initialized = false;
|
|
5719
|
+
}
|
|
5720
|
+
/**
|
|
5721
|
+
* Current armed campaigns visible to the InAppManager. Accessible for
|
|
5722
|
+
* the Prefetch Inspector and for debugging. WidgetManager's prefetched
|
|
5723
|
+
* spin_wheel / scratch_card configs are NOT in this list — they live
|
|
5724
|
+
* in `this.widgets` and have their own lifecycle.
|
|
5725
|
+
*/
|
|
5726
|
+
getCampaigns() {
|
|
5727
|
+
return this.inApp.campaigns ?? [];
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
4354
5730
|
const COOKIE_NAME = "aegis_fpc";
|
|
4355
5731
|
const COOKIE_MAX_AGE_DAYS = 365 * 2;
|
|
4356
5732
|
class BootstrapError extends Error {
|
|
@@ -4428,6 +5804,8 @@ const aegis = new Aegis();
|
|
|
4428
5804
|
export {
|
|
4429
5805
|
Aegis,
|
|
4430
5806
|
AegisInAppManager,
|
|
5807
|
+
AegisInbox,
|
|
5808
|
+
AegisMessageRuntime,
|
|
4431
5809
|
AegisPlacementManager,
|
|
4432
5810
|
AegisWebPush,
|
|
4433
5811
|
AegisWidgetManager,
|
|
@@ -4435,6 +5813,7 @@ export {
|
|
|
4435
5813
|
BootstrapError,
|
|
4436
5814
|
E as EcommerceTracker,
|
|
4437
5815
|
N as NameGovernor,
|
|
5816
|
+
PrefetchBundleClient,
|
|
4438
5817
|
R as RateLimiter,
|
|
4439
5818
|
SdkConfigPoller,
|
|
4440
5819
|
TriggerEngine,
|