@active-reach/web-sdk 1.35.2 → 1.36.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/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);
@@ -5899,6 +5968,11 @@ const _AegisInAppManager = class _AegisInAppManager {
5899
5968
  }
5900
5969
  if (typeof localStorage !== "undefined") {
5901
5970
  localStorage.setItem("aegis_returning_user", "1");
5971
+ try {
5972
+ const pv = parseInt(localStorage.getItem("aegis_pv_count") || "0", 10);
5973
+ localStorage.setItem("aegis_pv_count", String(pv + 1));
5974
+ } catch {
5975
+ }
5902
5976
  }
5903
5977
  ensureInAppFont();
5904
5978
  await this.refreshCampaigns();
@@ -5911,6 +5985,22 @@ const _AegisInAppManager = class _AegisInAppManager {
5911
5985
  this.log("Re-fetching campaigns for surface/screen declared during init");
5912
5986
  void this.refreshCampaigns();
5913
5987
  }
5988
+ if (typeof Notification !== "undefined" && Notification.permission === "granted" && this.propertyId) {
5989
+ let alreadyHealed = false;
5990
+ try {
5991
+ alreadyHealed = sessionStorage.getItem("aegis_push_boot_heal") === "1";
5992
+ } catch {
5993
+ }
5994
+ if (!alreadyHealed) {
5995
+ try {
5996
+ sessionStorage.setItem("aegis_push_boot_heal", "1");
5997
+ } catch {
5998
+ }
5999
+ void this._subscribeFromPushPrimer().catch((err) => {
6000
+ this.log("boot push resubscribe failed", err);
6001
+ });
6002
+ }
6003
+ }
5914
6004
  this.log("AegisInApp initialized successfully");
5915
6005
  }
