@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.
Files changed (51) hide show
  1. package/dist/aegis.min.js +1 -1
  2. package/dist/aegis.min.js.map +1 -1
  3. package/dist/{analytics-B0JfoAJs.mjs → analytics-C00PJUSy.mjs} +269 -2
  4. package/dist/analytics-C00PJUSy.mjs.map +1 -0
  5. package/dist/cdn.d.ts +7 -0
  6. package/dist/cdn.d.ts.map +1 -1
  7. package/dist/core/analytics.d.ts +20 -0
  8. package/dist/core/analytics.d.ts.map +1 -1
  9. package/dist/core/bootstrap.d.ts +71 -0
  10. package/dist/core/bootstrap.d.ts.map +1 -0
  11. package/dist/core/prefetch-bundle-client.d.ts +150 -0
  12. package/dist/core/prefetch-bundle-client.d.ts.map +1 -0
  13. package/dist/governance/bloom-filter.d.ts +47 -0
  14. package/dist/governance/bloom-filter.d.ts.map +1 -0
  15. package/dist/governance/index.d.ts +6 -0
  16. package/dist/governance/index.d.ts.map +1 -0
  17. package/dist/governance/murmur3.d.ts +43 -0
  18. package/dist/governance/murmur3.d.ts.map +1 -0
  19. package/dist/governance/name-governor.d.ts +98 -0
  20. package/dist/governance/name-governor.d.ts.map +1 -0
  21. package/dist/inapp/AegisInAppManager.d.ts +28 -1
  22. package/dist/inapp/AegisInAppManager.d.ts.map +1 -1
  23. package/dist/inapp/renderers/carousel-cards.d.ts +15 -0
  24. package/dist/inapp/renderers/carousel-cards.d.ts.map +1 -0
  25. package/dist/inapp/renderers/coachmark-tour.d.ts +24 -0
  26. package/dist/inapp/renderers/coachmark-tour.d.ts.map +1 -0
  27. package/dist/inapp/renderers/index.d.ts +12 -0
  28. package/dist/inapp/renderers/index.d.ts.map +1 -0
  29. package/dist/inapp/renderers/product-recommendation.d.ts +23 -0
  30. package/dist/inapp/renderers/product-recommendation.d.ts.map +1 -0
  31. package/dist/inapp/renderers/progress-bar.d.ts +24 -0
  32. package/dist/inapp/renderers/progress-bar.d.ts.map +1 -0
  33. package/dist/inapp/renderers/sticky-bar.d.ts +14 -0
  34. package/dist/inapp/renderers/sticky-bar.d.ts.map +1 -0
  35. package/dist/inapp/renderers/types.d.ts +27 -0
  36. package/dist/inapp/renderers/types.d.ts.map +1 -0
  37. package/dist/inbox/AegisInbox.d.ts +103 -0
  38. package/dist/inbox/AegisInbox.d.ts.map +1 -0
  39. package/dist/inbox/index.d.ts +3 -0
  40. package/dist/inbox/index.d.ts.map +1 -0
  41. package/dist/index.d.ts +8 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +1396 -10
  44. package/dist/index.js.map +1 -1
  45. package/dist/push/AegisWebPush.d.ts +17 -2
  46. package/dist/push/AegisWebPush.d.ts.map +1 -1
  47. package/dist/push/AegisWebPush.js +95 -29
  48. package/dist/push/AegisWebPush.js.map +1 -1
  49. package/dist/react.js +1 -1
  50. package/package.json +1 -1
  51. 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-B0JfoAJs.mjs";
2
- import { E, R } from "./analytics-B0JfoAJs.mjs";
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((c) => !this.displayedCampaigns.has(c.id));
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 m = String(Math.floor(diff % 36e5 / 6e4)).padStart(2, "0");
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 = m;
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 m = manager;
1464
- m.trackEvent = async () => {
2277
+ const m2 = manager;
2278
+ m2.trackEvent = async () => {
1465
2279
  };
1466
- m.addAnimationStyles();
1467
- m.displayCampaign(config);
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