@active-reach/web-sdk 1.39.0 → 1.41.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
@@ -1,5 +1,5 @@
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";
1
+ import { S as Storage, r as redactUrl, l as logger, A as Aegis } from "./analytics-C-Go3Pm5.mjs";
2
+ import { B, E, N, R, U, m } from "./analytics-C-Go3Pm5.mjs";
3
3
  import { AegisWebPush } from "./push/AegisWebPush.js";
4
4
  function debounce(func, wait) {
5
5
  let timeoutId = null;
@@ -5037,22 +5037,35 @@ class TriggerEngine {
5037
5037
  callbacks.delete(callback);
5038
5038
  }
5039
5039
  }
5040
+ // A register* call after start() takes effect at once: in-app campaigns
5041
+ // (and so their triggers) arrive after the engine is already running.
5040
5042
  registerScrollDepth(depthPercent) {
5043
+ const firstTarget = this.scrollDepthTargets.size === 0;
5041
5044
  this.scrollDepthTargets.add(depthPercent);
5045
+ if (!this.isStarted) return;
5046
+ if (firstTarget) {
5047
+ this.attachScrollListener();
5048
+ } else {
5049
+ this.handleScroll();
5050
+ }
5042
5051
  }
5043
5052
  registerTimeOnPage(seconds) {
5044
5053
  if (!this.timeOnPageTargets.has(seconds)) {
5054
+ const elapsedMs = this.isStarted && this.pageLoadTime ? Date.now() - this.pageLoadTime : 0;
5045
5055
  const timerId = window.setTimeout(() => {
5046
5056
  this.emit(`time_on_page_${seconds}`, {
5047
5057
  seconds,
5048
5058
  page_url: redactUrl(window.location.href)
5049
5059
  });
5050
5060
  this.timeOnPageTargets.delete(seconds);
5051
- }, seconds * 1e3);
5061
+ }, Math.max(0, seconds * 1e3 - elapsedMs));
5052
5062
  this.timeOnPageTargets.set(seconds, timerId);
5053
5063
  }
5054
5064
  }
5055
5065
  registerExitIntent() {
5066
+ if (this.isStarted && !this.exitIntentEnabled) {
5067
+ this.attachExitIntentListener();
5068
+ }
5056
5069
  this.exitIntentEnabled = true;
5057
5070
  }
5058
5071
  registerInactivity(idleSeconds) {
@@ -5064,13 +5077,16 @@ class TriggerEngine {
5064
5077
  idle_seconds: idleSeconds,
5065
5078
  actual_idle_time: idleTime
5066
5079
  });
5080
+ this.inactivityTargets.delete(idleSeconds);
5067
5081
  }
5068
- this.inactivityTargets.delete(idleSeconds);
5069
5082
  }, idleSeconds * 1e3);
5070
5083
  this.inactivityTargets.set(idleSeconds, timerId);
5071
5084
  }
5072
5085
  }