5916
6006
  updateUserId(userId) {
@@ -6126,6 +6216,7 @@ const _AegisInAppManager = class _AegisInAppManager {
6126
6216
  return;
6127
6217
  }
6128
6218
  const url = new URL("/v1/stream/realtime", this.apiHost);
6219
+ url.searchParams.set("org", this.organizationId);
6129
6220
  const headers = {
6130
6221
  "X-Aegis-Write-Key": this.writeKey,
6131
6222
  "X-Organization-ID": this.organizationId
@@ -6133,11 +6224,10 @@ const _AegisInAppManager = class _AegisInAppManager {
6133
6224
  if (this.contactId) {
6134
6225
  headers["X-Contact-ID"] = this.contactId;
6135
6226
  }
6136
- const queryParams = new URLSearchParams();
6137
6227
  Object.entries(headers).forEach(([key, value]) => {
6138
- queryParams.append(key, value);
6228
+ url.searchParams.append(key, value);
6139
6229
  });
6140
- this.eventSource = new EventSource(`${url}?${queryParams.toString()}`);
6230
+ this.eventSource = new EventSource(url.toString());
6141
6231
  this.eventSource.addEventListener("open", () => {
6142
6232
  this.log("SSE connection established");
6143
6233
  this.reconnectAttempts = 0;
@@ -6300,7 +6390,7 @@ const _AegisInAppManager = class _AegisInAppManager {
6300
6390
  }
6301
6391
  tryDisplayNextCampaign() {
6302
6392
  const campaign = this.campaigns.find(
6303
- (c) => !this.displayedCampaigns.has(c.id) && !c.client_trigger && !this.isSuppressed(c.id) && c.type !== "active_web_chat"
6393
+ (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
6394
  );
6305
6395
  if (campaign) {
6306
6396
  this.displayCampaign(campaign);
@@ -6333,34 +6423,38 @@ const _AegisInAppManager = class _AegisInAppManager {
6333
6423
  * `<div data-aegis-slot="feedback">` will accept the first active
6334
6424
  * campaign with `widget_category='feedback'` + embedded_card mode.
6335
6425
  */
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);
6426
+ /** Normalize a declared free-form screen name to a canonical surface key
6427
+ * ('storefront.catalog' → 'storefront_catalog'), or null when it maps
6428
+ * outside the enum (e.g. 'shopify.product'). Mirrors the server's
6429
+ * `_canonical_surface_from_screen`. */
6430
+ canonicalSurfaceFromScreen() {
6431
+ if (!this.currentScreen) return null;
6432
+ const norm = this.currentScreen.trim().toLowerCase().replace(/[.\- ]/g, "_");
6433
+ return _AegisInAppManager.KNOWN_SURFACES.has(norm) ? norm : null;
6348
6434
  }
6349
6435
  /**
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.
6436
+ * Dynamic Screens P2 + substrate isolation — combined surface + free-form
6437
+ * screen predicate, mirroring the server's `_screen_or_surface_matches`:
6438
+ * - screen-only campaign matches strictly on `currentScreen`
6439
+ * - a declared screen NORMALIZES into the canonical tier
6440
+ * ('storefront.catalog' → storefront_catalog) so surface pins match
6441
+ * - a declared screen that maps OUTSIDE the enum (e.g. 'shopify.product')
6442
+ * does NOT fall through to the all-surfaces default for surface-pinned
6443
+ * campaigns — actii.me-pinned campaigns never render on Shopify
6444
+ * - hosts that declare nothing keep the legacy render-everything default
6355
6445
  */
6356
6446
  matchesCurrentScreenOrSurface(c) {
6357
6447
  const screens = c.target_screens;
6358
- if (!screens || screens.length === 0) {
6359
- return this.matchesCurrentSurface(c);
6448
+ const screenOk = !!screens && screens.length > 0 && this.currentScreen !== null && screens.includes(this.currentScreen);
6449
+ if (screens && screens.length > 0 && (!c.surface || c.surface.length === 0)) {
6450
+ return screenOk;
6360
6451
  }
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);
6452
+ if (screenOk) return true;
6453
+ if (!c.surface || c.surface.length === 0) return true;
6454
+ const effective = this.currentSurface ?? this.canonicalSurfaceFromScreen();
6455
+ if (effective !== null) return c.surface.includes(effective);
6456
+ if (this.currentScreen !== null) return false;
6457
+ return true;
6364
6458
  }
6365
6459
  // Re-fill slots that mount after a poll (SPA route changes). Observes the DOM
6366
6460
  // once; when a new [data-aegis-slot] appears it re-runs renderIntoSlots from
@@ -6396,6 +6490,7 @@ const _AegisInAppManager = class _AegisInAppManager {
6396
6490
  this.slotObserver.observe(document.body, { childList: true, subtree: true });
6397
6491
  }
6398
6492
  renderIntoSlots() {
6493
+ var _a, _b;
6399
6494
  if (typeof document === "undefined") return;
6400
6495
  this.ensureSlotObserver();
6401
6496
  const slots = document.querySelectorAll("[data-aegis-slot]");
@@ -6408,6 +6503,16 @@ const _AegisInAppManager = class _AegisInAppManager {
6408
6503
  const category = c.widget_category;
6409
6504
  if (!modes || !modes.includes("embedded_card")) continue;
6410
6505
  if (!category) continue;
6506
+ const anchorSel = (_b = (_a = c.interactive_config) == null ? void 0 : _a.anchor) == null ? void 0 : _b.element_selector;
6507
+ if (typeof anchorSel === "string" && anchorSel.trim()) {
6508
+ let anchorResolves = false;
6509
+ try {
6510
+ anchorResolves = !!document.querySelector(anchorSel);
6511
+ } catch {
6512
+ anchorResolves = false;
6513
+ }
6514
+ if (anchorResolves) continue;
6515
+ }
6411
6516
  if (seenIds.has(c.id)) continue;
6412
6517
  seenIds.add(c.id);
6413
6518
  const arr2 = eligibleByCategory.get(category);
@@ -6434,8 +6539,8 @@ const _AegisInAppManager = class _AegisInAppManager {
6434
6539
  slot.querySelectorAll(":scope > [data-aegis-slot-default]").forEach((d) => d.remove());
6435
6540
  const attrMs = parseInt(slot.getAttribute("data-aegis-slot-rotate") || "0", 10);
6436
6541
  const declaredMs = list.reduce((m2, c) => {
6437
- var _a;
6438
- const v = Number((_a = c.interactive_config) == null ? void 0 : _a.slot_rotate_ms);
6542
+ var _a2;
6543
+ const v = Number((_a2 = c.interactive_config) == null ? void 0 : _a2.slot_rotate_ms);
6439
6544
  return Number.isFinite(v) && v > m2 ? v : m2;
6440
6545
  }, 0);
6441
6546
  const rotateMs = attrMs > 0 ? attrMs : declaredMs;
@@ -9128,6 +9233,72 @@ const _AegisInAppManager = class _AegisInAppManager {
9128
9233
  * so journeys can react. Degrades gracefully where Notification is absent.
9129
9234
  */
9130
9235
  renderPushPrimer(campaign, ic, bg, text) {
9236
+ if (typeof Notification !== "undefined" && Notification.permission !== "default") {
9237
+ void this.trackEvent(campaign.id, "dismissed", {
9238
+ meta: { reason: `permission_${Notification.permission}` }
9239
+ });
9240
+ return;
9241
+ }
9242
+ void this._fetchPushOptInConfig().then((optIn) => {
9243
+ if (optIn.mode !== "standard") {
9244
+ void this.trackEvent(campaign.id, "dismissed", {
9245
+ meta: { reason: `opt_in_mode_${optIn.mode}` }
9246
+ });
9247
+ return;
9248
+ }
9249
+ if (!this._promptTriggerSatisfied(optIn.trigger)) {
9250
+ return;
9251
+ }
9252
+ this._renderPushPrimerModal(campaign, ic, bg, text);
9253
+ });
9254
+ }
9255
+ /**
9256
+ * Fetch the property's push opt-in mode/trigger from /v1/sdk/config
9257
+ * (browser HTTP cache + ETag make repeat calls cheap). Fail-open to
9258
+ * 'standard'/'immediate' — a config blip must not silently kill the
9259
+ * only web-push opt-in surface.
9260
+ */
9261
+ async _fetchPushOptInConfig() {
9262
+ var _a, _b;
9263
+ try {
9264
+ const res = await fetch(`${this.apiHost}/v1/sdk/config`, {
9265
+ headers: { "X-Aegis-Write-Key": this.writeKey }
9266
+ });
9267
+ const cfg = await res.json();
9268
+ return {
9269
+ mode: ((_a = cfg.push) == null ? void 0 : _a.opt_in_mode) || "standard",
9270
+ trigger: ((_b = cfg.push) == null ? void 0 : _b.prompt_trigger) || "immediate"
9271
+ };
9272
+ } catch {
9273
+ return { mode: "standard", trigger: "immediate" };
9274
+ }
9275
+ }
9276
+ /**
9277
+ * Prompt-timing gate for the standard primer.
9278
+ * immediate → always
9279
+ * second_page_view → from the 2nd page load on (localStorage counter,
9280
+ * incremented once per manager init)
9281
+ * after_first_order→ once `aegis_ordered` is set (host runtimes set it
9282
+ * on purchase; storefront checkout does)
9283
+ * manual → never auto-shows; host calls the primer explicitly
9284
+ */
9285
+ _promptTriggerSatisfied(trigger) {
9286
+ try {
9287
+ switch (trigger) {
9288
+ case "second_page_view":
9289
+ return parseInt(localStorage.getItem("aegis_pv_count") || "0", 10) >= 2;
9290
+ case "after_first_order":
9291
+ return localStorage.getItem("aegis_ordered") === "1";
9292
+ case "manual":
9293
+ return false;
9294
+ default:
9295
+ return true;
9296
+ }
9297
+ } catch {
9298
+ return true;
9299
+ }
9300
+ }
9301
+ _renderPushPrimerModal(campaign, ic, bg, text) {
9131
9302
  const overlay = this.createOverlay("aegis-in-app-push-primer-overlay");
9132
9303
  const modal = document.createElement("div");
9133
9304
  modal.style.cssText = `
@@ -9138,8 +9309,13 @@ const _AegisInAppManager = class _AegisInAppManager {
9138
9309
  const body = document.createElement("div");
9139
9310
  body.style.cssText = "padding: 24px; text-align: center;";
9140
9311
  const icon = document.createElement("div");
9141
- icon.style.cssText = "font-size: 36px; margin-bottom: 8px;";
9142
- icon.textContent = ic.icon || "🔔";
9312
+ icon.style.cssText = "margin-bottom: 8px; display: flex; justify-content: center;";
9313
+ if (typeof ic.icon === "string" && ic.icon.length > 0) {
9314
+ icon.textContent = ic.icon;
9315
+ icon.style.fontSize = "36px";
9316
+ } else {
9317
+ icon.innerHTML = lucideSvg("bell", { size: 36, color: text });
9318
+ }
9143
9319
  body.appendChild(icon);
9144
9320
  const title = document.createElement("div");
9145
9321
  title.style.cssText = "font-size: 18px; font-weight: 700; margin-bottom: 8px;";
@@ -9159,7 +9335,8 @@ const _AegisInAppManager = class _AegisInAppManager {
9159
9335
  if (overlay.parentNode) document.body.removeChild(overlay);
9160
9336
  return;
9161
9337
  }
9162
- void Promise.resolve(Notification.requestPermission()).then((perm) => {
9338
+ enableBtn.disabled = true;
9339
+ void this._subscribeFromPushPrimer().then((perm) => {
9163
9340
  void this.trackEvent(campaign.id, "clicked", { meta: { permission: perm } });
9164
9341
  if (overlay.parentNode) document.body.removeChild(overlay);
9165
9342
  });
@@ -9178,6 +9355,93 @@ const _AegisInAppManager = class _AegisInAppManager {
9178
9355
  this.addAnimationStyles();
9179
9356
  document.body.appendChild(overlay);
9180
9357
  }
9358
+ /**
9359
+ * Push-primer Enable handler — runs the SAME end-to-end subscribe flow the
9360
+ * (now-retired) ACS PushOptInBanner did, so the primer is a complete web-push
9361
+ * opt-in on every surface (ACS / Shopify / 3rd-party site), not just an OS
9362
+ * permission prompt. Steps: fetch VAPID public key → register SW → hard-ask →
9363
+ * subscribe → POST /v1/web_push/subscriptions → stitch known contact.
9364
+ *
9365
+ * Push runtime resolution prefers the already-loaded `window.AegisWebPush`
9366
+ * global (the Shopify embed lazy-loads it via /apps/active_reach/push.js, so
9367
+ * we reuse that single instance instead of registering the SW twice), and
9368
+ * only dynamic-imports the module as a fallback for hosts that didn't preload
9369
+ * it (ACS / 3rd-party sites). Returns the resolved permission string for
9370
+ * engagement reporting; falls back to a bare permission ask if push can't be
9371
+ * wired (no propertyId, no VAPID).
9372
+ */
9373
+ async _subscribeFromPushPrimer() {
9374
+ var _a, _b, _c, _d, _e;
9375
+ if (!this.propertyId) {
9376
+ return Notification.requestPermission();
9377
+ }
9378
+ try {
9379
+ const cfgRes = await fetch(`${this.apiHost}/v1/sdk/config`, {
9380
+ headers: { "X-Aegis-Write-Key": this.writeKey }
9381
+ });
9382
+ const cfg = await cfgRes.json();
9383
+ const vapidPublicKey = ((_a = cfg.push) == null ? void 0 : _a.vapid_public_key) || "";
9384
+ if (!((_b = cfg.push) == null ? void 0 : _b.enabled) || !vapidPublicKey) {
9385
+ return Notification.requestPermission();
9386
+ }
9387
+ if (((_c = cfg.push) == null ? void 0 : _c.opt_in_mode) === "off") {
9388
+ return Notification.permission;
9389
+ }
9390
+ const serviceWorkerPath = ((_d = cfg.push) == null ? void 0 : _d.service_worker_path) || "/aegis-sw.js";
9391
+ const serviceWorkerScope = ((_e = cfg.push) == null ? void 0 : _e.service_worker_scope) || "/";
9392
+ if (this.contactId && typeof window !== "undefined") {
9393
+ try {
9394
+ window.localStorage.setItem("aegis_contact_id", this.contactId);
9395
+ } catch {
9396
+ }
9397
+ }
9398
+ const writeKey = this.writeKey;
9399
+ const apiHost = this.apiHost;
9400
+ const organizationId = this.organizationId;
9401
+ const apiClient = {
9402
+ async post(endpoint, payload, headers) {
9403
+ const res = await fetch(`${apiHost}${endpoint}`, {
9404
+ method: "POST",
9405
+ headers: {
9406
+ "Content-Type": "application/json",
9407
+ "X-Aegis-Write-Key": writeKey,
9408
+ ...organizationId ? { "X-Organization-ID": organizationId } : {},
9409
+ ...headers ?? {}
9410
+ },
9411
+ body: JSON.stringify(payload)
9412
+ });
9413
+ if (!res.ok) throw new Error(`push api ${endpoint} → ${res.status}`);
9414
+ return res.json().catch(() => ({}));
9415
+ }
9416
+ };
9417
+ const AegisWebPush2 = typeof window !== "undefined" && window.AegisWebPush || (await import("./push/AegisWebPush.js")).AegisWebPush;
9418
+ const webPush = new AegisWebPush2(apiClient, {
9419
+ vapidPublicKey,
9420
+ propertyId: this.propertyId,
9421
+ // Anon id doubles as the device-fingerprint salt so a re-subscribe
9422
+ // after VAPID rotation upserts onto the same subscription row.
9423
+ firstPartyCookieId: readAnonIdFromStorage$1() || this.contactId || this.organizationId || "",
9424
+ serviceWorkerPath,
9425
+ serviceWorkerScope,
9426
+ autoPrompt: false,
9427
+ writeKey,
9428
+ organizationId
9429
+ });
9430
+ await webPush.initialize();
9431
+ const granted = await webPush.requestPermission();
9432
+ if (granted && this.contactId) {
9433
+ await webPush.identify({ contactId: this.contactId });
9434
+ }
9435
+ return Notification.permission;
9436
+ } catch (err) {
9437
+ console.warn("[Aegis in-app] push-primer subscribe failed:", err);
9438
+ try {
9439
+ return await Notification.requestPermission();
9440
+ } catch {
9441
+ return Notification.permission;
9442
+ }
9443
+ }
9444
+ }
9181
9445
  renderQuiz(campaign, ic, bg, text) {
9182
9446
  const overlay = this.createOverlay("aegis-in-app-quiz-overlay");
9183
9447
  const modal = document.createElement("div");
@@ -11241,12 +11505,16 @@ const _AegisInAppManager = class _AegisInAppManager {
11241
11505
  // journey/template/outlet on the engagement delivery_event.
11242
11506
  journey_execution_id: trackedCampaign == null ? void 0 : trackedCampaign.journey_execution_id,
11243
11507
  lifecycle_fork_id: trackedCampaign == null ? void 0 : trackedCampaign.lifecycle_fork_id,
11244
- location_id: trackedCampaign == null ? void 0 : trackedCampaign.location_id,
11508
+ // Outlet the customer is ACTIVELY viewing (setLocation) wins — that's
11509
+ // where the impression/click actually happened; fall back to the served
11510
+ // campaign's outlet, else brand-tier. The gateway promotes this to the
11511
+ // canonical event's top-level for per-outlet in-app analytics.
11512
+ location_id: this.currentLocationId ?? (trackedCampaign == null ? void 0 : trackedCampaign.location_id),
11245
11513
  metadata: {
11246
11514
  property_id: this.propertyId,
11247
11515
  variant_id: this.getVariantId(campaignId) ?? void 0,
11248
11516
  lifecycle_fork_id: trackedCampaign == null ? void 0 : trackedCampaign.lifecycle_fork_id,
11249
- location_id: trackedCampaign == null ? void 0 : trackedCampaign.location_id,
11517
+ location_id: this.currentLocationId ?? (trackedCampaign == null ? void 0 : trackedCampaign.location_id),
11250
11518
  // Renderer-supplied metadata (e.g. anchor_resolved/anchor_selector on
11251
11519
  // an anchor_unresolved event). The gateway spreads payload.metadata
11252
11520
  // into the canonical event so these land in in_app_events.metadata.
@@ -14537,7 +14805,7 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
14537
14805
  var _a;
14538
14806
  this.initialized = false;
14539
14807
  {
14540
- const v = "1.35.2";
14808
+ const v = "1.36.0";
14541
14809
  try {
14542
14810
  globalThis.__AEGIS_SDK_VERSION__ = v;
14543
14811
  } catch {