@active-reach/web-sdk 1.37.0 → 1.39.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 (54) hide show
  1. package/dist/aegis-push.min.js +1 -1
  2. package/dist/aegis-push.min.js.map +1 -1
  3. package/dist/aegis.min.js +1 -1
  4. package/dist/aegis.min.js.map +1 -1
  5. package/dist/{analytics-Qyzk8JaB.mjs → analytics-DoHdxjjh.mjs} +255 -110
  6. package/dist/analytics-DoHdxjjh.mjs.map +1 -0
  7. package/dist/core/analytics.d.ts +10 -0
  8. package/dist/core/analytics.d.ts.map +1 -1
  9. package/dist/core/prefetch-bundle-client.d.ts +1 -0
  10. package/dist/core/prefetch-bundle-client.d.ts.map +1 -1
  11. package/dist/core/queue.d.ts.map +1 -1
  12. package/dist/core/session.d.ts +12 -1
  13. package/dist/core/session.d.ts.map +1 -1
  14. package/dist/ecommerce/index.d.ts.map +1 -1
  15. package/dist/inapp/AegisInAppManager.d.ts +22 -2
  16. package/dist/inapp/AegisInAppManager.d.ts.map +1 -1
  17. package/dist/inapp/actions.d.ts +7 -0
  18. package/dist/inapp/actions.d.ts.map +1 -1
  19. package/dist/inapp/index.d.ts +1 -1
  20. package/dist/inapp/index.d.ts.map +1 -1
  21. package/dist/inapp/renderers/card-template.d.ts.map +1 -1
  22. package/dist/inapp/renderers/games.d.ts.map +1 -1
  23. package/dist/inapp/renderers/types.d.ts +13 -0
  24. package/dist/inapp/renderers/types.d.ts.map +1 -1
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +266 -40
  28. package/dist/index.js.map +1 -1
  29. package/dist/push/AegisWebPush.d.ts +19 -0
  30. package/dist/push/AegisWebPush.d.ts.map +1 -1
  31. package/dist/push/AegisWebPush.js +31 -2
  32. package/dist/push/AegisWebPush.js.map +1 -1
  33. package/dist/react.js +1 -1
  34. package/dist/runtime/AegisMessageRuntime.d.ts +1 -1
  35. package/dist/runtime/AegisMessageRuntime.d.ts.map +1 -1
  36. package/dist/triggers/ContactScoresFetcher.d.ts +2 -1
  37. package/dist/triggers/ContactScoresFetcher.d.ts.map +1 -1
  38. package/dist/triggers/IntentRuleEvaluator.d.ts +1 -1
  39. package/dist/triggers/IntentRuleEvaluator.d.ts.map +1 -1
  40. package/dist/triggers/TriggerEngine.d.ts +5 -0
  41. package/dist/triggers/TriggerEngine.d.ts.map +1 -1
  42. package/dist/utils/device.d.ts.map +1 -1
  43. package/dist/utils/meta-cookies.d.ts +7 -0
  44. package/dist/utils/meta-cookies.d.ts.map +1 -1
  45. package/dist/utils/redact-url.d.ts +56 -0
  46. package/dist/utils/redact-url.d.ts.map +1 -0
  47. package/dist/utils/url-parser.d.ts +38 -0
  48. package/dist/utils/url-parser.d.ts.map +1 -1
  49. package/dist/utils/visitor.d.ts +43 -0
  50. package/dist/utils/visitor.d.ts.map +1 -0
  51. package/dist/widgets/AegisWidgetManager.d.ts +17 -0
  52. package/dist/widgets/AegisWidgetManager.d.ts.map +1 -1
  53. package/package.json +1 -1
  54. package/dist/analytics-Qyzk8JaB.mjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { S as Storage, l as logger, A as Aegis } from "./analytics-Qyzk8JaB.mjs";
2
- import { B, E, N, R, U, m } from "./analytics-Qyzk8JaB.mjs";
1
+ import { S as Storage, r as redactUrl, l as logger, A as Aegis } from "./analytics-DoHdxjjh.mjs";
2
+ import { B, E, N, R, U, m } from "./analytics-DoHdxjjh.mjs";
3
3
  import { AegisWebPush } from "./push/AegisWebPush.js";
4
4
  function debounce(func, wait) {
5
5
  let timeoutId = null;
@@ -26,6 +26,13 @@ function throttle(func, limit) {
26
26
  };
27
27
  }
