@active-reach/web-sdk 1.35.2 → 1.35.4

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/index.js CHANGED
@@ -524,7 +524,8 @@ function applyPerCardOverride(tpl, look) {
524
524
  if (look.hide_price) drop("price");
525
525
  if (look.hide_cta) drop("cta");
526
526
  if (look.hide_rating) drop("rating");
527
- if (look.cta_text) elements = elements.map((e) => e.type === "cta" ? { ...e, text: look.cta_text } : e);
527
+ if (look.cta_text)
528
+ elements = elements.map((e) => e.type === "cta" ? { ...e, text: look.cta_text } : e);
528
529
  if (look.cta_action || look.cta_url || look.cta_custom_name) {
529
530
  elements = elements.map(
530
531
  (e) => e.type === "cta" ? {
@@ -537,7 +538,9 @@ function applyPerCardOverride(tpl, look) {
537
538
  }
538
539
  if (look.badge) {
539
540
  const badgeEl = { type: "badge", text: look.badge, color: look.badge_color };
540
- elements = elements.some((e) => e.type === "badge") ? elements.map((e) => e.type === "badge" ? badgeEl : e) : [elements[0], badgeEl, ...elements.slice(1)].filter(Boolean);
541
+ elements = elements.some((e) => e.type === "badge") ? elements.map((e) => e.type === "badge" ? badgeEl : e) : [elements[0], badgeEl, ...elements.slice(1)].filter(
542
+ Boolean
543
+ );
541
544
  }
542
545
  return { ...tpl, frame, elements };
543
546
  }
@@ -585,13 +588,12 @@ function paintRating(card) {
585
588
  const count = Number(meta.review_count ?? 0);
586
589
  const rr = document.createElement("div");
587
590
  rr.style.cssText = "display:flex;align-items:center;gap:3px;font-size:11px;line-height:1;";
588
- const star2 = document.createElement("span");
589
- star2.textContent = "★";
590
- star2.style.cssText = "color:#059669;";
591
+ const star2 = lucideSvgEl("star", { size: 12, color: "#059669" });
592
+ if (star2) star2.setAttribute("fill", "#059669");
591
593
  const val = document.createElement("span");
592
594
  val.textContent = avg.toFixed(1);
593
595
  val.style.cssText = "color:#059669;font-weight:700;";
594
- rr.appendChild(star2);
596
+ if (star2) rr.appendChild(star2);
595
597
  rr.appendChild(val);
596
598
  if (count > 0) {
597
599
  const c = document.createElement("span");
@@ -650,7 +652,9 @@ function appendRatingThanks(wrap, el2, ctx, fg, accent, isHigh) {
650
652
  cta.textContent = el2.rating_high_cta || "Leave a review";
651
653
  cta.style.cssText = `margin:4px auto 0;display:block;padding:9px 18px;border:none;border-radius:999px;background:${accent};color:#fff;font-size:13px;font-weight:800;cursor:pointer;`;
652
654
  cta.addEventListener("click", () => {
653
- ctx.trackEvent(ctx.campaign.id, "clicked", { meta: { element: "rating_input", cta: "high_review" } });
655
+ ctx.trackEvent(ctx.campaign.id, "clicked", {
656
+ meta: { element: "rating_input", cta: "high_review" }
657
+ });
654
658
  if (ctx.navigate) ctx.navigate(url, "cta");
655
659
  else window.location.href = url;
656
660
  });
@@ -866,11 +870,25 @@ function buildProductRatingListElement(el2, ctx, fg) {
866
870
  panel.style.display = "flex";
867
871
  toggle.style.display = "none";
868
872
  });
873
+ const hint = document.createElement("div");
874
+ hint.style.cssText = `font-size:11px;color:${fg};opacity:.65;`;
869
875
  if (typeof el2.review_earn_hint === "string" && el2.review_earn_hint.trim()) {
870
- const hint = document.createElement("div");
871
876
  hint.textContent = el2.review_earn_hint.trim();
872
- hint.style.cssText = `font-size:11px;color:${fg};opacity:.65;`;
873
877
  panel.appendChild(hint);
878
+ } else if (ctx.apiHost && ctx.writeKey && ctx.organizationId) {
879
+ panel.appendChild(hint);
880
+ const url = `${ctx.apiHost}/v1/in-app/review-earn-hint`;
881
+ void fetch(url, {
882
+ headers: { "X-Aegis-Write-Key": ctx.writeKey, "X-Organization-ID": ctx.organizationId }
883
+ }).then((r) => r.ok ? r.json() : null).then((d) => {
884
+ if (!d || d.enabled !== true) return;
885
+ const name = typeof d.points_name === "string" ? d.points_name : "points";
886
+ const parts = [`Earn ${Number(d.base)} ${name}`];
887
+ if (Number(d.photo_bonus) > 0) parts.push(`+${Number(d.photo_bonus)} photo`);
888
+ if (Number(d.video_bonus) > 0) parts.push(`+${Number(d.video_bonus)} video`);
889
+ hint.textContent = parts.join(" · ");
890
+ }).catch(() => {
891
+ });
874
892
  }
875
893
  if (allowText) {
876
894
  const ta = document.createElement("textarea");
@@ -970,7 +988,9 @@ function buildProductRatingListElement(el2, ctx, fg) {
970
988
  submitBtn.disabled = true;
971
989
  submitBtn.style.opacity = ".6";
972
990
  submitBtn.textContent = "Saving…";
973
- ctx.trackEvent(ctx.campaign.id, "clicked", { meta: { element: "product_rating_list", items: rated.length } });
991
+ ctx.trackEvent(ctx.campaign.id, "clicked", {
992
+ meta: { element: "product_rating_list", items: rated.length }
993
+ });
974
994
  Promise.resolve(
975
995
  (_a = ctx.submitResponse) == null ? void 0 : _a.call(ctx, "rating", {
976
996
  items: rated.map((pid) => ({ product_id: pid, score: scores[pid] })),
@@ -1005,10 +1025,12 @@ function buildProductRatingListElement(el2, ctx, fg) {
1005
1025
  nameEl.textContent = it.name || it.product_id;
1006
1026
  nameEl.style.cssText = `flex:1;min-width:0;font-size:13px;color:${fg};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;`;
1007
1027
  rowEl.appendChild(nameEl);
1008
- rowEl.appendChild(starRow(scale, (n) => {
1009
- scores[it.product_id] = n;
1010
- refreshSubmit();
1011
- }));
1028
+ rowEl.appendChild(
1029
+ starRow(scale, (n) => {
1030
+ scores[it.product_id] = n;
1031
+ refreshSubmit();
1032
+ })
1033
+ );
1012
1034
  list.appendChild(rowEl);
1013
1035
  });
1014
1036
  wrap.appendChild(list);
@@ -1029,12 +1051,13 @@ function buildProductRatingListElement(el2, ctx, fg) {
1029
1051
  wrap.appendChild(loader);
1030
1052
  const url = `${ctx.apiHost}/v1/in-app/rateable-items?token=${encodeURIComponent(token)}`;
1031
1053
  const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
1032
- if (ac) setTimeout(() => {
1033
- try {
1034
- ac.abort();
1035
- } catch {
1036
- }
1037
- }, 6e3);
1054
+ if (ac)
1055
+ setTimeout(() => {
1056
+ try {
1057
+ ac.abort();
1058
+ } catch {
1059
+ }
1060
+ }, 6e3);
1038
1061
  fetch(url, { headers: { "X-Aegis-Write-Key": ctx.writeKey }, signal: ac == null ? void 0 : ac.signal }).then((r) => r.ok ? r.json() : null).then((data) => {
1039
1062
  render(Array.isArray(data == null ? void 0 : data.items) ? data.items : []);
1040
1063
  }).catch(() => {
@@ -1086,7 +1109,9 @@ function buildPollElement(el2, ctx, fg, accent) {
1086
1109
  btn.style.background = accent;
1087
1110
  btn.style.color = "#fff";
1088
1111
  btn.style.opacity = "1";
1089
- ctx.trackEvent(ctx.campaign.id, "clicked", { meta: { element: "poll", option_index: idx, option_label: label } });
1112
+ ctx.trackEvent(ctx.campaign.id, "clicked", {
1113
+ meta: { element: "poll", option_index: idx, option_label: label }
1114
+ });
1090
1115
  void ((_a = ctx.submitResponse) == null ? void 0 : _a.call(ctx, "poll", { option_index: idx, option_label: label }));
1091
1116
  (_b = ctx.playReaction) == null ? void 0 : _b.call(ctx, wrap, "affirm");
1092
1117
  appendThanks(wrap, el2, fg, "Thanks for your answer!");
@@ -1109,7 +1134,9 @@ function appendNpsThanks(wrap, el2, ctx, fg, accent, isPromoter) {
1109
1134
  cta.textContent = el2.nps_promoter_cta || "Leave a review";
1110
1135
  cta.style.cssText = `margin:4px auto 0;display:block;padding:9px 18px;border:none;border-radius:999px;background:${accent};color:#fff;font-size:13px;font-weight:800;cursor:pointer;`;
1111
1136
  cta.addEventListener("click", () => {
1112
- ctx.trackEvent(ctx.campaign.id, "clicked", { meta: { element: "nps", cta: "promoter_review" } });
1137
+ ctx.trackEvent(ctx.campaign.id, "clicked", {
1138
+ meta: { element: "nps", cta: "promoter_review" }
1139
+ });
1113
1140
  if (ctx.navigate) ctx.navigate(url, "cta");
1114
1141
  else window.location.href = url;
1115
1142
  });
@@ -1154,7 +1181,11 @@ function renderNpsReasonStep(wrap, el2, ctx, score, fg, accent) {
1154
1181
  done = true;
1155
1182
  const comment = withComment ? ta.value.trim() : "";
1156
1183
  void ((_a = ctx.submitResponse) == null ? void 0 : _a.call(ctx, "nps", comment ? { score, band, comment } : { score, band }));
1157
- (_b = ctx.playReaction) == null ? void 0 : _b.call(ctx, wrap, score >= promoterThreshold ? "affirm" : score <= 6 ? "empathize" : "acknowledge");
1184
+ (_b = ctx.playReaction) == null ? void 0 : _b.call(
1185
+ ctx,
1186
+ wrap,
1187
+ score >= promoterThreshold ? "affirm" : score <= 6 ? "empathize" : "acknowledge"
1188
+ );
1158
1189
  appendNpsThanks(wrap, el2, ctx, fg, accent, score >= promoterThreshold);
1159
1190
  };
1160
1191
  const btnRow = document.createElement("div");
@@ -1210,7 +1241,9 @@ function buildNpsElement(el2, ctx, fg, accent, muted) {
1210
1241
  btn.style.color = "#fff";
1211
1242
  btn.style.opacity = "1";
1212
1243
  const npsPayload = { score: n, band: npsBandFromScore(n) };
1213
- ctx.trackEvent(ctx.campaign.id, "clicked", { meta: { element: "nps", score: n, band: npsPayload.band } });
1244
+ ctx.trackEvent(ctx.campaign.id, "clicked", {
1245
+ meta: { element: "nps", score: n, band: npsPayload.band }
1246
+ });
1214
1247
  if (el2.nps_followup_enabled === true) {
1215
1248
  renderNpsReasonStep(wrap, el2, ctx, n, fg, accent);
1216
1249
  } else {
@@ -1327,13 +1360,21 @@ function buildCtaAction(el2, card, meta, inline, sanitizeUrl) {
1327
1360
  const sku = meta.sku;
1328
1361
  const platformProductId = meta.platform_product_id;
1329
1362
  const platformUrl = safe(meta.platform_url);
1363
+ const platformVariantId = meta.platform_variant_id;
1330
1364
  switch (kind) {
1331
1365
  case "open_cart":
1332
1366
  return { type: "open_cart" };
1333
1367
  case "open_login":
1334
1368
  return { type: "open_login" };
1335
1369
  case "open_product":
1336
- return { type: "open_product", productId, sku, url: platformUrl || url, platformProductId, platformUrl };
1370
+ return {
1371
+ type: "open_product",
1372
+ productId,
1373
+ sku,
1374
+ url: platformUrl || url,
1375
+ platformProductId,
1376
+ platformUrl
1377
+ };
1337
1378
  case "navigate":
1338
1379
  return { type: "navigate", url: url || "" };
1339
1380
  case "custom":
@@ -1345,6 +1386,7 @@ function buildCtaAction(el2, card, meta, inline, sanitizeUrl) {
1345
1386
  productId,
1346
1387
  sku,
1347
1388
  platformProductId,
1389
+ platformVariantId,
1348
1390
  title: card.title,
1349
1391
  price: meta.price ?? card.body,
1350
1392
  imageUrl: card.image_url,
@@ -1550,7 +1592,10 @@ function renderCardFromTemplate(card, opts) {
1550
1592
  var _a;
1551
1593
  e.stopPropagation();
1552
1594
  trackEvent(campaign.id, "clicked", { stepId: el2.id || "add" });
1553
- (_a = ctx.dispatchAction) == null ? void 0 : _a.call(ctx, buildCtaAction(el2, card, meta, inline, sanitizeUrl));
1595
+ (_a = ctx.dispatchAction) == null ? void 0 : _a.call(
1596
+ ctx,
1597
+ buildCtaAction(el2, card, meta, inline, sanitizeUrl)
1598
+ );
1554
1599
  });
1555
1600
  (overlayOnImage ? imageWrap : container).appendChild(cta);
1556
1601
  break;
@@ -4018,6 +4063,7 @@ class AegisChat {
4018
4063
  this.pendingEcho = [];
4019
4064
  this.hasInteracted = false;
4020
4065
  this.writeKey = config.writeKey;
4066
+ this.organizationId = config.organizationId;
4021
4067
  this.apiHost = (config.apiHost ?? "").replace(/\/$/, "");
4022
4068
  this.contactId = config.contactId;
4023
4069
  this.channel = config.channel ?? "web";
@@ -4207,6 +4253,7 @@ class AegisChat {
4207
4253
  const url = new URL("/v1/stream/realtime", this.apiHost || window.location.origin);
4208
4254
  url.searchParams.set("ticket", ticket);
4209
4255
  url.searchParams.set("mode", "ephemeral");
4256
+ if (this.organizationId) url.searchParams.set("org", this.organizationId);
4210
4257
  try {
4211
4258
  this.eventSource = new EventSource(url.toString());
4212
4259
  this.eventSource.onopen = () => {
@@ -5368,6 +5415,27 @@ function verifiedBadge() {
5368
5415
  b.appendChild(el("span", "", "Verified purchase"));
5369
5416
  return b;
5370
5417
  }
5418
+ function incentivizedBadge() {
5419
+ const b = el(
5420
+ "span",
5421
+ "display:inline-flex;align-items:center;gap:3px;font-size:12px;font-weight:600;color:#6b7280;"
5422
+ );
5423
+ const svg = document.createElementNS(SVG_NS, "svg");
5424
+ svg.setAttribute("viewBox", "0 0 24 24");
5425
+ svg.setAttribute("width", "13");
5426
+ svg.setAttribute("height", "13");
5427
+ const p = document.createElementNS(SVG_NS, "path");
5428
+ p.setAttribute("d", "M20 12v9H4v-9M2 7h20v5H2zM12 7v14M12 7a2.5 2.5 0 0 0-2.5-2.5C8 4.5 7 6 7 7M12 7a2.5 2.5 0 0 1 2.5-2.5C16 4.5 17 6 17 7");
5429
+ p.setAttribute("fill", "none");
5430
+ p.setAttribute("stroke", "#6b7280");
5431
+ p.setAttribute("stroke-width", "2");
5432
+ p.setAttribute("stroke-linecap", "round");
5433
+ p.setAttribute("stroke-linejoin", "round");
5434
+ svg.appendChild(p);
5435
+ b.appendChild(svg);
5436
+ b.appendChild(el("span", "", "Incentivized"));
5437
+ return b;
5438
+ }
5371
5439
  function avatar(name, accent) {
5372
5440
  const initial = (name.trim()[0] || "?").toUpperCase();
5373
5441
  const a = el(
@@ -5495,6 +5563,7 @@ function renderReviewsInline(ctx, target, creds) {
5495
5563
  const nameRow = el("div", "display:flex;flex-wrap:wrap;align-items:center;gap:8px;");
5496
5564
  nameRow.appendChild(el("span", "font-size:15px;font-weight:700;color:#111827;", ((_b = r.reviewer_snapshot) == null ? void 0 : _b.name) ?? "Customer"));
5497
5565
  if (r.verified_purchase) nameRow.appendChild(verifiedBadge());
5566
+ if (r.is_incentivized) nameRow.appendChild(incentivizedBadge());
5498
5567
  idn.appendChild(nameRow);
5499
5568
  idn.appendChild(stars(r.rating ?? 0, 14));
5500
5569
  head.appendChild(idn);
@@ -6126,6 +6195,7 @@ const _AegisInAppManager = class _AegisInAppManager {
6126
6195
  return;
6127
6196
  }
6128
6197
  const url = new URL("/v1/stream/realtime", this.apiHost);
6198
+ url.searchParams.set("org", this.organizationId);
6129
6199
  const headers = {
6130
6200
  "X-Aegis-Write-Key": this.writeKey,
6131
6201
  "X-Organization-ID": this.organizationId
@@ -6133,11 +6203,10 @@ const _AegisInAppManager = class _AegisInAppManager {
6133
6203
  if (this.contactId) {
6134
6204
  headers["X-Contact-ID"] = this.contactId;
6135
6205
  }
6136
- const queryParams = new URLSearchParams();
6137
6206
  Object.entries(headers).forEach(([key, value]) => {
6138
- queryParams.append(key, value);
6207
+ url.searchParams.append(key, value);
6139
6208
  });
6140
- this.eventSource = new EventSource(`${url}?${queryParams.toString()}`);
6209
+ this.eventSource = new EventSource(url.toString());
6141
6210
  this.eventSource.addEventListener("open", () => {
6142
6211
  this.log("SSE connection established");
6143
6212
  this.reconnectAttempts = 0;
@@ -6300,7 +6369,7 @@ const _AegisInAppManager = class _AegisInAppManager {
6300
6369
  }
6301
6370
  tryDisplayNextCampaign() {
6302
6371
  const campaign = this.campaigns.find(
6303
- (c) => !this.displayedCampaigns.has(c.id) && !c.client_trigger && !this.isSuppressed(c.id) && c.type !== "active_web_chat"
6372
+ (c) => !this.displayedCampaigns.has(c.id) && !c.client_trigger && !this.isSuppressed(c.id) && c.type !== "active_web_chat" && !(Array.isArray(c.delivery_modes) && c.delivery_modes.length > 0 && !c.delivery_modes.includes("in_app_overlay"))
6304
6373
  );
6305
6374
  if (campaign) {
6306
6375
  this.displayCampaign(campaign);
@@ -6333,34 +6402,38 @@ const _AegisInAppManager = class _AegisInAppManager {
6333
6402
  * `<div data-aegis-slot="feedback">` will accept the first active
6334
6403
  * campaign with `widget_category='feedback'` + embedded_card mode.
6335
6404
  */
6336
- /**
6337
- * Surface predicate (In-App Surface Filter tracker P2). Byte-for-byte the
6338
- * same logic as the server-side `_surface_matches` in
6339
- * apps/cell-plane/app/api/v1/in_app/campaigns.py:
6340
- * - campaign with no `surface` (undefined / []) renders on every surface
6341
- * - no declared `currentSurface` => no filter (back-compat)
6342
- * - otherwise the current surface must be in the campaign's surface array
6343
- */
6344
- matchesCurrentSurface(c) {
6345
- if (!c.surface || c.surface.length === 0) return true;
6346
- if (this.currentSurface === null) return true;
6347
- return c.surface.includes(this.currentSurface);
6405
+ /** Normalize a declared free-form screen name to a canonical surface key
6406
+ * ('storefront.catalog' → 'storefront_catalog'), or null when it maps
6407
+ * outside the enum (e.g. 'shopify.product'). Mirrors the server's
6408
+ * `_canonical_surface_from_screen`. */
6409
+ canonicalSurfaceFromScreen() {
6410
+ if (!this.currentScreen) return null;
6411
+ const norm = this.currentScreen.trim().toLowerCase().replace(/[.\- ]/g, "_");
6412
+ return _AegisInAppManager.KNOWN_SURFACES.has(norm) ? norm : null;
6348
6413
  }
6349
6414
  /**
6350
- * Dynamic Screens P2 — combined surface + free-form screen predicate.
6351
- * Byte-for-byte the server's `_screen_or_surface_matches`: when the campaign
6352
- * has no `target_screens` this is exactly `matchesCurrentSurface` (every
6353
- * existing campaign → zero behaviour change). A screen-only campaign matches
6354
- * strictly on `currentScreen`; a dual campaign ORs the two tiers.
6415
+ * Dynamic Screens P2 + substrate isolation — combined surface + free-form
6416
+ * screen predicate, mirroring the server's `_screen_or_surface_matches`:
6417
+ * - screen-only campaign matches strictly on `currentScreen`
6418
+ * - a declared screen NORMALIZES into the canonical tier
6419
+ * ('storefront.catalog' → storefront_catalog) so surface pins match
6420
+ * - a declared screen that maps OUTSIDE the enum (e.g. 'shopify.product')
6421
+ * does NOT fall through to the all-surfaces default for surface-pinned
6422
+ * campaigns — actii.me-pinned campaigns never render on Shopify
6423
+ * - hosts that declare nothing keep the legacy render-everything default
6355
6424
  */
6356
6425
  matchesCurrentScreenOrSurface(c) {
6357
6426
  const screens = c.target_screens;
6358
- if (!screens || screens.length === 0) {
6359
- return this.matchesCurrentSurface(c);
6427
+ const screenOk = !!screens && screens.length > 0 && this.currentScreen !== null && screens.includes(this.currentScreen);
6428
+ if (screens && screens.length > 0 && (!c.surface || c.surface.length === 0)) {
6429
+ return screenOk;
6360
6430
  }
6361
- const screenOk = this.currentScreen !== null && screens.includes(this.currentScreen);
6362
- if (!c.surface || c.surface.length === 0) return screenOk;
6363
- return screenOk || this.matchesCurrentSurface(c);
6431
+ if (screenOk) return true;
6432
+ if (!c.surface || c.surface.length === 0) return true;
6433
+ const effective = this.currentSurface ?? this.canonicalSurfaceFromScreen();
6434
+ if (effective !== null) return c.surface.includes(effective);
6435
+ if (this.currentScreen !== null) return false;
6436
+ return true;
6364
6437
  }
6365
6438
  // Re-fill slots that mount after a poll (SPA route changes). Observes the DOM
6366
6439
  // once; when a new [data-aegis-slot] appears it re-runs renderIntoSlots from
@@ -6396,6 +6469,7 @@ const _AegisInAppManager = class _AegisInAppManager {
6396
6469
  this.slotObserver.observe(document.body, { childList: true, subtree: true });
6397
6470
  }
6398
6471
  renderIntoSlots() {
6472
+ var _a, _b;
6399
6473
  if (typeof document === "undefined") return;
6400
6474
  this.ensureSlotObserver();
6401
6475
  const slots = document.querySelectorAll("[data-aegis-slot]");
@@ -6408,6 +6482,16 @@ const _AegisInAppManager = class _AegisInAppManager {
6408
6482
  const category = c.widget_category;
6409
6483
  if (!modes || !modes.includes("embedded_card")) continue;
6410
6484
  if (!category) continue;
6485
+ const anchorSel = (_b = (_a = c.interactive_config) == null ? void 0 : _a.anchor) == null ? void 0 : _b.element_selector;
6486
+ if (typeof anchorSel === "string" && anchorSel.trim()) {
6487
+ let anchorResolves = false;
6488
+ try {
6489
+ anchorResolves = !!document.querySelector(anchorSel);
6490
+ } catch {
6491
+ anchorResolves = false;
6492
+ }
6493
+ if (anchorResolves) continue;
6494
+ }
6411
6495
  if (seenIds.has(c.id)) continue;
6412
6496
  seenIds.add(c.id);
6413
6497
  const arr2 = eligibleByCategory.get(category);
@@ -6434,8 +6518,8 @@ const _AegisInAppManager = class _AegisInAppManager {
6434
6518
  slot.querySelectorAll(":scope > [data-aegis-slot-default]").forEach((d) => d.remove());
6435
6519
  const attrMs = parseInt(slot.getAttribute("data-aegis-slot-rotate") || "0", 10);
6436
6520
  const declaredMs = list.reduce((m2, c) => {
6437
- var _a;
6438
- const v = Number((_a = c.interactive_config) == null ? void 0 : _a.slot_rotate_ms);
6521
+ var _a2;
6522
+ const v = Number((_a2 = c.interactive_config) == null ? void 0 : _a2.slot_rotate_ms);
6439
6523
  return Number.isFinite(v) && v > m2 ? v : m2;
6440
6524
  }, 0);
6441
6525
  const rotateMs = attrMs > 0 ? attrMs : declaredMs;
@@ -9128,6 +9212,12 @@ const _AegisInAppManager = class _AegisInAppManager {
9128
9212
  * so journeys can react. Degrades gracefully where Notification is absent.
9129
9213
  */
9130
9214
  renderPushPrimer(campaign, ic, bg, text) {
9215
+ if (typeof Notification !== "undefined" && Notification.permission !== "default") {
9216
+ void this.trackEvent(campaign.id, "dismissed", {
9217
+ meta: { reason: `permission_${Notification.permission}` }
9218
+ });
9219
+ return;
9220
+ }
9131
9221
  const overlay = this.createOverlay("aegis-in-app-push-primer-overlay");
9132
9222
  const modal = document.createElement("div");
9133
9223
  modal.style.cssText = `
@@ -9138,8 +9228,13 @@ const _AegisInAppManager = class _AegisInAppManager {
9138
9228
  const body = document.createElement("div");
9139
9229
  body.style.cssText = "padding: 24px; text-align: center;";
9140
9230
  const icon = document.createElement("div");
9141
- icon.style.cssText = "font-size: 36px; margin-bottom: 8px;";
9142
- icon.textContent = ic.icon || "🔔";
9231
+ icon.style.cssText = "margin-bottom: 8px; display: flex; justify-content: center;";
9232
+ if (typeof ic.icon === "string" && ic.icon.length > 0) {
9233
+ icon.textContent = ic.icon;
9234
+ icon.style.fontSize = "36px";
9235
+ } else {
9236
+ icon.innerHTML = lucideSvg("bell", { size: 36, color: text });
9237
+ }
9143
9238
  body.appendChild(icon);
9144
9239
  const title = document.createElement("div");
9145
9240
  title.style.cssText = "font-size: 18px; font-weight: 700; margin-bottom: 8px;";
@@ -9159,7 +9254,8 @@ const _AegisInAppManager = class _AegisInAppManager {
9159
9254
  if (overlay.parentNode) document.body.removeChild(overlay);
9160
9255
  return;
9161
9256
  }
9162
- void Promise.resolve(Notification.requestPermission()).then((perm) => {
9257
+ enableBtn.disabled = true;
9258
+ void this._subscribeFromPushPrimer().then((perm) => {
9163
9259
  void this.trackEvent(campaign.id, "clicked", { meta: { permission: perm } });
9164
9260
  if (overlay.parentNode) document.body.removeChild(overlay);
9165
9261
  });
@@ -9178,6 +9274,90 @@ const _AegisInAppManager = class _AegisInAppManager {
9178
9274
  this.addAnimationStyles();
9179
9275
  document.body.appendChild(overlay);
9180
9276
  }
9277
+ /**
9278
+ * Push-primer Enable handler — runs the SAME end-to-end subscribe flow the
9279
+ * (now-retired) ACS PushOptInBanner did, so the primer is a complete web-push
9280
+ * opt-in on every surface (ACS / Shopify / 3rd-party site), not just an OS
9281
+ * permission prompt. Steps: fetch VAPID public key → register SW → hard-ask →
9282
+ * subscribe → POST /v1/web_push/subscriptions → stitch known contact.
9283
+ *
9284
+ * Push runtime resolution prefers the already-loaded `window.AegisWebPush`
9285
+ * global (the Shopify embed lazy-loads it via /apps/active_reach/push.js, so
9286
+ * we reuse that single instance instead of registering the SW twice), and
9287
+ * only dynamic-imports the module as a fallback for hosts that didn't preload
9288
+ * it (ACS / 3rd-party sites). Returns the resolved permission string for
9289
+ * engagement reporting; falls back to a bare permission ask if push can't be
9290
+ * wired (no propertyId, no VAPID).
9291
+ */
9292
+ async _subscribeFromPushPrimer() {
9293
+ var _a, _b, _c, _d;
9294
+ if (!this.propertyId) {
9295
+ return Notification.requestPermission();
9296
+ }
9297
+ try {
9298
+ const cfgRes = await fetch(`${this.apiHost}/v1/sdk/config`, {
9299
+ headers: { "X-Aegis-Write-Key": this.writeKey }
9300
+ });
9301
+ const cfg = await cfgRes.json();
9302
+ const vapidPublicKey = ((_a = cfg.push) == null ? void 0 : _a.vapid_public_key) || "";
9303
+ if (!((_b = cfg.push) == null ? void 0 : _b.enabled) || !vapidPublicKey) {
9304
+ return Notification.requestPermission();
9305
+ }
9306
+ const serviceWorkerPath = ((_c = cfg.push) == null ? void 0 : _c.service_worker_path) || "/aegis-sw.js";
9307
+ const serviceWorkerScope = ((_d = cfg.push) == null ? void 0 : _d.service_worker_scope) || "/";
9308
+ if (this.contactId && typeof window !== "undefined") {
9309
+ try {
9310
+ window.localStorage.setItem("aegis_contact_id", this.contactId);
9311
+ } catch {
9312
+ }
9313
+ }
9314
+ const writeKey = this.writeKey;
9315
+ const apiHost = this.apiHost;
9316
+ const organizationId = this.organizationId;
9317
+ const apiClient = {
9318
+ async post(endpoint, payload, headers) {
9319
+ const res = await fetch(`${apiHost}${endpoint}`, {
9320
+ method: "POST",
9321
+ headers: {
9322
+ "Content-Type": "application/json",
9323
+ "X-Aegis-Write-Key": writeKey,
9324
+ ...organizationId ? { "X-Organization-ID": organizationId } : {},
9325
+ ...headers ?? {}
9326
+ },
9327
+ body: JSON.stringify(payload)
9328
+ });
9329
+ if (!res.ok) throw new Error(`push api ${endpoint} → ${res.status}`);
9330
+ return res.json().catch(() => ({}));
9331
+ }
9332
+ };
9333
+ const AegisWebPush2 = typeof window !== "undefined" && window.AegisWebPush || (await import("./push/AegisWebPush.js")).AegisWebPush;
9334
+ const webPush = new AegisWebPush2(apiClient, {
9335
+ vapidPublicKey,
9336
+ propertyId: this.propertyId,
9337
+ // Anon id doubles as the device-fingerprint salt so a re-subscribe
9338
+ // after VAPID rotation upserts onto the same subscription row.
9339
+ firstPartyCookieId: readAnonIdFromStorage$1() || this.contactId || this.organizationId || "",
9340
+ serviceWorkerPath,
9341
+ serviceWorkerScope,
9342
+ autoPrompt: false,
9343
+ writeKey,
9344
+ organizationId
9345
+ });
9346
+ await webPush.initialize();
9347
+ const granted = await webPush.requestPermission();
9348
+ if (granted && this.contactId) {
9349
+ await webPush.identify({ contactId: this.contactId });
9350
+ }
9351
+ return Notification.permission;
9352
+ } catch (err) {
9353
+ console.warn("[Aegis in-app] push-primer subscribe failed:", err);
9354
+ try {
9355
+ return await Notification.requestPermission();
9356
+ } catch {
9357
+ return Notification.permission;
9358
+ }
9359
+ }
9360
+ }
9181
9361
  renderQuiz(campaign, ic, bg, text) {
9182
9362
  const overlay = this.createOverlay("aegis-in-app-quiz-overlay");
9183
9363
  const modal = document.createElement("div");
@@ -11241,12 +11421,16 @@ const _AegisInAppManager = class _AegisInAppManager {
11241
11421
  // journey/template/outlet on the engagement delivery_event.
11242
11422
  journey_execution_id: trackedCampaign == null ? void 0 : trackedCampaign.journey_execution_id,
11243
11423
  lifecycle_fork_id: trackedCampaign == null ? void 0 : trackedCampaign.lifecycle_fork_id,
11244
- location_id: trackedCampaign == null ? void 0 : trackedCampaign.location_id,
11424
+ // Outlet the customer is ACTIVELY viewing (setLocation) wins — that's
11425
+ // where the impression/click actually happened; fall back to the served
11426
+ // campaign's outlet, else brand-tier. The gateway promotes this to the
11427
+ // canonical event's top-level for per-outlet in-app analytics.
11428
+ location_id: this.currentLocationId ?? (trackedCampaign == null ? void 0 : trackedCampaign.location_id),
11245
11429
  metadata: {
11246
11430
  property_id: this.propertyId,
11247
11431
  variant_id: this.getVariantId(campaignId) ?? void 0,
11248
11432
  lifecycle_fork_id: trackedCampaign == null ? void 0 : trackedCampaign.lifecycle_fork_id,
11249
- location_id: trackedCampaign == null ? void 0 : trackedCampaign.location_id,
11433
+ location_id: this.currentLocationId ?? (trackedCampaign == null ? void 0 : trackedCampaign.location_id),
11250
11434
  // Renderer-supplied metadata (e.g. anchor_resolved/anchor_selector on
11251
11435
  // an anchor_unresolved event). The gateway spreads payload.metadata
11252
11436
  // into the canonical event so these land in in_app_events.metadata.
@@ -14537,7 +14721,7 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
14537
14721
  var _a;
14538
14722
  this.initialized = false;
14539
14723
  {
14540
- const v = "1.35.2";
14724
+ const v = "1.35.4";
14541
14725
  try {
14542
14726
  globalThis.__AEGIS_SDK_VERSION__ = v;
14543
14727
  } catch {