5073
5086
  registerScrollVelocity(config) {
5087
+ if (this.isStarted && !this.scrollVelocityEnabled) {
5088
+ this.attachScrollVelocityListener();
5089
+ }
5074
5090
  this.scrollVelocityEnabled = true;
5075
5091
  if (config) {
5076
5092
  this.scrollVelocityConfig = {
@@ -6013,6 +6029,70 @@ function markReturningVisitor() {
6013
6029
  } catch {
6014
6030
  }
6015
6031
  }
6032
+ const DEFAULT_SCROLL_DEPTH_PERCENT = 50;
6033
+ const DEFAULT_TIME_ON_PAGE_SECONDS = 30;
6034
+ const DEFAULT_IDLE_SECONDS = 30;
6035
+ function positiveNumber(raw, fallback) {
6036
+ const n = Number(raw);
6037
+ return Number.isFinite(n) && n > 0 ? n : fallback;
6038
+ }
6039
+ function behaviouralTriggerEvent(trigger) {
6040
+ if (!trigger) return null;
6041
+ const cfg = trigger.config || {};
6042
+ switch (trigger.type) {
6043
+ case "exit_intent":
6044
+ return "exit_intent";
6045
+ case "scroll_depth":
6046
+ return `scroll_depth_${positiveNumber(cfg.depth_percent, DEFAULT_SCROLL_DEPTH_PERCENT)}`;
6047
+ case "time_on_page":
6048
+ return `time_on_page_${positiveNumber(cfg.seconds, DEFAULT_TIME_ON_PAGE_SECONDS)}`;
6049
+ case "inactivity":
6050
+ return `inactivity_${positiveNumber(cfg.idle_seconds, DEFAULT_IDLE_SECONDS)}`;
6051
+ default:
6052
+ return null;
6053
+ }
6054
+ }
6055
+ function triggerFromEngineEvent(eventName) {
6056
+ if (eventName === "exit_intent") return { type: "exit_intent" };
6057
+ const match = /^(inactivity|time_on_page|scroll_depth)_(\d+)$/.exec(eventName);
6058
+ if (!match) return null;
6059
+ const n = Number(match[2]);
6060
+ if (match[1] === "inactivity") return { type: "inactivity", config: { idle_seconds: n } };
6061
+ if (match[1] === "time_on_page") return { type: "time_on_page", config: { seconds: n } };
6062
+ return { type: "scroll_depth", config: { depth_percent: n } };
6063
+ }
6064
+ function armBehaviouralTriggers(engine, triggers, armed, fire, isMobile) {
6065
+ const newlyArmed = [];
6066
+ for (const trigger of triggers) {
6067
+ const eventName = behaviouralTriggerEvent(trigger);
6068
+ if (!eventName || armed.has(eventName)) continue;
6069
+ armed.add(eventName);
6070
+ newlyArmed.push(eventName);
6071
+ const cfg = trigger.config || {};
6072
+ switch (trigger.type) {
6073
+ case "exit_intent":
6074
+ if (isMobile) {
6075
+ engine.registerScrollVelocity();
6076
+ engine.on("mobile_exit_intent", () => fire("exit_intent"));
6077
+ } else {
6078
+ engine.registerExitIntent();
6079
+ engine.on("exit_intent", () => fire("exit_intent"));
6080
+ }
6081
+ continue;
6082
+ case "scroll_depth":
6083
+ engine.registerScrollDepth(positiveNumber(cfg.depth_percent, DEFAULT_SCROLL_DEPTH_PERCENT));
6084
+ break;
6085
+ case "time_on_page":
6086
+ engine.registerTimeOnPage(positiveNumber(cfg.seconds, DEFAULT_TIME_ON_PAGE_SECONDS));
6087
+ break;
6088
+ case "inactivity":
6089
+ engine.registerInactivity(positiveNumber(cfg.idle_seconds, DEFAULT_IDLE_SECONDS));
6090
+ break;
6091
+ }
6092
+ engine.on(eventName, () => fire(eventName));
6093
+ }
6094
+ return newlyArmed;
6095
+ }
6016
6096
  function readAnonIdFromStorage$1() {
6017
6097
  if (typeof document === "undefined") return void 0;
6018
6098
  try {
@@ -6108,7 +6188,6 @@ const _AegisInAppManager = class _AegisInAppManager {
6108
6188
  this.propertyId = config.propertyId;
6109
6189
  this.debugMode = config.debugMode || false;
6110
6190
  this.enableSSE = config.enableSSE === true;
6111
- this.onInteractiveCampaign = config.onInteractiveCampaign;
6112
6191
  this.onActionHandler = config.onAction;
6113
6192
  if (config.productUrl) this.productUrl = config.productUrl;
6114
6193
  this.getWorkspaceId = config.getWorkspaceId;
@@ -6536,6 +6615,9 @@ const _AegisInAppManager = class _AegisInAppManager {
6536
6615
  if (this.contactId) {
6537
6616
  headers["X-Contact-ID"] = this.contactId;
6538
6617
  }
6618
+ if (this.contactId && this.contactToken) {
6619
+ headers["X-Contact-Token"] = this.contactToken;
6620
+ }
6539
6621
  Object.entries(headers).forEach(([key, value]) => {
6540
6622
  url.searchParams.append(key, value);
6541
6623
  });
@@ -6614,6 +6696,9 @@ const _AegisInAppManager = class _AegisInAppManager {
6614
6696
  if (this.contactId) {
6615
6697
  headers["X-Contact-ID"] = this.contactId;
6616
6698
  }
6699
+ if (this.contactId && this.contactToken) {
6700
+ headers["X-Contact-Token"] = this.contactToken;
6701
+ }
6617
6702
  if (this.organizationId) {
6618
6703
  headers["X-Organization-ID"] = this.organizationId;
6619
6704
  }
@@ -9030,6 +9115,9 @@ const _AegisInAppManager = class _AegisInAppManager {
9030
9115
  * matches config.product_id (string or string[])
9031
9116
  * - `delay` : client-side setTimeout — evaluated at armeng
9032
9117
  * time (kicked off from `displayCampaign`), not here.
9118
+ * - `exit_intent` / `scroll_depth` / `time_on_page` / `inactivity` : the
9119
+ * TriggerEngine event named by `behaviouralTriggerEvent`;
9120
+ * AegisMessageRuntime arms the engine and forwards it.
9033
9121
  */
9034
9122
  onClientEvent(eventName, eventData = {}) {
9035
9123
  for (const c of this.campaigns) {
@@ -9059,8 +9147,10 @@ const _AegisInAppManager = class _AegisInAppManager {
9059
9147
  );
9060
9148
  return wanted.includes(actual);
9061
9149
  }
9062
- default:
9063
- return false;
9150
+ default: {
9151
+ const behavioural = behaviouralTriggerEvent(trigger);
9152
+ return behavioural !== null && behavioural === eventName;
9153
+ }
9064
9154
  }
9065
9155
  }
9066
9156
  displayCampaign(campaign) {
@@ -9284,11 +9374,7 @@ const _AegisInAppManager = class _AegisInAppManager {
9284
9374
  break;
9285
9375
  case "spin_wheel":
9286
9376
  case "scratch_card":
9287
- if (this.onInteractiveCampaign) {
9288
- this.onInteractiveCampaign(campaign);
9289
- } else {
9290
- this.renderGamificationOverlay(campaign, ic, bg, text);
9291
- }
9377
+ this.renderGamificationOverlay(campaign, ic, bg, text);
9292
9378
  break;
9293
9379
  default:
9294
9380
  this.log(`Unknown interactive sub_type: ${campaign.sub_type}`, "warn");
@@ -11918,6 +12004,7 @@ const _AegisInAppManager = class _AegisInAppManager {
11918
12004
  if (this.organizationId) headers["X-Organization-ID"] = this.organizationId;
11919
12005
  if (this.userId) headers["X-User-ID"] = this.userId;
11920
12006
  if (this.contactId) headers["X-Contact-ID"] = this.contactId;
12007
+ if (this.contactId && this.contactToken) headers["X-Contact-Token"] = this.contactToken;
11921
12008
  const workspaceId = (_a = this.getWorkspaceId) == null ? void 0 : _a.call(this);
11922
12009
  const storedAnonId = readAnonIdFromStorage$1();
11923
12010
  const identifiedUserId = this.userId && this.userId !== storedAnonId ? this.userId : void 0;
@@ -12018,6 +12105,7 @@ const _AegisInAppManager = class _AegisInAppManager {
12018
12105
  if (this.organizationId) headers["X-Organization-ID"] = this.organizationId;
12019
12106
  if (this.userId) headers["X-User-ID"] = this.userId;
12020
12107
  if (this.contactId) headers["X-Contact-ID"] = this.contactId;
12108
+ if (this.contactId && this.contactToken) headers["X-Contact-Token"] = this.contactToken;
12021
12109
  let retryable = true;
12022
12110
  try {
12023
12111
  const response = await fetch(
@@ -12530,6 +12618,9 @@ class ContactScoresFetcher {
12530
12618
  "X-Contact-ID": contactId,
12531
12619
  "X-User-ID": contactId
12532
12620
  };
12621
+ if (opts.contactToken) {
12622
+ headers["X-Contact-Token"] = opts.contactToken;
12623
+ }
12533
12624
  if (this.organizationId) {
12534
12625
  headers["X-Organization-ID"] = this.organizationId;
12535
12626
  }
@@ -12692,6 +12783,7 @@ class PrefetchBundleClient {
12692
12783
  this.writeKey = options.writeKey;
12693
12784
  this.organizationId = options.organizationId;
12694
12785
  this.contactId = options.contactId;
12786
+ this.contactToken = options.contactToken;
12695
12787
  this.userId = options.userId;
12696
12788
  this.propertyId = options.propertyId;
12697
12789
  this.abAssignments = options.abAssignments ?? {};
@@ -12756,7 +12848,10 @@ class PrefetchBundleClient {
12756
12848
  let changed = false;
12757
12849
  if (partial.contactId !== void 0 && partial.contactId !== this.contactId) {
12758
12850
  this.contactId = partial.contactId;
12851
+ this.contactToken = partial.contactToken;
12759
12852
  changed = true;
12853
+ } else if (partial.contactToken !== void 0 && partial.contactToken !== this.contactToken) {
12854
+ this.contactToken = partial.contactToken;
12760
12855
  }
12761
12856
  if (partial.userId !== void 0 && partial.userId !== this.userId) {
12762
12857
  this.userId = partial.userId;
@@ -12829,6 +12924,7 @@ class PrefetchBundleClient {
12829
12924
  };
12830
12925
  if (this.organizationId) headers["X-Organization-ID"] = this.organizationId;
12831
12926
  if (this.contactId) headers["X-Contact-ID"] = this.contactId;
12927
+ if (this.contactId && this.contactToken) headers["X-Contact-Token"] = this.contactToken;
12832
12928
  if (this.userId) headers["X-User-ID"] = this.userId;
12833
12929
  if (this.propertyId) headers["X-Property-Id"] = this.propertyId;
12834
12930
  if (this.currentETag) headers["If-None-Match"] = this.currentETag;
@@ -12888,6 +12984,7 @@ class AegisInbox {
12888
12984
  this.writeKey = config.writeKey;
12889
12985
  this.organizationId = config.organizationId;
12890
12986
  this.contactId = config.contactId;
12987
+ this.contactToken = config.contactToken;
12891
12988
  this.propertyId = config.propertyId;
12892
12989
  this.bundleClient = config.bundleClient;
12893
12990
  this.unreadPollIntervalMs = config.unreadPollIntervalMs ?? DEFAULT_UNREAD_POLL;
@@ -12936,7 +13033,10 @@ class AegisInbox {
12936
13033
  let changed = false;
12937
13034
  if (partial.contactId !== void 0 && partial.contactId !== this.contactId) {
12938
13035
  this.contactId = partial.contactId;
13036
+ this.contactToken = partial.contactToken;
12939
13037
  changed = true;
13038
+ } else if (partial.contactToken !== void 0 && partial.contactToken !== this.contactToken) {
13039
+ this.contactToken = partial.contactToken;
12940
13040
  }
12941
13041
  if (partial.propertyId !== void 0 && partial.propertyId !== this.propertyId) {
12942
13042
  this.propertyId = partial.propertyId;
@@ -13107,6 +13207,7 @@ class AegisInbox {
13107
13207
  };
13108
13208
  if (this.organizationId) h["X-Organization-ID"] = this.organizationId;
13109
13209
  if (this.contactId) h["X-Contact-ID"] = this.contactId;
13210
+ if (this.contactId && this.contactToken) h["X-Contact-Token"] = this.contactToken;
13110
13211
  if (this.propertyId) h["X-Property-Id"] = this.propertyId;
13111
13212
  return h;
13112
13213
  }
@@ -13221,6 +13322,7 @@ class AegisWidgetManager {
13221
13322
  this.apiHost = config.apiHost || "https://api.aegis.ai";
13222
13323
  this.userId = config.userId ?? readAnonIdFromStorage();
13223
13324
  this.contactId = config.contactId;
13325
+ this.contactToken = config.contactToken;
13224
13326
  this.organizationId = config.organizationId;
13225
13327
  this.debugMode = config.debugMode || false;
13226
13328
  this.triggerEngine = config.triggerEngine;
@@ -13243,48 +13345,16 @@ class AegisWidgetManager {
13243
13345
  * configs so subsequent renders target the new identity. Does NOT
13244
13346
  * re-render already-visible widgets — a full teardown requires destroy().
13245
13347
  */
13246
- async updateContactId(contactId) {
13247
- if (this.contactId === contactId) return;
13348
+ async updateContactId(contactId, contactToken) {
13349
+ if (this.contactId === contactId && this.contactToken === contactToken) return;
13248
13350
  this.contactId = contactId;
13351
+ this.contactToken = contactToken;
13249
13352
  this.log(`Contact ID updated → ${contactId ?? "(cleared)"}`);
13250
13353
  if (this.isInitialized && !this.isDestroyed && this.enablePrefetch && contactId) {
13251
13354
  await this.fetchPrefetchConfigs();
13252
13355
  this.preloadWidgetAssets();
13253
13356
  }
13254
13357
  }
13255
- /**
13256
- * Render an in-app campaign whose sub_type routes it to the
13257
- * gamification-widget code path (spin_wheel / scratch_card).
13258
- * InAppCampaign is adapted to the minimal WidgetConfig shape that
13259
- * the internal renderers already understand — no rewrite needed.
13260
- *
13261
- * AegisMessageRuntime wires this method as AegisInAppManager's
13262
- * `onInteractiveCampaign` callback so impressions from the inbox
13263
- * prefetch bundle actually land on screen.
13264
- */
13265
- renderInteractiveCampaign(campaign) {
13266
- if (this.isDestroyed) return;
13267
- if (typeof document === "undefined") return;
13268
- const subType = campaign.sub_type;
13269
- if (subType !== "spin_wheel" && subType !== "scratch_card") {
13270
- this.log(`renderInteractiveCampaign: unsupported sub_type "${subType}"`, true);
13271
- return;
13272
- }
13273
- const widget = {
13274
- widget_id: campaign.id,
13275
- widget_type: subType,
13276
- name: campaign.title,
13277
- config: campaign.interactive_config ?? {},
13278
- priority: campaign.priority ?? 0
13279
- };
13280
- if (this.renderedWidgets.has(widget.widget_id)) return;
13281
- this.renderedWidgets.add(widget.widget_id);
13282
- if (subType === "spin_wheel") {
13283
- this.renderSpinWheel(widget);
13284
- } else {
13285
- this.renderScratchCard(widget);
13286
- }
13287
- }
13288
13358
  /**
13289
13359
  * Tear down everything the manager installed: trigger listeners (if
13290
13360
  * we own the TriggerEngine), injected style sheets, and any rendered
@@ -13460,15 +13530,6 @@ class AegisWidgetManager {
13460
13530
  this.log(`Preloading exit intent image: ${imageUrl}`);
13461
13531
  }
13462
13532
  }
13463
- const spinWheelConfig = this.prefetchWidgetConfigs.spin_wheel;
13464
- if (spinWheelConfig == null ? void 0 : spinWheelConfig.enabled) {
13465
- const imageUrl = spinWheelConfig.image_url;
13466
- if (imageUrl) {
13467
- const img = new Image();
13468
- img.src = imageUrl;
13469
- this.log(`Preloading spin wheel image: ${imageUrl}`);
13470
- }
13471
- }
13472
13533
  } catch (error) {
13473
13534
  this.log(`Error preloading widget assets: ${error}`, true);
13474
13535
  }
@@ -13481,6 +13542,7 @@ class AegisWidgetManager {
13481
13542
  "X-Aegis-Write-Key": this.writeKey
13482
13543
  };
13483
13544
  if (this.contactId) headers["X-Contact-ID"] = this.contactId;
13545
+ if (this.contactId && this.contactToken) headers["X-Contact-Token"] = this.contactToken;
13484
13546
  if (this.organizationId) headers["X-Organization-ID"] = this.organizationId;
13485
13547
  const response = await fetch(url, { headers, credentials: "include" });
13486
13548
  if (!response.ok) {
@@ -13503,6 +13565,7 @@ class AegisWidgetManager {
13503
13565
  };
13504
13566
  if (this.userId) headers["X-User-ID"] = this.userId;
13505
13567
  if (this.contactId) headers["X-Contact-ID"] = this.contactId;
13568
+ if (this.contactId && this.contactToken) headers["X-Contact-Token"] = this.contactToken;
13506
13569
  if (this.organizationId) headers["X-Organization-ID"] = this.organizationId;
13507
13570
  if (this.sourcePlatform) headers["X-Source-Platform"] = this.sourcePlatform;
13508
13571
  const response = await fetch(url, { headers, credentials: "include" });
@@ -13563,67 +13626,7 @@ class AegisWidgetManager {
13563
13626
  if (!this.enablePrefetch || !this.triggerEngine) {
13564
13627
  return;
13565
13628
  }
13566
- const spinWheelConfig = this.prefetchWidgetConfigs.spin_wheel;
13567
13629
  const exitIntentConfig = this.prefetchWidgetConfigs.exit_intent;
13568
- if (spinWheelConfig && spinWheelConfig.enabled) {
13569
- const isMobile2 = this.isMobileDevice();
13570
- const mobileConfig2 = (exitIntentConfig == null ? void 0 : exitIntentConfig.mobile_triggers) || {};
13571
- const handleSpinWheelIntent = () => {
13572
- const widgetId = `prefetch_spin_wheel_${Date.now()}`;
13573
- if (this.renderedWidgets.has(widgetId)) {
13574
- return;
13575
- }
13576
- this.renderedWidgets.add(widgetId);
13577
- const detectedCart = this.detectPlatformCart();
13578
- if (spinWheelConfig.type === "cart_recovery" && detectedCart) {
13579
- this.cartData = detectedCart;
13580
- this.renderSpinWheelWidget(widgetId, spinWheelConfig);
13581
- this.sendCartAbandonmentBeacon();
13582
- } else if (spinWheelConfig.type === "lead_gen") {
13583
- this.renderSpinWheelWidget(widgetId, spinWheelConfig);
13584
- }
13585
- };
13586
- if (isMobile2) {
13587
- const scrollVelocityEnabled = mobileConfig2.scroll_velocity !== false;
13588
- if (scrollVelocityEnabled) {
13589
- this.triggerEngine.registerScrollVelocity({
13590
- threshold: mobileConfig2.scroll_threshold || 0.5,
13591
- minScrollPosition: mobileConfig2.scroll_min_position || 100,
13592
- cooldown: mobileConfig2.scroll_cooldown || 5e3
13593
- });
13594
- this.triggerEngine.on("mobile_exit_intent", handleSpinWheelIntent);
13595
- this.log("Registered mobile scroll velocity trigger for spin wheel");
13596
- }
13597
- const idleTimerEnabled = mobileConfig2.idle_timer !== false;
13598
- if (idleTimerEnabled) {
13599
- const idleSeconds = mobileConfig2.idle_seconds || 15;
13600
- this.triggerEngine.registerInactivity(idleSeconds);
13601
- this.triggerEngine.on(`inactivity_${idleSeconds}`, handleSpinWheelIntent);
13602
- this.log(`Registered mobile idle timer trigger for spin wheel: ${idleSeconds}s`);
13603
- }
13604
- const visibilityChangeEnabled = mobileConfig2.visibility_change !== false;
13605
- if (visibilityChangeEnabled) {
13606
- const detectedCart = this.detectPlatformCart();
13607
- if (detectedCart && detectedCart.cart_items && detectedCart.cart_items.length > 0) {
13608
- this.triggerEngine.registerVisibilityChange();
13609
- this.triggerEngine.on("visibility_hidden", handleSpinWheelIntent);
13610
- this.log("Registered mobile visibility change trigger for spin wheel (cart detected)");
13611
- }
13612
- }
13613
- const backButtonEnabled = mobileConfig2.back_button === true;
13614
- if (backButtonEnabled) {
13615
- this.triggerEngine.registerBackButton();
13616
- this.triggerEngine.on("back_button", handleSpinWheelIntent);
13617
- this.log("Registered mobile back button trigger for spin wheel");
13618
- }
13619
- this.log(`Setup mobile spin wheel: type=${spinWheelConfig.type}, priority=${spinWheelConfig.priority}`);
13620
- } else {
13621
- this.triggerEngine.registerExitIntent();
13622
- this.triggerEngine.on("exit_intent", handleSpinWheelIntent);
13623
- this.log(`Setup desktop spin wheel exit intent: type=${spinWheelConfig.type}, priority=${spinWheelConfig.priority}`);
13624
- }
13625
- return;
13626
- }
13627
13630
  if (!exitIntentConfig || !exitIntentConfig.enabled) {
13628
13631
  this.log("No exit intent config found in prefetch");
13629
13632
  return;
@@ -13740,12 +13743,6 @@ class AegisWidgetManager {
13740
13743
  case "chat_bubble":
13741
13744
  this.renderChatBubble(widget);
13742
13745
  break;
13743
- case "spin_wheel":
13744
- this.renderSpinWheel(widget);
13745
- break;
13746
- case "scratch_card":
13747
- this.renderScratchCard(widget);
13748
- break;
13749
13746
  case "toast":
13750
13747
  this.renderToast(widget);
13751
13748
  break;
@@ -13814,531 +13811,6 @@ class AegisWidgetManager {
13814
13811
  bubble.setAttribute("data-aegis-widget-root", "");
13815
13812
  document.body.appendChild(bubble);
13816
13813
  }
13817
- renderSpinWheel(widget) {
13818
- const overlay = document.createElement("div");
13819
- overlay.className = "aegis-gamification-overlay";
13820
- overlay.setAttribute("data-widget-id", widget.widget_id);
13821
- overlay.style.cssText = `
13822
- position: fixed;
13823
- top: 0;
13824
- left: 0;
13825
- right: 0;
13826
- bottom: 0;
13827
- background: rgba(0, 0, 0, 0.7);
13828
- z-index: 1000000;
13829
- display: flex;
13830
- align-items: center;
13831
- justify-content: center;
13832
- animation: aegisFadeIn 0.3s ease-out;
13833
- `;
13834
- const modal = document.createElement("div");
13835
- modal.className = "aegis-spin-wheel-modal";
13836
- modal.style.cssText = `
13837
- background: white;
13838
- border-radius: 16px;
13839
- padding: 32px;
13840
- max-width: 500px;
13841
- width: 90%;
13842
- box-shadow: 0 10px 40px rgba(0,0,0,0.3);
13843
- text-align: center;
13844
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
13845
- animation: aegisScaleIn 0.3s ease-out;
13846
- `;
13847
- const title = document.createElement("h2");
13848
- title.textContent = widget.config.title || "Spin to Win!";
13849
- title.style.cssText = "margin: 0 0 16px 0; font-size: 24px; font-weight: 700; color: #1a1a1a;";
13850
- modal.appendChild(title);
13851
- const description = document.createElement("p");
13852
- description.textContent = widget.config.description || "Try your luck and win exclusive prizes!";
13853
- description.style.cssText = "margin: 0 0 24px 0; font-size: 14px; color: #666;";
13854
- modal.appendChild(description);
13855
- const segments = Array.isArray(
13856
- widget.config.segments
13857
- ) ? widget.config.segments : [
13858
- { label: "Prize 1", color: "#ff6b6b" },
13859
- { label: "Prize 2", color: "#feca57" },
13860
- { label: "Prize 3", color: "#48dbfb" },
13861
- { label: "Prize 4", color: "#ff6348" }
13862
- ];
13863
- const SVG_NS2 = "http://www.w3.org/2000/svg";
13864
- const wheel = document.createElementNS(SVG_NS2, "svg");
13865
- wheel.setAttribute("viewBox", "-160 -160 320 320");
13866
- wheel.setAttribute("width", "300");
13867
- wheel.setAttribute("height", "300");
13868
- wheel.style.cssText = "margin: 0 auto 24px; display: block;";
13869
- const n = segments.length;
13870
- const anglePer = 2 * Math.PI / n;
13871
- const radius = 140;
13872
- for (let i = 0; i < n; i++) {
13873
- const start = i * anglePer - Math.PI / 2;
13874
- const end = start + anglePer;
13875
- const x1 = Math.cos(start) * radius;
13876
- const y1 = Math.sin(start) * radius;
13877
- const x2 = Math.cos(end) * radius;
13878
- const y2 = Math.sin(end) * radius;
13879
- const largeArc = anglePer > Math.PI ? 1 : 0;
13880
- const path = document.createElementNS(SVG_NS2, "path");
13881
- path.setAttribute(
13882
- "d",
13883
- `M 0 0 L ${x1.toFixed(2)} ${y1.toFixed(2)} A ${radius} ${radius} 0 ${largeArc} 1 ${x2.toFixed(2)} ${y2.toFixed(2)} Z`
13884
- );
13885
- path.setAttribute("fill", this.sanitizeColor(segments[i].color || "#cccccc"));
13886
- path.setAttribute("stroke", "#ffffff");
13887
- path.setAttribute("stroke-width", "2");
13888
- wheel.appendChild(path);
13889
- const labelAngle = start + anglePer / 2;
13890
- const lx = Math.cos(labelAngle) * radius * 0.65;
13891
- const ly = Math.sin(labelAngle) * radius * 0.65;
13892
- const text = document.createElementNS(SVG_NS2, "text");
13893
- text.setAttribute("x", lx.toFixed(2));
13894
- text.setAttribute("y", ly.toFixed(2));
13895
- text.setAttribute("fill", "#ffffff");
13896
- text.setAttribute("font-size", "13");
13897
- text.setAttribute("font-weight", "600");
13898
- text.setAttribute("text-anchor", "middle");
13899
- text.setAttribute("dominant-baseline", "middle");
13900
- text.textContent = segments[i].label || `#${i + 1}`;
13901
- wheel.appendChild(text);
13902
- }
13903
- modal.appendChild(wheel);
13904
- const spinButton = document.createElement("button");
13905
- spinButton.textContent = "SPIN NOW!";
13906
- spinButton.style.cssText = `
13907
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
13908
- color: white;
13909
- border: none;
13910
- padding: 16px 48px;
13911
- border-radius: 8px;
13912
- font-weight: 700;
13913
- font-size: 16px;
13914
- cursor: pointer;
13915
- margin-bottom: 16px;
13916
- `;
13917
- spinButton.addEventListener("click", async () => {
13918
- spinButton.disabled = true;
13919
- spinButton.textContent = "SPINNING...";
13920
- try {
13921
- const prize = await this.generatePrize(widget.config.config_id);
13922
- wheel.style.animation = "aegisSpin 2s ease-out";
13923
- setTimeout(() => {
13924
- this.showPrizeResult(modal, prize);
13925
- }, 2e3);
13926
- } catch (error) {
13927
- this.log(`Error generating prize: ${error}`, true);
13928
- spinButton.disabled = false;
13929
- spinButton.textContent = "TRY AGAIN";
13930
- }
13931
- });
13932
- modal.appendChild(spinButton);
13933
- const closeButton = document.createElement("button");
13934
- closeButton.textContent = "×";
13935
- closeButton.setAttribute("aria-label", "Close");
13936
- closeButton.style.cssText = `
13937
- position: absolute;
13938
- top: 16px;
13939
- right: 16px;
13940
- background: transparent;
13941
- border: none;
13942
- font-size: 28px;
13943
- cursor: pointer;
13944
- color: #999;
13945
- `;
13946
- closeButton.addEventListener("click", () => {
13947
- this.trackEvent(widget.widget_id, "dismiss");
13948
- document.body.removeChild(overlay);
13949
- this.renderedWidgets.delete(widget.widget_id);
13950
- });
13951
- modal.style.position = "relative";
13952
- modal.appendChild(closeButton);
13953
- overlay.appendChild(modal);
13954
- this.addAnimationStyles();
13955
- overlay.setAttribute("data-aegis-widget-root", "");
13956
- document.body.appendChild(overlay);
13957
- }
13958
- renderSpinWheelWidget(widgetId, config) {
13959
- if (this.isDestroyed) return;
13960
- const cart = this.detectPlatformCart();
13961
- if (config.type === "cart_recovery" && !cart) {
13962
- this.log("Spin wheel requires cart, but none detected");
13963
- return;
13964
- }
13965
- if (cart) {
13966
- this.cartData = cart;
13967
- }
13968
- const customization = this.cssCustomization.spinWheel || {};
13969
- const accentColor = customization.accentColor || config.accent_color || "#FF6B6B";
13970
- const backgroundColor = customization.backgroundColor || config.background_color || "#FFFFFF";
13971
- const textColor = customization.textColor || config.text_color || "#333333";
13972
- const buttonColor = customization.buttonColor || config.button_color || accentColor;
13973
- const wheelColors = customization.wheelColors || ["#FF6B6B", "#4ECDC4", "#FFE66D", "#95E1D3"];
13974
- const overlay = document.createElement("div");
13975
- overlay.className = "aegis-spin-wheel-overlay";
13976
- overlay.setAttribute("data-widget-id", widgetId);
13977
- overlay.style.cssText = `
13978
- position: fixed;
13979
- top: 0;
13980
- left: 0;
13981
- width: 100%;
13982
- height: 100%;
13983
- background: rgba(0, 0, 0, 0.7);
13984
- display: flex;
13985
- align-items: center;
13986
- justify-content: center;
13987
- z-index: 999999;
13988
- animation: aegisFadeIn 0.3s ease;
13989
- `;
13990
- const modal = document.createElement("div");
13991
- modal.className = "aegis-spin-wheel-modal";
13992
- modal.style.cssText = `
13993
- background: ${this.sanitizeColor(backgroundColor)};
13994
- border-radius: 16px;
13995
- padding: 32px;
13996
- max-width: 500px;
13997
- width: 90%;
13998
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
13999
- position: relative;
14000
- animation: aegisScaleIn 0.4s ease;
14001
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
14002
- `;
14003
- const closeBtn = document.createElement("button");
14004
- closeBtn.innerHTML = lucideSvg("x", { size: 16 });
14005
- closeBtn.setAttribute("aria-label", "Close");
14006
- closeBtn.className = "aegis-spin-wheel-close";
14007
- closeBtn.style.cssText = `
14008
- position: absolute;
14009
- top: 16px;
14010
- right: 16px;
14011
- background: none;
14012
- border: none;
14013
- font-size: 24px;
14014
- cursor: pointer;
14015
- color: #999;
14016
- `;
14017
- closeBtn.onclick = () => {
14018
- this.trackEvent(widgetId, "dismiss", { type: "spin_wheel" });
14019
- document.body.removeChild(overlay);
14020
- this.renderedWidgets.delete(widgetId);
14021
- };
14022
- modal.appendChild(closeBtn);
14023
- const title = document.createElement("h2");
14024
- title.textContent = this.interpolateCartVariables(config.title || "Spin to Win!");
14025
- title.style.cssText = `
14026
- margin: 0 0 16px 0;
14027
- font-size: 28px;
14028
- font-weight: bold;
14029
- text-align: center;
14030
- color: ${this.sanitizeColor(textColor)};
14031
- `;
14032
- modal.appendChild(title);
14033
- if (cart) {
14034
- const subtitle = document.createElement("p");
14035
- subtitle.textContent = `Complete your ${cart.cart_currency} ${cart.cart_total.toFixed(2)} order and save!`;
14036
- subtitle.style.cssText = `
14037
- margin: 0 0 24px 0;
14038
- font-size: 16px;
14039
- text-align: center;
14040
- color: #666;
14041
- `;
14042
- modal.appendChild(subtitle);
14043
- }
14044
- const wheelContainer = document.createElement("div");
14045
- wheelContainer.className = "aegis-wheel-container";
14046
- wheelContainer.style.cssText = `
14047
- width: 300px;
14048
- height: 300px;
14049
- margin: 0 auto 24px auto;
14050
- background: conic-gradient(
14051
- from 0deg,
14052
- ${wheelColors[0]} 0deg 90deg,
14053
- ${wheelColors[1]} 90deg 180deg,
14054
- ${wheelColors[2]} 180deg 270deg,
14055
- ${wheelColors[3]} 270deg 360deg
14056
- );
14057
- border-radius: 50%;
14058
- position: relative;
14059
- display: flex;
14060
- align-items: center;
14061
- justify-content: center;
14062
- `;
14063
- const form = document.createElement("form");
14064
- form.className = "aegis-spin-form";
14065
- form.style.cssText = "display: block;";
14066
- const phoneInput = document.createElement("input");
14067
- phoneInput.type = "tel";
14068
- phoneInput.placeholder = "Enter your phone number";
14069
- phoneInput.required = true;
14070
- phoneInput.style.cssText = `
14071
- width: 100%;
14072
- padding: 14px;
14073
- border: 2px solid #ddd;
14074
- border-radius: 8px;
14075
- font-size: 16px;
14076
- margin-bottom: 12px;
14077
- box-sizing: border-box;
14078
- `;
14079
- form.appendChild(phoneInput);
14080
- const emailInput = document.createElement("input");
14081
- emailInput.type = "email";
14082
- emailInput.placeholder = "Enter your email (optional)";
14083
- emailInput.style.cssText = `
14084
- width: 100%;
14085
- padding: 14px;
14086
- border: 2px solid #ddd;
14087
- border-radius: 8px;
14088
- font-size: 16px;
14089
- margin-bottom: 12px;
14090
- box-sizing: border-box;
14091
- `;
14092
- form.appendChild(emailInput);
14093
- const nameInput = document.createElement("input");
14094
- nameInput.type = "text";
14095
- nameInput.placeholder = "Enter your name (optional)";
14096
- nameInput.style.cssText = `
14097
- width: 100%;
14098
- padding: 14px;
14099
- border: 2px solid #ddd;
14100
- border-radius: 8px;
14101
- font-size: 16px;
14102
- margin-bottom: 12px;
14103
- box-sizing: border-box;
14104
- `;
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
- }
14137
- const submitButton = document.createElement("button");
14138
- submitButton.type = "submit";
14139
- submitButton.textContent = "Spin the Wheel!";
14140
- submitButton.style.cssText = `
14141
- width: 100%;
14142
- padding: 14px;
14143
- background: ${this.sanitizeColor(buttonColor)};
14144
- color: white;
14145
- border: none;
14146
- border-radius: 8px;
14147
- font-size: 16px;
14148
- font-weight: 700;
14149
- cursor: pointer;
14150
- `;
14151
- form.appendChild(submitButton);
14152
- const errorMessage = document.createElement("div");
14153
- errorMessage.className = "aegis-spin-error";
14154
- errorMessage.style.cssText = `
14155
- color: #d32f2f;
14156
- font-size: 14px;
14157
- margin-top: 12px;
14158
- text-align: center;
14159
- display: none;
14160
- `;
14161
- form.appendChild(errorMessage);
14162
- form.addEventListener("submit", async (e) => {
14163
- e.preventDefault();
14164
- const phone = phoneInput.value.trim();
14165
- const email = emailInput.value.trim();
14166
- const name = nameInput.value.trim();
14167
- const dateOfBirth = birthdayInput ? birthdayInput.value.trim() : "";
14168
- if (!this.validatePhone(phone)) {
14169
- errorMessage.textContent = "Please enter a valid phone number (e.g., +1234567890)";
14170
- errorMessage.style.display = "block";
14171
- return;
14172
- }
14173
- if (email && !this.validateEmail(email)) {
14174
- errorMessage.textContent = "Please enter a valid email address";
14175
- errorMessage.style.display = "block";
14176
- return;
14177
- }
14178
- errorMessage.style.display = "none";
14179
- submitButton.disabled = true;
14180
- submitButton.textContent = "Spinning...";
14181
- wheelContainer.style.animation = "aegisSpin 2s ease-out";
14182
- setTimeout(async () => {
14183
- try {
14184
- const prize = await this.submitSpinWheel({
14185
- phone,
14186
- email,
14187
- name,
14188
- dateOfBirth,
14189
- cart,
14190
- widgetId
14191
- });
14192
- this.showSpinWheelPrize(modal, prize);
14193
- this.trackEvent(widgetId, "submit", {
14194
- type: "spin_wheel",
14195
- prize_label: prize.prize_label,
14196
- has_email: !!email
14197
- });
14198
- } catch (error) {
14199
- this.log(`Error submitting spin wheel: ${error}`, true);
14200
- errorMessage.textContent = "Failed to submit. Please try again.";
14201
- errorMessage.style.display = "block";
14202
- submitButton.disabled = false;
14203
- submitButton.textContent = "Spin the Wheel!";
14204
- wheelContainer.style.animation = "";
14205
- }
14206
- }, 2e3);
14207
- });
14208
- modal.appendChild(wheelContainer);
14209
- modal.appendChild(form);
14210
- overlay.appendChild(modal);
14211
- this.addAnimationStyles();
14212
- overlay.setAttribute("data-aegis-widget-root", "");
14213
- document.body.appendChild(overlay);
14214
- this.trackEvent(widgetId, "show", { type: "spin_wheel" });
14215
- }
14216
- validatePhone(phone) {
14217
- const e164Regex = /^\+?[1-9]\d{1,14}$/;
14218
- return e164Regex.test(phone.replace(/[\s()-]/g, ""));
14219
- }
14220
- validateEmail(email) {
14221
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
14222
- return emailRegex.test(email);
14223
- }
14224
- async submitSpinWheel(data) {
14225
- var _a, _b, _c, _d, _e;
14226
- const url = `${this.apiHost}/v1/widgets/spin-wheel/submit`;
14227
- const headers = {
14228
- "X-Aegis-Write-Key": this.writeKey,
14229
- "Content-Type": "application/json"
14230
- };
14231
- if (this.organizationId) headers["X-Organization-ID"] = this.organizationId;
14232
- const geoRegion = this.detectGeoRegion();
14233
- const deviceType = this.getDeviceType();
14234
- const utmParams = this.getUTMParams();
14235
- const body = {
14236
- phone: data.phone.replace(/[\s()-]/g, ""),
14237
- email: data.email || void 0,
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,
14242
- cart_id: ((_a = data.cart) == null ? void 0 : _a.cart_id) || `web_${Date.now()}`,
14243
- cart_token: (_b = data.cart) == null ? void 0 : _b.cart_id,
14244
- cart_total: ((_c = data.cart) == null ? void 0 : _c.cart_total) || 0,
14245
- cart_currency: ((_d = data.cart) == null ? void 0 : _d.cart_currency) || "USD",
14246
- cart_items: ((_e = data.cart) == null ? void 0 : _e.cart_items) || [],
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),
14250
- platform: "web",
14251
- geo_region: geoRegion,
14252
- device_type: deviceType,
14253
- session_id: this.getSessionId() || void 0,
14254
- anonymous_id: this.getAnonymousId() || void 0,
14255
- utm_source: utmParams.utm_source,
14256
- utm_medium: utmParams.utm_medium,
14257
- utm_campaign: utmParams.utm_campaign
14258
- };
14259
- const response = await fetch(url, {
14260
- method: "POST",
14261
- headers,
14262
- body: JSON.stringify(body),
14263
- credentials: "include"
14264
- });
14265
- if (!response.ok) {
14266
- const errorText = await response.text();
14267
- throw new Error(`Failed to submit spin wheel: ${response.status} - ${errorText}`);
14268
- }
14269
- return await response.json();
14270
- }
14271
- showSpinWheelPrize(modal, prize) {
14272
- modal.innerHTML = "";
14273
- const resultContainer = document.createElement("div");
14274
- resultContainer.style.cssText = "text-align: center; padding: 40px 20px;";
14275
- const emoji = document.createElement("div");
14276
- emoji.textContent = "🎉";
14277
- emoji.style.cssText = "font-size: 60px; margin-bottom: 20px;";
14278
- resultContainer.appendChild(emoji);
14279
- const title = document.createElement("h2");
14280
- title.textContent = "Congratulations!";
14281
- title.style.cssText = "margin: 0 0 12px 0; font-size: 28px; font-weight: 700; color: #1a73e8;";
14282
- resultContainer.appendChild(title);
14283
- const prizeLabel = document.createElement("p");
14284
- prizeLabel.textContent = prize.prize_label;
14285
- prizeLabel.style.cssText = "margin: 0 0 20px 0; font-size: 20px; font-weight: 600; color: #333;";
14286
- resultContainer.appendChild(prizeLabel);
14287
- if (prize.coupon_code) {
14288
- const couponContainer = document.createElement("div");
14289
- couponContainer.style.cssText = `
14290
- background: #f5f5f5;
14291
- padding: 16px;
14292
- border-radius: 8px;
14293
- margin-bottom: 20px;
14294
- border: 2px dashed #1a73e8;
14295
- `;
14296
- const couponLabel = document.createElement("div");
14297
- couponLabel.textContent = "Your coupon code:";
14298
- couponLabel.style.cssText = "font-size: 12px; color: #666; margin-bottom: 8px;";
14299
- couponContainer.appendChild(couponLabel);
14300
- const couponCode = document.createElement("div");
14301
- couponCode.textContent = prize.coupon_code;
14302
- couponCode.style.cssText = "font-size: 24px; font-weight: 700; color: #1a73e8; letter-spacing: 2px;";
14303
- couponContainer.appendChild(couponCode);
14304
- resultContainer.appendChild(couponContainer);
14305
- }
14306
- const message = document.createElement("p");
14307
- message.textContent = "Check your WhatsApp/SMS for your discount code and cart link!";
14308
- message.style.cssText = "margin: 0 0 20px 0; font-size: 14px; color: #666;";
14309
- resultContainer.appendChild(message);
14310
- const closeButton = document.createElement("button");
14311
- closeButton.textContent = "Close";
14312
- closeButton.style.cssText = `
14313
- background: #1a73e8;
14314
- color: white;
14315
- border: none;
14316
- padding: 12px 32px;
14317
- border-radius: 8px;
14318
- font-weight: 600;
14319
- font-size: 14px;
14320
- cursor: pointer;
14321
- `;
14322
- closeButton.addEventListener("click", () => {
14323
- const overlay = modal.parentElement;
14324
- if (overlay && document.body.contains(overlay)) {
14325
- document.body.removeChild(overlay);
14326
- }
14327
- });
14328
- resultContainer.appendChild(closeButton);
14329
- modal.appendChild(resultContainer);
14330
- }
14331
- detectGeoRegion() {
14332
- const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
14333
- if (timezone.includes("America")) return "north_america";
14334
- if (timezone.includes("Europe")) return "europe";
14335
- if (timezone.includes("Asia/Kolkata") || timezone.includes("Asia/Calcutta")) return "india";
14336
- if (timezone.includes("Asia/Singapore") || timezone.includes("Asia/Bangkok") || timezone.includes("Asia/Jakarta")) return "southeast_asia";
14337
- if (timezone.includes("Asia/Dubai") || timezone.includes("Asia/Riyadh")) return "middle_east";
14338
- if (timezone.includes("America") && (timezone.includes("Sao_Paulo") || timezone.includes("Buenos_Aires"))) return "latin_america";
14339
- if (timezone.includes("Australia") || timezone.includes("Pacific/Auckland")) return "oceania";
14340
- return "north_america";
14341
- }
14342
13814
  getUTMParams() {
14343
13815
  const params = new URLSearchParams(window.location.search);
14344
13816
  return {
@@ -14347,119 +13819,6 @@ class AegisWidgetManager {
14347
13819
  utm_campaign: params.get("utm_campaign") || void 0
14348
13820
  };
14349
13821
  }
14350
- getSessionId() {
14351
- return sessionStorage.getItem("aegis_session_id");
14352
- }
14353
- getAnonymousId() {
14354
- return localStorage.getItem("aegis_anonymous_id");
14355
- }
14356
- renderScratchCard(widget) {
14357
- const overlay = document.createElement("div");
14358
- overlay.className = "aegis-gamification-overlay";
14359
- overlay.setAttribute("data-widget-id", widget.widget_id);
14360
- overlay.style.cssText = `
14361
- position: fixed;
14362
- top: 0;
14363
- left: 0;
14364
- right: 0;
14365
- bottom: 0;
14366
- background: rgba(0, 0, 0, 0.7);
14367
- z-index: 1000000;
14368
- display: flex;
14369
- align-items: center;
14370
- justify-content: center;
14371
- animation: aegisFadeIn 0.3s ease-out;
14372
- `;
14373
- const modal = document.createElement("div");
14374
- modal.className = "aegis-scratch-card-modal";
14375
- modal.style.cssText = `
14376
- background: white;
14377
- border-radius: 16px;
14378
- padding: 32px;
14379
- max-width: 500px;
14380
- width: 90%;
14381
- box-shadow: 0 10px 40px rgba(0,0,0,0.3);
14382
- text-align: center;
14383
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
14384
- animation: aegisScaleIn 0.3s ease-out;
14385
- `;
14386
- const title = document.createElement("h2");
14387
- title.textContent = widget.config.title || "Scratch & Win!";
14388
- title.style.cssText = "margin: 0 0 16px 0; font-size: 24px; font-weight: 700; color: #1a1a1a;";
14389
- modal.appendChild(title);
14390
- const description = document.createElement("p");
14391
- description.textContent = widget.config.description || "Scratch to reveal your prize!";
14392
- description.style.cssText = "margin: 0 0 24px 0; font-size: 14px; color: #666;";
14393
- modal.appendChild(description);
14394
- const canvas = document.createElement("canvas");
14395
- canvas.width = 300;
14396
- canvas.height = 200;
14397
- canvas.style.cssText = `
14398
- border-radius: 8px;
14399
- cursor: pointer;
14400
- margin: 0 auto 24px;
14401
- display: block;
14402
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
14403
- `;
14404
- modal.appendChild(canvas);
14405
- const ctx = canvas.getContext("2d");
14406
- if (ctx) {
14407
- ctx.fillStyle = "#c0c0c0";
14408
- ctx.fillRect(0, 0, canvas.width, canvas.height);
14409
- ctx.fillStyle = "#888";
14410
- ctx.font = "20px Arial";
14411
- ctx.textAlign = "center";
14412
- ctx.fillText("Scratch here!", canvas.width / 2, canvas.height / 2);
14413
- let isScratching = false;
14414
- const scratch = (x, y) => {
14415
- ctx.globalCompositeOperation = "destination-out";
14416
- ctx.beginPath();
14417
- ctx.arc(x, y, 20, 0, Math.PI * 2);
14418
- ctx.fill();
14419
- };
14420
- canvas.addEventListener("mousedown", () => {
14421
- isScratching = true;
14422
- });
14423
- canvas.addEventListener("mouseup", () => {
14424
- isScratching = false;
14425
- });
14426
- canvas.addEventListener("mousemove", (e) => {
14427
- if (isScratching) {
14428
- const rect = canvas.getBoundingClientRect();
14429
- scratch(e.clientX - rect.left, e.clientY - rect.top);
14430
- }
14431
- });
14432
- }
14433
- const scratchButton = document.createElement("button");
14434
- scratchButton.textContent = "REVEAL PRIZE";
14435
- scratchButton.style.cssText = `
14436
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
14437
- color: white;
14438
- border: none;
14439
- padding: 16px 48px;
14440
- border-radius: 8px;
14441
- font-weight: 700;
14442
- font-size: 16px;
14443
- cursor: pointer;
14444
- `;
14445
- scratchButton.addEventListener("click", async () => {
14446
- scratchButton.disabled = true;
14447
- scratchButton.textContent = "REVEALING...";
14448
- try {
14449
- const prize = await this.generatePrize(widget.config.config_id);
14450
- this.showPrizeResult(modal, prize);
14451
- } catch (error) {
14452
- this.log(`Error generating prize: ${error}`, true);
14453
- scratchButton.disabled = false;
14454
- scratchButton.textContent = "TRY AGAIN";
14455
- }
14456
- });
14457
- modal.appendChild(scratchButton);
14458
- overlay.appendChild(modal);
14459
- this.addAnimationStyles();
14460
- overlay.setAttribute("data-aegis-widget-root", "");
14461
- document.body.appendChild(overlay);
14462
- }
14463
13822
  renderToast(widget) {
14464
13823
  const { message, icon, duration } = widget.config;
14465
13824
  const position = widget.position || "bottom_left";
@@ -14732,82 +14091,6 @@ class AegisWidgetManager {
14732
14091
  overlay.setAttribute("data-aegis-widget-root", "");
14733
14092
  document.body.appendChild(overlay);
14734
14093
  }
14735
- async generatePrize(configId) {
14736
- const url = `${this.apiHost}/v1/widgets/gamification/generate-prize`;
14737
- const headers = {
14738
- "X-Aegis-Write-Key": this.writeKey,
14739
- "Content-Type": "application/json"
14740
- };
14741
- if (this.userId) headers["X-User-ID"] = this.userId;
14742
- if (this.contactId) headers["X-Contact-ID"] = this.contactId;
14743
- if (this.organizationId) headers["X-Organization-ID"] = this.organizationId;
14744
- const response = await fetch(url, {
14745
- method: "POST",
14746
- headers,
14747
- body: JSON.stringify({ config_id: configId }),
14748
- credentials: "include"
14749
- });
14750
- if (!response.ok) {
14751
- throw new Error(`Failed to generate prize: ${response.status}`);
14752
- }
14753
- return await response.json();
14754
- }
14755
- showPrizeResult(modal, prize) {
14756
- modal.innerHTML = "";
14757
- const resultContainer = document.createElement("div");
14758
- resultContainer.style.cssText = "text-align: center; padding: 40px 20px;";
14759
- const emoji = document.createElement("div");
14760
- emoji.textContent = "🎉";
14761
- emoji.style.cssText = "font-size: 60px; margin-bottom: 20px;";
14762
- resultContainer.appendChild(emoji);
14763
- const title = document.createElement("h2");
14764
- title.textContent = "Congratulations!";
14765
- title.style.cssText = "margin: 0 0 12px 0; font-size: 28px; font-weight: 700; color: #1a73e8;";
14766
- resultContainer.appendChild(title);
14767
- const prizeLabel = document.createElement("p");
14768
- prizeLabel.textContent = prize.prize_label;
14769
- prizeLabel.style.cssText = "margin: 0 0 20px 0; font-size: 20px; font-weight: 600; color: #333;";
14770
- resultContainer.appendChild(prizeLabel);
14771
- if (prize.coupon_code) {
14772
- const couponContainer = document.createElement("div");
14773
- couponContainer.style.cssText = `
14774
- background: #f5f5f5;
14775
- padding: 16px;
14776
- border-radius: 8px;
14777
- margin-bottom: 20px;
14778
- border: 2px dashed #1a73e8;
14779
- `;
14780
- const couponLabel = document.createElement("div");
14781
- couponLabel.textContent = "Your coupon code:";
14782
- couponLabel.style.cssText = "font-size: 12px; color: #666; margin-bottom: 8px;";
14783
- couponContainer.appendChild(couponLabel);
14784
- const couponCode = document.createElement("div");
14785
- couponCode.textContent = prize.coupon_code;
14786
- couponCode.style.cssText = "font-size: 24px; font-weight: 700; color: #1a73e8; letter-spacing: 2px;";
14787
- couponContainer.appendChild(couponCode);
14788
- resultContainer.appendChild(couponContainer);
14789
- }
14790
- const closeButton = document.createElement("button");
14791
- closeButton.textContent = "Close";
14792
- closeButton.style.cssText = `
14793
- background: #1a73e8;
14794
- color: white;
14795
- border: none;
14796
- padding: 12px 32px;
14797
- border-radius: 8px;
14798
- font-weight: 600;
14799
- font-size: 14px;
14800
- cursor: pointer;
14801
- `;
14802
- closeButton.addEventListener("click", () => {
14803
- const overlay = modal.parentElement;
14804
- if (overlay && document.body.contains(overlay)) {
14805
- document.body.removeChild(overlay);
14806
- }
14807
- });
14808
- resultContainer.appendChild(closeButton);
14809
- modal.appendChild(resultContainer);
14810
- }
14811
14094
  async submitFeedback(widgetId, data) {
14812
14095
  var _a;
14813
14096
  const url = `${this.apiHost}/v1/widgets/track-event`;
@@ -14817,6 +14100,7 @@ class AegisWidgetManager {
14817
14100
  };
14818
14101
  if (this.userId) headers["X-User-ID"] = this.userId;
14819
14102
  if (this.contactId) headers["X-Contact-ID"] = this.contactId;
14103
+ if (this.contactId && this.contactToken) headers["X-Contact-Token"] = this.contactToken;
14820
14104
  if (this.organizationId) headers["X-Organization-ID"] = this.organizationId;
14821
14105
  const workspaceId = (_a = this.getWorkspaceId) == null ? void 0 : _a.call(this);
14822
14106
  const response = await fetch(url, {
@@ -14844,6 +14128,7 @@ class AegisWidgetManager {
14844
14128
  };
14845
14129
  if (this.userId) headers["X-User-ID"] = this.userId;
14846
14130
  if (this.contactId) headers["X-Contact-ID"] = this.contactId;
14131
+ if (this.contactId && this.contactToken) headers["X-Contact-Token"] = this.contactToken;
14847
14132
  if (this.organizationId) headers["X-Organization-ID"] = this.organizationId;
14848
14133
  const workspaceId = (_a = this.getWorkspaceId) == null ? void 0 : _a.call(this);
14849
14134
  await fetch(url, {
@@ -15361,8 +14646,9 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15361
14646
  constructor(config) {
15362
14647
  var _a;
15363
14648
  this.initialized = false;
14649
+ this.armedInAppTriggers = /* @__PURE__ */ new Set();
15364
14650
  {
15365
- const v = "1.39.0";
14651
+ const v = "1.41.0";
15366
14652
  try {
15367
14653
  globalThis.__AEGIS_SDK_VERSION__ = v;
15368
14654
  } catch {
@@ -15381,13 +14667,17 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15381
14667
  }
15382
14668
  const triggerEngine = config.triggerEngine ?? new TriggerEngine();
15383
14669
  const ownsTriggerEngine = !config.triggerEngine;
14670
+ this.triggerEngine = triggerEngine;
14671
+ this.ownsTriggerEngine = ownsTriggerEngine;
15384
14672
  this.contactId = config.contactId;
14673
+ this.contactToken = config.contactToken;
15385
14674
  this.pairingApiHost = config.apiHost ?? "";
15386
14675
  this.widgets = new AegisWidgetManager({
15387
14676
  writeKey: config.writeKey,
15388
14677
  apiHost: config.apiHost,
15389
14678
  userId: config.userId,
15390
14679
  contactId: config.contactId,
14680
+ contactToken: config.contactToken,
15391
14681
  organizationId: config.organizationId,
15392
14682
  debugMode: config.debugMode,
15393
14683
  triggerEngine,
@@ -15415,12 +14705,6 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15415
14705
  propertyId: config.propertyId,
15416
14706
  debugMode: config.debugMode,
15417
14707
  enableSSE: config.enableSSE,
15418
- // Route gamification sub_types straight to the widget renderer —
15419
- // replaces the old CustomEvent('aegis:render-widget') bridge which
15420
- // had no listener and silently dropped impressions.
15421
- onInteractiveCampaign: (campaign) => {
15422
- this.widgets.renderInteractiveCampaign(campaign);
15423
- },
15424
14708
  getWorkspaceId: config.getWorkspaceId,
15425
14709
  // First-party cart reader — renderers (progress-bar) read live cart
15426
14710
  // state from here instead of `window.*` cart globals. See setCartState().
@@ -15459,13 +14743,36 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15459
14743
  debugMode: config.debugMode,
15460
14744
  ...config.chat
15461
14745
  });
15462
- const proactive = ((_a = config.chat) == null ? void 0 : _a.proactiveTriggers) ?? [];
15463
- for (const evt of proactive) {
15464
- triggerEngine.on(evt, () => {
14746
+ const proactive = (((_a = config.chat) == null ? void 0 : _a.proactiveTriggers) ?? []).map(triggerFromEngineEvent);
14747
+ armBehaviouralTriggers(
14748
+ triggerEngine,
14749
+ proactive,
14750
+ /* @__PURE__ */ new Set(),
14751
+ () => {
15465
14752
  var _a2;
15466
14753
  return (_a2 = this.chat) == null ? void 0 : _a2.openPanel();
15467
- });
15468
- }
14754
+ },
14755
+ _AegisMessageRuntime.hasNoHover()
14756
+ );
14757
+ }
14758
+ this.unsubscribeCampaignsLoaded = this.inApp.onCampaignsLoaded((campaigns) => {
14759
+ armBehaviouralTriggers(
14760
+ triggerEngine,
14761
+ campaigns.map((c) => c.client_trigger),
14762
+ this.armedInAppTriggers,
14763
+ (eventName) => this.inApp.onClientEvent(eventName),
14764
+ _AegisMessageRuntime.hasNoHover()
14765
+ );
14766
+ });
14767
+ }
14768
+ /** No hovering pointer (phones, tablets): exit intent is a fast scroll up,
14769
+ * not the mouse leaving the window. */
14770
+ static hasNoHover() {
14771
+ var _a, _b;
14772
+ try {
14773
+ return typeof window !== "undefined" && ((_b = (_a = window.matchMedia) == null ? void 0 : _a.call(window, "(hover: none)")) == null ? void 0 : _b.matches) === true;
14774
+ } catch {
14775
+ return false;
15469
14776
  }
15470
14777
  }
15471
14778
  /**
@@ -15482,8 +14789,11 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15482
14789
  this.widgets.initialize()
15483
14790
  ]);
15484
14791
  (_a = this.chat) == null ? void 0 : _a.initialize();
14792
+ if (this.ownsTriggerEngine) {
14793
+ this.triggerEngine.start();
14794
+ }
15485
14795
  if (this.contactId) {
15486
- void this.contactScores.fetchForContact(this.contactId);
14796
+ void this.contactScores.fetchForContact(this.contactId, { contactToken: this.contactToken });
15487
14797
  }
15488
14798
  }
15489
14799
  /**
@@ -15497,10 +14807,11 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15497
14807
  async updateContactId(contactId, contactToken) {
15498
14808
  var _a, _b, _c;
15499
14809
  this.contactId = contactId;
14810
+ this.contactToken = contactToken;
15500
14811
  (_b = (_a = this.inApp).updateContactId) == null ? void 0 : _b.call(_a, contactId, contactToken);
15501
14812
  (_c = this.chat) == null ? void 0 : _c.updateContactId(contactId);
15502
- await this.widgets.updateContactId(contactId);
15503
- void this.contactScores.fetchForContact(contactId);
14813
+ await this.widgets.updateContactId(contactId, contactToken);
14814
+ void this.contactScores.fetchForContact(contactId, { contactToken });
15504
14815
  }
15505
14816
  /**
15506
14817
  * Forward a client-side event (product_viewed, cart_idle_90s, etc.)
@@ -15690,6 +15001,7 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15690
15001
  */
15691
15002
  destroy() {
15692
15003
  var _a, _b, _c;
15004
+ this.unsubscribeCampaignsLoaded();
15693
15005
  (_b = (_a = this.inApp).destroy) == null ? void 0 : _b.call(_a);
15694
15006
  this.widgets.destroy();
15695
15007
  (_c = this.chat) == null ? void 0 : _c.destroy();
@@ -15698,8 +15010,8 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15698
15010
  /**
15699
15011
  * Current armed campaigns visible to the InAppManager. Accessible for
15700
15012
  * the Prefetch Inspector and for debugging. WidgetManager's prefetched
15701
- * spin_wheel / scratch_card configs are NOT in this list — they live
15702
- * in `this.widgets` and have their own lifecycle.
15013
+ * exit-intent popup config is NOT in this list — it lives in
15014
+ * `this.widgets` and has its own lifecycle.
15703
15015
  */
15704
15016
  getCampaigns() {
15705
15017
  return this.inApp.campaigns ?? [];