28
28
  const PDP_SKU_RE = /\/p\/([^/?#]+)/;
29
+ function productPath(sku) {
30
+ const encoded = encodeURIComponent(sku).replace(
31
+ /[!'()*]/g,
32
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
33
+ );
34
+ return `/p/${encoded}`;
35
+ }
29
36
  function parseAction(actionUrl, meta) {
30
37
  const url = (actionUrl ?? "").trim();
31
38
  const m2 = meta ?? {};
@@ -61,7 +68,7 @@ function parseAction(actionUrl, meta) {
61
68
  case "open-product":
62
69
  case "product": {
63
70
  const sku = params.get("sku") || m2.sku || void 0;
64
- return { type: "open_product", sku, productId: m2.product_id, url: sku ? `/p/${sku}` : void 0 };
71
+ return { type: "open_product", sku, productId: m2.product_id, url: sku ? productPath(sku) : void 0 };
65
72
  }
66
73
  default:
67
74
  return { type: "custom", name: verb || "unknown", payload: Object.fromEntries(params) };
@@ -96,7 +103,7 @@ function parseAction(actionUrl, meta) {
96
103
  }
97
104
  return { type: "dismiss" };
98
105
  }
99
- function fallbackUrlForAction(a, productUrl = (sku) => `/p/${encodeURIComponent(sku)}`) {
106
+ function fallbackUrlForAction(a, productUrl = productPath) {
100
107
  switch (a.type) {
101
108
  case "navigate":
102
109
  return a.url;
@@ -652,9 +659,11 @@ function appendRatingThanks(wrap, el2, ctx, fg, accent, isHigh) {
652
659
  cta.textContent = el2.rating_high_cta || "Leave a review";
653
660
  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;`;
654
661
  cta.addEventListener("click", () => {
662
+ var _a;
655
663
  ctx.trackEvent(ctx.campaign.id, "clicked", {
656
664
  meta: { element: "rating_input", cta: "high_review" }
657
665
  });
666
+ void ((_a = ctx.submitResponse) == null ? void 0 : _a.call(ctx, "review_redirect", { cta: "high_review" }));
658
667
  if (ctx.navigate) ctx.navigate(url, "cta");
659
668
  else window.location.href = url;
660
669
  });
@@ -821,6 +830,7 @@ function buildProductRatingListElement(el2, ctx, fg) {
821
830
  const orderId = slot.getAttribute("data-order-id");
822
831
  const billCode = slot.getAttribute("data-bill-code");
823
832
  const token = slot.getAttribute("data-token");
833
+ const reviewsViaPage = Boolean(slot.getAttribute("data-reviews-url"));
824
834
  const reviewsUrl = slot.getAttribute("data-reviews-url") || el2.rating_high_url || null;
825
835
  const scale = typeof el2.rating_scale === "number" && el2.rating_scale >= 2 && el2.rating_scale <= 10 ? Math.floor(el2.rating_scale) : 5;
826
836
  const showThumb = el2.show_thumbnail !== false && el2.rating_layout !== "compact";
@@ -1071,6 +1081,12 @@ function buildProductRatingListElement(el2, ctx, fg) {
1071
1081
  a.href = ctx.sanitizeUrl(reviewsUrl) || "#";
1072
1082
  a.textContent = el2.rating_high_cta || "Share a review";
1073
1083
  a.style.cssText = `display:inline-block;margin-top:6px;font-size:12px;font-weight:700;color:${accent};text-decoration:none;`;
1084
+ if (!reviewsViaPage) {
1085
+ a.addEventListener("click", () => {
1086
+ var _a2;
1087
+ void ((_a2 = ctx.submitResponse) == null ? void 0 : _a2.call(ctx, "review_redirect", { cta: "item_rating_review" }));
1088
+ });
1089
+ }
1074
1090
  ack.appendChild(a);
1075
1091
  }
1076
1092
  wrap.appendChild(ack);
@@ -1247,9 +1263,11 @@ function appendNpsThanks(wrap, el2, ctx, fg, accent, isPromoter) {
1247
1263
  cta.textContent = el2.nps_promoter_cta || "Leave a review";
1248
1264
  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;`;
1249
1265
  cta.addEventListener("click", () => {
1266
+ var _a;
1250
1267
  ctx.trackEvent(ctx.campaign.id, "clicked", {
1251
1268
  meta: { element: "nps", cta: "promoter_review" }
1252
1269
  });
1270
+ void ((_a = ctx.submitResponse) == null ? void 0 : _a.call(ctx, "review_redirect", { cta: "promoter_review" }));
1253
1271
  if (ctx.navigate) ctx.navigate(url, "cta");
1254
1272
  else window.location.href = url;
1255
1273
  });
@@ -1393,8 +1411,11 @@ function buildProgressElement(el2, ctx, accent, muted) {
1393
1411
  return code && M[code] || "";
1394
1412
  };
1395
1413
  const threshold = () => {
1414
+ var _a;
1396
1415
  if (typeof el2.threshold === "number" && el2.threshold > 0) return el2.threshold;
1397
1416
  if (el2.threshold_source === "free_delivery_setting") {
1417
+ const fromCart = ctx.getCartState && ((_a = ctx.getCartState()) == null ? void 0 : _a.freeDeliveryAbove);
1418
+ if (typeof fromCart === "number" && fromCart > 0) return fromCart;
1398
1419
  const v = Number(props()["cart.free_delivery_above"]);
1399
1420
  if (v > 0) return v;
1400
1421
  }
@@ -1429,6 +1450,11 @@ function buildProgressElement(el2, ctx, accent, muted) {
1429
1450
  var _a;
1430
1451
  const th = threshold();
1431
1452
  const cur = readCur();
1453
+ if (th <= 0 && el2.threshold_source === "free_delivery_setting") {
1454
+ wrap.style.display = "none";
1455
+ return;
1456
+ }
1457
+ wrap.style.display = "flex";
1432
1458
  const { pct, complete } = computeProgress({ threshold: th, current: cur });
1433
1459
  bar.style.width = `${pct}%`;
1434
1460
  const labeled = cardProgressSubLabel({
@@ -3195,10 +3221,11 @@ function fillBar(accent, pct, h = 8) {
3195
3221
  }
3196
3222
  function renderStreak(ctx) {
3197
3223
  const ic = ctx.campaign.interactive_config || {};
3224
+ const count = num(ic.streak_count, 0);
3225
+ if (count <= 0) return;
3198
3226
  const s = openGameModal(ctx, "aegis-in-app-streak");
3199
- const count = num(ic.streak_count, 5);
3200
3227
  const goal = num(ic.streak_goal, count + 1);
3201
- const reward = str$1(ic.streak_reward_text, "Come back tomorrow to keep it going.");
3228
+ const reward = str$1(ic.streak_reward_text, "Order today to keep it going.");
3202
3229
  const days = ["M", "T", "W", "T", "F", "S", "S"];
3203
3230
  s.content.appendChild(gEl("div", "font-size:44px;line-height:1", "🔥"));
3204
3231
  s.content.appendChild(gEl("div", "font-size:22px;font-weight:800;margin:4px 0 2px", `${count}-day streak!`));
@@ -3215,11 +3242,13 @@ function renderStreak(ctx) {
3215
3242
  note.appendChild(gEl("b", "", `Day ${goal}`));
3216
3243
  note.appendChild(document.createTextNode(" unlocks a reward."));
3217
3244
  s.content.appendChild(note);
3218
- s.ctaButton(str$1(ic.streak_cta_text, "Keep my streak"), () => {
3245
+ const hasUrl = Boolean(str$1(ctx.campaign.action_url));
3246
+ s.ctaButton(str$1(ic.streak_cta_text, hasUrl ? "Order today" : "Got it"), () => {
3219
3247
  var _a, _b, _c;
3220
3248
  (_a = ctx.haptic) == null ? void 0 : _a.call(ctx, 20);
3221
3249
  (_b = ctx.playReaction) == null ? void 0 : _b.call(ctx, s.card, "affirm");
3222
3250
  void ((_c = ctx.submitResponse) == null ? void 0 : _c.call(ctx, "streak", { streak_count: count }));
3251
+ if (!hasUrl) s.dismiss();
3223
3252
  });
3224
3253
  }
3225
3254
  function renderMilestone(ctx) {
@@ -4937,7 +4966,7 @@ class TriggerEngine {
4937
4966
  this.exitIntentFired = true;
4938
4967
  this.emit("exit_intent", {
4939
4968
  client_y: event.clientY,
4940
- page_url: window.location.href,
4969
+ page_url: redactUrl(window.location.href),
4941
4970
  time_on_page: this.pageLoadTime ? (Date.now() - this.pageLoadTime) / 1e3 : 0
4942
4971
  });
4943
4972
  }
@@ -4958,7 +4987,7 @@ class TriggerEngine {
4958
4987
  scroll_velocity: velocity,
4959
4988
  scroll_distance: distance,
4960
4989
  current_position: currentY,
4961
- page_url: window.location.href,
4990
+ page_url: redactUrl(window.location.href),
4962
4991
  time_on_page: this.pageLoadTime ? (Date.now() - this.pageLoadTime) / 1e3 : 0
4963
4992
  });
4964
4993
  this.scrollVelocityCooldownTimer = window.setTimeout(() => {
@@ -4972,12 +5001,12 @@ class TriggerEngine {
4972
5001
  this.handleVisibilityChange = () => {
4973
5002
  if (document.hidden) {
4974
5003
  this.emit("visibility_hidden", {
4975
- page_url: window.location.href,
5004
+ page_url: redactUrl(window.location.href),
4976
5005
  time_on_page: this.pageLoadTime ? (Date.now() - this.pageLoadTime) / 1e3 : 0
4977
5006
  });
4978
5007
  } else {
4979
5008
  this.emit("visibility_visible", {
4980
- page_url: window.location.href
5009
+ page_url: redactUrl(window.location.href)
4981
5010
  });
4982
5011
  }
4983
5012
  };
@@ -4988,7 +5017,7 @@ class TriggerEngine {
4988
5017
  this.backButtonFired = true;
4989
5018
  window.history.pushState(null, "", window.location.href);
4990
5019
  this.emit("back_button", {
4991
- page_url: window.location.href,
5020
+ page_url: redactUrl(window.location.href),
4992
5021
  time_on_page: this.pageLoadTime ? (Date.now() - this.pageLoadTime) / 1e3 : 0
4993
5022
  });
4994
5023
  };
@@ -5016,7 +5045,7 @@ class TriggerEngine {
5016
5045
  const timerId = window.setTimeout(() => {
5017
5046
  this.emit(`time_on_page_${seconds}`, {
5018
5047
  seconds,
5019
- page_url: window.location.href
5048
+ page_url: redactUrl(window.location.href)
5020
5049
  });
5021
5050
  this.timeOnPageTargets.delete(seconds);
5022
5051
  }, seconds * 1e3);
@@ -5936,6 +5965,54 @@ function renderReviewsOverlay(ctx, creds) {
5936
5965
  document.body.appendChild(overlay);
5937
5966
  renderReviewsInline(ctx, body, creds);
5938
5967
  }
5968
+ const RETURNING_MARKER_KEY = "aegis_returning_user";
5969
+ const SESSION_VERDICT_KEY = "aegis_new_visitor_session";
5970
+ let cachedVerdict = null;
5971
+ function readSessionVerdict() {
5972
+ try {
5973
+ if (typeof sessionStorage === "undefined") return null;
5974
+ const stored = sessionStorage.getItem(SESSION_VERDICT_KEY);
5975
+ if (stored === "1") return true;
5976
+ if (stored === "0") return false;
5977
+ return null;
5978
+ } catch {
5979
+ return null;
5980
+ }
5981
+ }
5982
+ function writeSessionVerdict(verdict) {
5983
+ try {
5984
+ if (typeof sessionStorage === "undefined") return;
5985
+ sessionStorage.setItem(SESSION_VERDICT_KEY, verdict ? "1" : "0");
5986
+ } catch {
5987
+ }
5988
+ }
5989
+ function readMarkerVerdict() {
5990
+ try {
5991
+ if (typeof localStorage === "undefined") return false;
5992
+ return localStorage.getItem(RETURNING_MARKER_KEY) === null;
5993
+ } catch {
5994
+ return false;
5995
+ }
5996
+ }
5997
+ function isNewVisitor() {
5998
+ if (cachedVerdict !== null) return cachedVerdict;
5999
+ const sessionVerdict = readSessionVerdict();
6000
+ if (sessionVerdict !== null) {
6001
+ cachedVerdict = sessionVerdict;
6002
+ return cachedVerdict;
6003
+ }
6004
+ cachedVerdict = readMarkerVerdict();
6005
+ writeSessionVerdict(cachedVerdict);
6006
+ return cachedVerdict;
6007
+ }
6008
+ function markReturningVisitor() {
6009
+ isNewVisitor();
6010
+ try {
6011
+ if (typeof localStorage === "undefined") return;
6012
+ localStorage.setItem(RETURNING_MARKER_KEY, "1");
6013
+ } catch {
6014
+ }
6015
+ }
5939
6016
  function readAnonIdFromStorage$1() {
5940
6017
  if (typeof document === "undefined") return void 0;
5941
6018
  try {
@@ -5945,6 +6022,11 @@ function readAnonIdFromStorage$1() {
5945
6022
  return void 0;
5946
6023
  }
5947
6024
  }
6025
+ const JOURNEY_ACK_ACTION = {
6026
+ impression: "viewed",
6027
+ clicked: "clicked",
6028
+ dismissed: "dismissed"
6029
+ };
5948
6030
  function svgNode(svg) {
5949
6031
  try {
5950
6032
  const doc = new DOMParser().parseFromString(svg, "image/svg+xml");
@@ -5956,6 +6038,7 @@ function svgNode(svg) {
5956
6038
  }
5957
6039
  }
5958
6040
  const productIdentityCache = /* @__PURE__ */ new WeakMap();
6041
+ const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5959
6042
  function productIdentitiesFor(campaign) {
5960
6043
  const ic = campaign == null ? void 0 : campaign.interactive_config;
5961
6044
  const cards = ic && Array.isArray(ic.cards) ? ic.cards : [];
@@ -5966,11 +6049,18 @@ function productIdentitiesFor(campaign) {
5966
6049
  const seen = /* @__PURE__ */ new Set();
5967
6050
  for (const card of cards.slice(0, 24)) {
5968
6051
  const meta = (card == null ? void 0 : card.metadata) || {};
5969
- const identity = String(meta.product_id || "").trim() || String(meta.sku || "").trim() || String(meta.platform_product_id || "").trim();
6052
+ const rawId = String(meta.product_id || "").trim();
6053
+ const rawSku = String(meta.sku || "").trim();
6054
+ const platformId = String(meta.platform_product_id || "").trim();
6055
+ const productId = UUID_SHAPE.test(rawId) ? rawId : "";
6056
+ const sku = rawSku && !UUID_SHAPE.test(rawSku) ? rawSku : rawId && !productId ? rawId : "";
6057
+ const identity = productId || sku || platformId;
5970
6058
  if (!identity || seen.has(identity)) continue;
5971
6059
  seen.add(identity);
5972
6060
  items.push({
5973
- product_id: identity,
6061
+ ...productId ? { product_id: productId } : {},
6062
+ ...sku ? { sku } : {},
6063
+ ...!productId && !sku && platformId ? { platform_product_id: platformId } : {},
5974
6064
  name: String((card == null ? void 0 : card.title) || identity),
5975
6065
  quantity: 1
5976
6066
  });
@@ -5981,7 +6071,7 @@ function productIdentitiesFor(campaign) {
5981
6071
  }
5982
6072
  const _AegisInAppManager = class _AegisInAppManager {
5983
6073
  constructor(config) {
5984
- this.productUrl = (sku) => `/p/${encodeURIComponent(sku)}`;
6074
+ this.productUrl = productPath;
5985
6075
  this.campaigns = [];
5986
6076
  this.displayedCampaigns = /* @__PURE__ */ new Set();
5987
6077
  this._displaySizeMul = 1;
@@ -6007,10 +6097,13 @@ const _AegisInAppManager = class _AegisInAppManager {
6007
6097
  this.slotUnfilledBeaconed = /* @__PURE__ */ new Set();
6008
6098
  this.slotUnfilledSweepTimer = null;
6009
6099
  this._pushOptInLoading = false;
6100
+ this.journeyStepByCampaign = /* @__PURE__ */ new Map();
6101
+ this.journeyAcks = /* @__PURE__ */ new Set();
6010
6102
  this.writeKey = config.writeKey;
6011
6103
  this.apiHost = config.apiHost || "https://api.aegis.ai";
6012
6104
  this.userId = config.userId ?? readAnonIdFromStorage$1();
6013
6105
  this.contactId = config.contactId;
6106
+ this.contactToken = config.contactToken;
6014
6107
  this.organizationId = config.organizationId;
6015
6108
  this.propertyId = config.propertyId;
6016
6109
  this.debugMode = config.debugMode || false;
@@ -6112,13 +6205,13 @@ const _AegisInAppManager = class _AegisInAppManager {
6112
6205
  this.log("AegisInApp already initialized");
6113
6206
  return;
6114
6207
  }
6115
- if (typeof localStorage !== "undefined") {
6116
- localStorage.setItem("aegis_returning_user", "1");
6117
- try {
6208
+ markReturningVisitor();
6209
+ try {
6210
+ if (typeof localStorage !== "undefined") {
6118
6211
  const pv = parseInt(localStorage.getItem("aegis_pv_count") || "0", 10);
6119
6212
  localStorage.setItem("aegis_pv_count", String(pv + 1));
6120
- } catch {
6121
6213
  }
6214
+ } catch {
6122
6215
  }
6123
6216
  ensureInAppFont();
6124
6217
  await this.refreshCampaigns();
@@ -6225,9 +6318,10 @@ const _AegisInAppManager = class _AegisInAppManager {
6225
6318
  this.userId = userId;
6226
6319
  this.refreshCampaigns();
6227
6320
  }
6228
- updateContactId(contactId) {
6321
+ updateContactId(contactId, contactToken) {
6229
6322
  var _a;
6230
6323
  this.contactId = contactId;
6324
+ this.contactToken = contactToken;
6231
6325
  (_a = this.activeWebChat) == null ? void 0 : _a.updateContactId(contactId);
6232
6326
  this.disconnectSSE();
6233
6327
  if (this.enableSSE && this.organizationId) {
@@ -6495,9 +6589,11 @@ const _AegisInAppManager = class _AegisInAppManager {
6495
6589
  try {
6496
6590
  const context = new URLSearchParams({
6497
6591
  device_type: this.detectDeviceType(),
6498
- page_url: typeof window !== "undefined" ? window.location.pathname + window.location.search : "/"
6592
+ // Credential params keep their key and lose their value, so a
6593
+ // `*placed=1*` / `*?step=cart*` rule still matches.
6594
+ page_url: typeof window !== "undefined" ? redactUrl(window.location.pathname + window.location.search) : "/"
6499
6595
  });
6500
- context.set("is_new_user", this.isNewUser() ? "true" : "false");
6596
+ context.set("is_new_user", isNewVisitor() ? "true" : "false");
6501
6597
  if (this.currentSurface) {
6502
6598
  context.set("current_surface", this.currentSurface);
6503
6599
  }
@@ -6579,28 +6675,29 @@ const _AegisInAppManager = class _AegisInAppManager {
6579
6675
  if (/iPad|Tablet/i.test(ua)) return "tablet";
6580
6676
  return "desktop";
6581
6677
  }
6582
- isNewUser() {
6583
- if (typeof localStorage === "undefined") return true;
6584
- return !localStorage.getItem("aegis_returning_user");
6585
- }
6586
6678
  // --- A/B Assignment Persistence (Critical Fix C) ---
6679
+ // Both A/B helpers sit on the campaign-fetch path, so blocked storage
6680
+ // (where even `typeof localStorage` throws) must not abort the fetch.
6587
6681
  getABAssignments() {
6588
- if (typeof localStorage === "undefined") return {};
6589
6682
  try {
6683
+ if (typeof localStorage === "undefined") return {};
6590
6684
  return JSON.parse(localStorage.getItem("aegis_ab_assignments") || "{}");
6591
6685
  } catch {
6592
6686
  return {};
6593
6687
  }
6594
6688
  }
6595
6689
  processABAssignments(campaigns) {
6596
- if (typeof localStorage === "undefined") return;
6597
6690
  const stored = this.getABAssignments();
6598
6691
  for (const campaign of campaigns) {
6599
6692
  if (campaign.assigned_variant_id) {
6600
6693
  stored[campaign.id] = campaign.assigned_variant_id;
6601
6694
  }
6602
6695
  }
6603
- localStorage.setItem("aegis_ab_assignments", JSON.stringify(stored));
6696
+ try {
6697
+ if (typeof localStorage === "undefined") return;
6698
+ localStorage.setItem("aegis_ab_assignments", JSON.stringify(stored));
6699
+ } catch {
6700
+ }
6604
6701
  }
6605
6702
  getVariantId(campaignId) {
6606
6703
  const assignments = this.getABAssignments();
@@ -7454,6 +7551,23 @@ const _AegisInAppManager = class _AegisInAppManager {
7454
7551
  b.textContent = campaign.body;
7455
7552
  body.appendChild(b);
7456
7553
  }
7554
+ const _profileKeys = /* @__PURE__ */ new Set(["first_name", "email", "date_of_birth"]);
7555
+ const _collectsProfile = fields.some(
7556
+ (f) => typeof f.maps_to === "string" && _profileKeys.has(f.maps_to)
7557
+ );
7558
+ if (_collectsProfile && this.organizationId && this.writeKey) {
7559
+ const earn = document.createElement("div");
7560
+ earn.style.cssText = "font-size: 11px; font-weight: 600; opacity: 0.75; margin-top: -4px;";
7561
+ body.appendChild(earn);
7562
+ void fetch(`${this.apiHost}/v1/in-app/earn-hint?rule_type=profile_complete`, {
7563
+ headers: { "X-Aegis-Write-Key": this.writeKey, "X-Organization-ID": this.organizationId }
7564
+ }).then((r) => r.ok ? r.json() : null).then((d) => {
7565
+ if (!d || d.enabled !== true || !(Number(d.points) > 0)) return;
7566
+ const name = typeof d.points_name === "string" ? d.points_name : "points";
7567
+ earn.textContent = `Earn ${Number(d.points)} ${name} for completing your profile`;
7568
+ }).catch(() => {
7569
+ });
7570
+ }
7457
7571
  const inputs = /* @__PURE__ */ new Map();
7458
7572
  const requiredKeys = /* @__PURE__ */ new Set();
7459
7573
  const ctrlCss = `padding: 9px 11px; border-radius: 9px; border: 1px solid ${text}33; background: ${text}0d; color: ${text}; font-size: 13px; width: 100%; box-sizing: border-box; font-family: inherit;`;
@@ -7560,6 +7674,7 @@ const _AegisInAppManager = class _AegisInAppManager {
7560
7674
  "X-Organization-ID": this.organizationId
7561
7675
  };
7562
7676
  if (this.contactId) headers["X-Contact-ID"] = this.contactId;
7677
+ if (this.contactId && this.contactToken) headers["X-Contact-Token"] = this.contactToken;
7563
7678
  const variantId = campaign.assigned_variant_id;
7564
7679
  const res = await fetch(`${this.apiHost}/v1/in-app/responses`, {
7565
7680
  method: "POST",
@@ -7569,6 +7684,9 @@ const _AegisInAppManager = class _AegisInAppManager {
7569
7684
  campaign_id: campaign.id,
7570
7685
  response_type: "form",
7571
7686
  contact_id: this.contactId,
7687
+ // The device id, so a play before login joins the person once they
7688
+ // log in (RP1, Aman 2026-09-27). `user_id` may be the brand's own id.
7689
+ anonymous_id: readAnonIdFromStorage$1(),
7572
7690
  user_id: this.userId,
7573
7691
  variant_id: variantId,
7574
7692
  platform: "web",
@@ -7595,6 +7713,7 @@ const _AegisInAppManager = class _AegisInAppManager {
7595
7713
  "X-Organization-ID": this.organizationId
7596
7714
  };
7597
7715
  if (this.contactId) headers["X-Contact-ID"] = this.contactId;
7716
+ if (this.contactId && this.contactToken) headers["X-Contact-Token"] = this.contactToken;
7598
7717
  const variantId = campaign.assigned_variant_id;
7599
7718
  const res = await fetch(`${this.apiHost}/v1/in-app/responses`, {
7600
7719
  method: "POST",
@@ -7604,6 +7723,9 @@ const _AegisInAppManager = class _AegisInAppManager {
7604
7723
  campaign_id: campaign.id,
7605
7724
  response_type: responseType,
7606
7725
  contact_id: this.contactId,
7726
+ // The device id, so a play before login joins the person once they
7727
+ // log in (RP1, Aman 2026-09-27). `user_id` may be the brand's own id.
7728
+ anonymous_id: readAnonIdFromStorage$1(),
7607
7729
  user_id: this.userId,
7608
7730
  variant_id: variantId,
7609
7731
  platform: "web",
@@ -11797,12 +11919,14 @@ const _AegisInAppManager = class _AegisInAppManager {
11797
11919
  if (this.userId) headers["X-User-ID"] = this.userId;
11798
11920
  if (this.contactId) headers["X-Contact-ID"] = this.contactId;
11799
11921
  const workspaceId = (_a = this.getWorkspaceId) == null ? void 0 : _a.call(this);
11922
+ const storedAnonId = readAnonIdFromStorage$1();
11923
+ const identifiedUserId = this.userId && this.userId !== storedAnonId ? this.userId : void 0;
11800
11924
  const body = {
11801
11925
  campaign_id: campaignId,
11802
11926
  event_type: eventType,
11803
- user_id: this.userId,
11927
+ user_id: identifiedUserId,
11804
11928
  contact_id: this.contactId,
11805
- anonymous_id: this.userId,
11929
+ anonymous_id: storedAnonId,
11806
11930
  platform: "web",
11807
11931
  workspace_id: workspaceId,
11808
11932
  // Per-surface traffic (PROPERTY_IN_APP_TAB_SURFACE_BREAKDOWN_TRACKER
@@ -11864,6 +11988,67 @@ const _AegisInAppManager = class _AegisInAppManager {
11864
11988
  } catch (error) {
11865
11989
  this.log(`Error tracking event: ${error}`, "error");
11866
11990
  }
11991
+ const step = (trackedCampaign == null ? void 0 : trackedCampaign.journey_step_execution_id) || this.journeyStepByCampaign.get(campaignId);
11992
+ const ackAction = JOURNEY_ACK_ACTION[eventType];
11993
+ if (step) {
11994
+ this.journeyStepByCampaign.set(campaignId, step);
11995
+ if (ackAction) void this.ackJourneyQueueItem(step, ackAction);
11996
+ }
11997
+ }
11998
+ /**
11999
+ * POST /v1/in-app/widgets/{step}/ack — the canonical writer of a queue row's
12000
+ * seen-state (the same endpoint the Android and iOS SDKs call). Fail-soft by
12001
+ * construction: it runs after the render, is never awaited by it, and never
12002
+ * throws. A network error or 5xx is retried once; if that also fails the key
12003
+ * is released, so the next render of the (still re-served) message tries
12004
+ * again. A 4xx is the server's answer and is not retried. The server side is
12005
+ * idempotent — a repeated action is answered `late_arrival` and writes
12006
+ * nothing — so a duplicate ack is harmless.
12007
+ */
12008
+ async ackJourneyQueueItem(step, action, attempt = 0) {
12009
+ const key = `${step}:${action}`;
12010
+ if (attempt === 0) {
12011
+ if (this.journeyAcks.has(key)) return;
12012
+ this.journeyAcks.add(key);
12013
+ }
12014
+ const headers = {
12015
+ "Content-Type": "application/json",
12016
+ "X-Aegis-Write-Key": this.writeKey
12017
+ };
12018
+ if (this.organizationId) headers["X-Organization-ID"] = this.organizationId;
12019
+ if (this.userId) headers["X-User-ID"] = this.userId;
12020
+ if (this.contactId) headers["X-Contact-ID"] = this.contactId;
12021
+ let retryable = true;
12022
+ try {
12023
+ const response = await fetch(
12024
+ `${this.apiHost}/v1/in-app/widgets/${encodeURIComponent(step)}/ack`,
12025
+ {
12026
+ method: "POST",
12027
+ headers,
12028
+ body: JSON.stringify({ action }),
12029
+ credentials: "include",
12030
+ // A click usually navigates away at once; keepalive lets the ack
12031
+ // outlive the page instead of being cancelled with it.
12032
+ keepalive: true
12033
+ }
12034
+ );
12035
+ if (response.ok) {
12036
+ this.log(`Acknowledged queued message ${step}: ${action}`);
12037
+ return;
12038
+ }
12039
+ retryable = response.status >= 500 || response.status === 408 || response.status === 429;
12040
+ this.log(`Queue ack ${action} for ${step} failed: HTTP ${response.status}`, "warn");
12041
+ } catch (error) {
12042
+ this.log(`Queue ack ${action} for ${step} failed: ${error}`, "warn");
12043
+ }
12044
+ if (!retryable) return;
12045
+ if (attempt === 0) {
12046
+ setTimeout(() => {
12047
+ void this.ackJourneyQueueItem(step, action, 1);
12048
+ }, _AegisInAppManager.JOURNEY_ACK_RETRY_MS);
12049
+ return;
12050
+ }
12051
+ this.journeyAcks.delete(key);
11867
12052
  }
11868
12053
  log(message, level = "log") {
11869
12054
  if (this.debugMode) {
@@ -11914,6 +12099,7 @@ _AegisInAppManager.KNOWN_SURFACES = /* @__PURE__ */ new Set([
11914
12099
  "external_mobile_screen"
11915
12100
  ]);
11916
12101
  _AegisInAppManager.SERVED_DEDUP_PREFIX = "aegis_served_fired:";
12102
+ _AegisInAppManager.JOURNEY_ACK_RETRY_MS = 2e3;
11917
12103
  let AegisInAppManager = _AegisInAppManager;
11918
12104
  function renderPreview(config, opts) {
11919
12105
  document.querySelectorAll(
@@ -12632,9 +12818,10 @@ class PrefetchBundleClient {
12632
12818
  const ctx = this.contextProvider();
12633
12819
  const qs = new URLSearchParams();
12634
12820
  if (ctx.device_type) qs.set("device_type", ctx.device_type);
12635
- if (ctx.page_url) qs.set("page_url", ctx.page_url);
12821
+ if (ctx.page_url) qs.set("page_url", redactUrl(ctx.page_url));
12636
12822
  if (ctx.geo) qs.set("geo", ctx.geo);
12637
- qs.set("is_new_user", ctx.is_new_user ? "true" : "false");
12823
+ const isNewUser = ctx.is_new_user ?? isNewVisitor();
12824
+ qs.set("is_new_user", isNewUser ? "true" : "false");
12638
12825
  const url = `${this.apiHost}/v1/sdk/prefetch-bundle?${qs.toString()}`;
12639
12826
  const headers = {
12640
12827
  "X-Aegis-Write-Key": this.writeKey,
@@ -13516,8 +13703,8 @@ class AegisWidgetManager {
13516
13703
  cart_items: this.cartData.cart_items,
13517
13704
  user_email: this.userId,
13518
13705
  abandoned_at: (/* @__PURE__ */ new Date()).toISOString(),
13519
- page_url: window.location.href,
13520
- referrer: document.referrer
13706
+ page_url: redactUrl(window.location.href),
13707
+ referrer: redactUrl(document.referrer)
13521
13708
  };
13522
13709
  const beaconUrl = `${this.apiHost}/v1/widgets/beacon/cart-abandoned`;
13523
13710
  if (navigator.sendBeacon) {
@@ -13916,6 +14103,37 @@ class AegisWidgetManager {
13916
14103
  box-sizing: border-box;
13917
14104
  `;
13918
14105
  form.appendChild(nameInput);
14106
+ const collectBirthday = config.collect_birthday === true;
14107
+ let birthdayInput = null;
14108
+ if (collectBirthday) {
14109
+ const birthdayId = `aegis-spin-dob-${widgetId}`;
14110
+ const birthdayLabel = document.createElement("label");
14111
+ birthdayLabel.htmlFor = birthdayId;
14112
+ birthdayLabel.textContent = "Birthday (optional) — for a birthday treat";
14113
+ birthdayLabel.style.cssText = `
14114
+ display: block;
14115
+ font-size: 12px;
14116
+ font-weight: 600;
14117
+ opacity: 0.75;
14118
+ margin-bottom: 4px;
14119
+ color: ${this.sanitizeColor(textColor)};
14120
+ `;
14121
+ form.appendChild(birthdayLabel);
14122
+ birthdayInput = document.createElement("input");
14123
+ birthdayInput.id = birthdayId;
14124
+ birthdayInput.type = "date";
14125
+ birthdayInput.max = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
14126
+ birthdayInput.style.cssText = `
14127
+ width: 100%;
14128
+ padding: 14px;
14129
+ border: 2px solid #ddd;
14130
+ border-radius: 8px;
14131
+ font-size: 16px;
14132
+ margin-bottom: 12px;
14133
+ box-sizing: border-box;
14134
+ `;
14135
+ form.appendChild(birthdayInput);
14136
+ }
13919
14137
  const submitButton = document.createElement("button");
13920
14138
  submitButton.type = "submit";
13921
14139
  submitButton.textContent = "Spin the Wheel!";
@@ -13946,6 +14164,7 @@ class AegisWidgetManager {
13946
14164
  const phone = phoneInput.value.trim();
13947
14165
  const email = emailInput.value.trim();
13948
14166
  const name = nameInput.value.trim();
14167
+ const dateOfBirth = birthdayInput ? birthdayInput.value.trim() : "";
13949
14168
  if (!this.validatePhone(phone)) {
13950
14169
  errorMessage.textContent = "Please enter a valid phone number (e.g., +1234567890)";
13951
14170
  errorMessage.style.display = "block";
@@ -13966,6 +14185,7 @@ class AegisWidgetManager {
13966
14185
  phone,
13967
14186
  email,
13968
14187
  name,
14188
+ dateOfBirth,
13969
14189
  cart,
13970
14190
  widgetId
13971
14191
  });
@@ -14016,12 +14236,17 @@ class AegisWidgetManager {
14016
14236
  phone: data.phone.replace(/[\s()-]/g, ""),
14017
14237
  email: data.email || void 0,
14018
14238
  name: data.name || void 0,
14239
+ // Omitted when blank — the server distinguishes "not given" from a value
14240
+ // it could not parse, and neither is an empty string.
14241
+ date_of_birth: data.dateOfBirth || void 0,
14019
14242
  cart_id: ((_a = data.cart) == null ? void 0 : _a.cart_id) || `web_${Date.now()}`,
14020
14243
  cart_token: (_b = data.cart) == null ? void 0 : _b.cart_id,
14021
14244
  cart_total: ((_c = data.cart) == null ? void 0 : _c.cart_total) || 0,
14022
14245
  cart_currency: ((_d = data.cart) == null ? void 0 : _d.cart_currency) || "USD",
14023
14246
  cart_items: ((_e = data.cart) == null ? void 0 : _e.cart_items) || [],
14024
- cart_url: window.location.href,
14247
+ // Redacted: a stored cart URL is re-sent later as a recovery link, and a
14248
+ // credential in it (`?t=` / `?k=`) must not outlive this page view.
14249
+ cart_url: redactUrl(window.location.href),
14025
14250
  platform: "web",
14026
14251
  geo_region: geoRegion,
14027
14252
  device_type: deviceType,
@@ -15137,7 +15362,7 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15137
15362
  var _a;
15138
15363
  this.initialized = false;
15139
15364
  {
15140
- const v = "1.37.0";
15365
+ const v = "1.39.0";
15141
15366
  try {
15142
15367
  globalThis.__AEGIS_SDK_VERSION__ = v;
15143
15368
  } catch {
@@ -15185,6 +15410,7 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15185
15410
  apiHost: config.apiHost,
15186
15411
  userId: config.userId,
15187
15412
  contactId: config.contactId,
15413
+ contactToken: config.contactToken,
15188
15414
  organizationId: config.organizationId,
15189
15415
  propertyId: config.propertyId,
15190
15416
  debugMode: config.debugMode,
@@ -15268,10 +15494,10 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15268
15494
  * wait — callers that care about the fetched state await on the
15269
15495
  * returned promise.
15270
15496
  */
15271
- async updateContactId(contactId) {
15497
+ async updateContactId(contactId, contactToken) {
15272
15498
  var _a, _b, _c;
15273
15499
  this.contactId = contactId;
15274
- (_b = (_a = this.inApp).updateContactId) == null ? void 0 : _b.call(_a, contactId);
15500
+ (_b = (_a = this.inApp).updateContactId) == null ? void 0 : _b.call(_a, contactId, contactToken);
15275
15501
  (_c = this.chat) == null ? void 0 : _c.updateContactId(contactId);
15276
15502
  await this.widgets.updateContactId(contactId);
15277
15503
  void this.contactScores.fetchForContact(contactId);