@active-reach/web-sdk 1.3.1 → 1.5.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-B0JfoAJs.mjs → analytics-C00PJUSy.mjs} +269 -2
- package/dist/analytics-C00PJUSy.mjs.map +1 -0
- package/dist/cdn.d.ts +7 -0
- package/dist/cdn.d.ts.map +1 -1
- package/dist/core/analytics.d.ts +20 -0
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/core/bootstrap.d.ts +71 -0
- package/dist/core/bootstrap.d.ts.map +1 -0
- package/dist/core/prefetch-bundle-client.d.ts +150 -0
- package/dist/core/prefetch-bundle-client.d.ts.map +1 -0
- package/dist/governance/bloom-filter.d.ts +47 -0
- package/dist/governance/bloom-filter.d.ts.map +1 -0
- package/dist/governance/index.d.ts +6 -0
- package/dist/governance/index.d.ts.map +1 -0
- package/dist/governance/murmur3.d.ts +43 -0
- package/dist/governance/murmur3.d.ts.map +1 -0
- package/dist/governance/name-governor.d.ts +98 -0
- package/dist/governance/name-governor.d.ts.map +1 -0
- package/dist/inapp/AegisInAppManager.d.ts +28 -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 +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1396 -10
- package/dist/index.js.map +1 -1
- package/dist/push/AegisWebPush.d.ts +17 -2
- package/dist/push/AegisWebPush.d.ts.map +1 -1
- package/dist/push/AegisWebPush.js +95 -29
- package/dist/push/AegisWebPush.js.map +1 -1
- package/dist/react.js +1 -1
- package/package.json +1 -1
- package/dist/analytics-B0JfoAJs.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { l as logger, A as Aegis } from "./analytics-
|
|
2
|
-
import { E, R } 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 = [];
|
|
@@ -37,6 +746,7 @@ class AegisInAppManager {
|
|
|
37
746
|
this.userId = config.userId;
|
|
38
747
|
this.contactId = config.contactId;
|
|
39
748
|
this.organizationId = config.organizationId;
|
|
749
|
+
this.propertyId = config.propertyId;
|
|
40
750
|
this.debugMode = config.debugMode || false;
|
|
41
751
|
this.enableSSE = config.enableSSE === true;
|
|
42
752
|
}
|
|
@@ -154,6 +864,9 @@ class AegisInAppManager {
|
|
|
154
864
|
if (this.organizationId) {
|
|
155
865
|
headers["X-Organization-ID"] = this.organizationId;
|
|
156
866
|
}
|
|
867
|
+
if (this.propertyId) {
|
|
868
|
+
headers["X-Property-Id"] = this.propertyId;
|
|
869
|
+
}
|
|
157
870
|
const abAssignments = this.getABAssignments();
|
|
158
871
|
if (Object.keys(abAssignments).length > 0) {
|
|
159
872
|
headers["X-AB-Assignments"] = btoa(JSON.stringify(abAssignments));
|
|
@@ -209,11 +922,60 @@ class AegisInAppManager {
|
|
|
209
922
|
return assignments[campaignId] ?? void 0;
|
|
210
923
|
}
|
|
211
924
|
tryDisplayNextCampaign() {
|
|
212
|
-
const campaign = this.campaigns.find(
|
|
925
|
+
const campaign = this.campaigns.find(
|
|
926
|
+
(c) => !this.displayedCampaigns.has(c.id) && !c.client_trigger
|
|
927
|
+
);
|
|
213
928
|
if (campaign) {
|
|
214
929
|
this.displayCampaign(campaign);
|
|
215
930
|
}
|
|
216
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
|
+
}
|
|
217
979
|
displayCampaign(campaign) {
|
|
218
980
|
this.displayedCampaigns.add(campaign.id);
|
|
219
981
|
const interactiveSubTypes = /* @__PURE__ */ new Set([
|
|
@@ -252,9 +1014,49 @@ class AegisInAppManager {
|
|
|
252
1014
|
case "tooltip":
|
|
253
1015
|
this.renderTooltip(campaign);
|
|
254
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;
|
|
255
1039
|
}
|
|
256
1040
|
this.trackEvent(campaign.id, "impression");
|
|
257
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
|
+
}
|
|
258
1060
|
/**
|
|
259
1061
|
* Renders interactive sub-type campaigns (spin wheel, NPS, quiz, etc.)
|
|
260
1062
|
* using DOM-safe rendering. These sub-types use the campaign's
|
|
@@ -379,12 +1181,12 @@ class AegisInAppManager {
|
|
|
379
1181
|
const update = () => {
|
|
380
1182
|
const diff = Math.max(0, target - Date.now());
|
|
381
1183
|
const h = String(Math.floor(diff / 36e5)).padStart(2, "0");
|
|
382
|
-
const
|
|
1184
|
+
const m2 = String(Math.floor(diff % 36e5 / 6e4)).padStart(2, "0");
|
|
383
1185
|
const s = String(Math.floor(diff % 6e4 / 1e3)).padStart(2, "0");
|
|
384
1186
|
const spans = digits.querySelectorAll("span");
|
|
385
1187
|
if (spans.length >= 5) {
|
|
386
1188
|
spans[0].textContent = h;
|
|
387
|
-
spans[2].textContent =
|
|
1189
|
+
spans[2].textContent = m2;
|
|
388
1190
|
spans[4].textContent = s;
|
|
389
1191
|
}
|
|
390
1192
|
if (diff > 0) requestAnimationFrame(update);
|
|
@@ -1337,6 +2139,14 @@ class AegisInAppManager {
|
|
|
1337
2139
|
from { transform: scale(0.9); opacity: 0; }
|
|
1338
2140
|
to { transform: scale(1); opacity: 1; }
|
|
1339
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
|
+
}
|
|
1340
2150
|
`;
|
|
1341
2151
|
document.head.appendChild(style);
|
|
1342
2152
|
}
|
|
@@ -1397,6 +2207,10 @@ class AegisInAppManager {
|
|
|
1397
2207
|
event_type: eventType,
|
|
1398
2208
|
user_id: this.userId,
|
|
1399
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,
|
|
1400
2214
|
platform: "web",
|
|
1401
2215
|
variant_id: this.getVariantId(campaignId) ?? void 0
|
|
1402
2216
|
},
|
|
@@ -1460,11 +2274,11 @@ function renderPreview(config) {
|
|
|
1460
2274
|
debugMode: false,
|
|
1461
2275
|
enableSSE: false
|
|
1462
2276
|
});
|
|
1463
|
-
const
|
|
1464
|
-
|
|
2277
|
+
const m2 = manager;
|
|
2278
|
+
m2.trackEvent = async () => {
|
|
1465
2279
|
};
|
|
1466
|
-
|
|
1467
|
-
|
|
2280
|
+
m2.addAnimationStyles();
|
|
2281
|
+
m2.displayCampaign(config);
|
|
1468
2282
|
}
|
|
1469
2283
|
class AegisPlacementManager {
|
|
1470
2284
|
constructor(config) {
|
|
@@ -4347,20 +5161,592 @@ class SdkConfigPoller {
|
|
|
4347
5161
|
}
|
|
4348
5162
|
}
|
|
4349
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
|
+
const COOKIE_NAME = "aegis_fpc";
|
|
5654
|
+
const COOKIE_MAX_AGE_DAYS = 365 * 2;
|
|
5655
|
+
class BootstrapError extends Error {
|
|
5656
|
+
constructor(status, message) {
|
|
5657
|
+
super(message);
|
|
5658
|
+
this.status = status;
|
|
5659
|
+
this.name = "BootstrapError";
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5662
|
+
function readFirstPartyCookie() {
|
|
5663
|
+
if (typeof document === "undefined") return null;
|
|
5664
|
+
const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${COOKIE_NAME}=([^;]+)`));
|
|
5665
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
5666
|
+
}
|
|
5667
|
+
function writeFirstPartyCookie(cookieId) {
|
|
5668
|
+
if (typeof document === "undefined") return;
|
|
5669
|
+
const expires = /* @__PURE__ */ new Date();
|
|
5670
|
+
expires.setDate(expires.getDate() + COOKIE_MAX_AGE_DAYS);
|
|
5671
|
+
document.cookie = `${COOKIE_NAME}=${encodeURIComponent(cookieId)};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
|
|
5672
|
+
try {
|
|
5673
|
+
window.localStorage.setItem(COOKIE_NAME, cookieId);
|
|
5674
|
+
} catch {
|
|
5675
|
+
}
|
|
5676
|
+
}
|
|
5677
|
+
async function bootstrap(apiHost, req) {
|
|
5678
|
+
const body = { writeKey: req.writeKey };
|
|
5679
|
+
if (req.currentOrigin) body.currentOrigin = req.currentOrigin;
|
|
5680
|
+
if (req.firstPartyCookieId) body.firstPartyCookieId = req.firstPartyCookieId;
|
|
5681
|
+
if (req.attestationToken) body.attestationToken = req.attestationToken;
|
|
5682
|
+
if (req.userAgent) body.userAgent = req.userAgent;
|
|
5683
|
+
const url = `${apiHost.replace(/\/$/, "")}/v1/sdk/bootstrap`;
|
|
5684
|
+
const doFetch = async () => fetch(url, {
|
|
5685
|
+
method: "POST",
|
|
5686
|
+
headers: { "Content-Type": "application/json" },
|
|
5687
|
+
body: JSON.stringify(body),
|
|
5688
|
+
credentials: "omit"
|
|
5689
|
+
});
|
|
5690
|
+
let resp = await doFetch();
|
|
5691
|
+
if (resp.status >= 500) {
|
|
5692
|
+
await new Promise((r) => setTimeout(r, 750));
|
|
5693
|
+
resp = await doFetch();
|
|
5694
|
+
}
|
|
5695
|
+
if (resp.status === 401) {
|
|
5696
|
+
throw new BootstrapError(401, "writeKey not recognized or revoked");
|
|
5697
|
+
}
|
|
5698
|
+
if (resp.status === 403) {
|
|
5699
|
+
throw new BootstrapError(403, "Origin validation failed — this writeKey is bound to a different property");
|
|
5700
|
+
}
|
|
5701
|
+
if (!resp.ok) {
|
|
5702
|
+
throw new BootstrapError(resp.status, `Bootstrap failed: HTTP ${resp.status}`);
|
|
5703
|
+
}
|
|
5704
|
+
const json = await resp.json();
|
|
5705
|
+
writeFirstPartyCookie(json.firstPartyCookieId);
|
|
5706
|
+
return json;
|
|
5707
|
+
}
|
|
5708
|
+
async function deriveDeviceFingerprint(firstPartyCookieId) {
|
|
5709
|
+
const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
|
|
5710
|
+
const platform = typeof navigator !== "undefined" ? navigator.platform || "" : "";
|
|
5711
|
+
const language = typeof navigator !== "undefined" ? navigator.language || "" : "";
|
|
5712
|
+
const input = `${firstPartyCookieId}|${ua}|${platform}|${language}`;
|
|
5713
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
5714
|
+
const bytes = new TextEncoder().encode(input);
|
|
5715
|
+
const hash = await crypto.subtle.digest("SHA-256", bytes);
|
|
5716
|
+
const hex = Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
5717
|
+
return hex;
|
|
5718
|
+
}
|
|
5719
|
+
let h = 2166136261;
|
|
5720
|
+
for (let i = 0; i < input.length; i++) {
|
|
5721
|
+
h ^= input.charCodeAt(i);
|
|
5722
|
+
h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
|
|
5723
|
+
}
|
|
5724
|
+
return `fnv-${h.toString(16)}`;
|
|
5725
|
+
}
|
|
4350
5726
|
const aegis = new Aegis();
|
|
4351
5727
|
export {
|
|
4352
5728
|
Aegis,
|
|
4353
5729
|
AegisInAppManager,
|
|
5730
|
+
AegisInbox,
|
|
4354
5731
|
AegisPlacementManager,
|
|
4355
5732
|
AegisWebPush,
|
|
4356
5733
|
AegisWidgetManager,
|
|
5734
|
+
B as BloomFilter,
|
|
5735
|
+
BootstrapError,
|
|
4357
5736
|
E as EcommerceTracker,
|
|
5737
|
+
N as NameGovernor,
|
|
5738
|
+
PrefetchBundleClient,
|
|
4358
5739
|
R as RateLimiter,
|
|
4359
5740
|
SdkConfigPoller,
|
|
4360
5741
|
TriggerEngine,
|
|
5742
|
+
bootstrap,
|
|
4361
5743
|
debounce,
|
|
4362
5744
|
aegis as default,
|
|
5745
|
+
deriveDeviceFingerprint,
|
|
5746
|
+
m as murmurhash3_x86_32,
|
|
5747
|
+
readFirstPartyCookie,
|
|
4363
5748
|
renderPreview,
|
|
4364
|
-
throttle
|
|
5749
|
+
throttle,
|
|
5750
|
+
writeFirstPartyCookie
|
|
4365
5751
|
};
|
|
4366
5752
|
//# sourceMappingURL=index.js.map
|