@active-reach/web-sdk 1.40.0 → 1.42.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
@@ -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;
@@ -6519,6 +6598,34 @@ const _AegisInAppManager = class _AegisInAppManager {
6519
6598
  }
6520
6599
  return true;
6521
6600
  }
6601
+ /**
6602
+ * Whether this tab recorded a conversion (`notifyConversion`, persisted per
6603
+ * goal in sessionStorage). The smart trigger's `converted_in_session`.
6604
+ */
6605
+ hasConvertedThisSession() {
6606
+ var _a;
6607
+ if (typeof sessionStorage === "undefined") return false;
6608
+ try {
6609
+ for (let i = 0; i < sessionStorage.length; i++) {
6610
+ if ((_a = sessionStorage.key(i)) == null ? void 0 : _a.startsWith(_AegisInAppManager.CONVERSION_STORAGE_PREFIX)) return true;
6611
+ }
6612
+ } catch {
6613
+ }
6614
+ return false;
6615
+ }
6616
+ /**
6617
+ * Display one loaded trigger-gated campaign by id — a smart trigger's fire
6618
+ * decision (MicroIntentArming). False when it is not loaded, already shown,
6619
+ * suppressed, or the host cancelled it (`campaign-will-show`).
6620
+ */
6621
+ showTriggeredCampaign(campaignId) {
6622
+ const campaign = this.campaigns.find((c) => c.id === campaignId);
6623
+ if (!campaign || this.displayedCampaigns.has(campaignId) || this.isSuppressed(campaignId)) {
6624
+ return false;
6625
+ }
6626
+ this.displayCampaign(campaign);
6627
+ return this.displayedCampaigns.has(campaignId);
6628
+ }
6522
6629
  connectSSE() {
6523
6630
  if (this.eventSource) {
6524
6631
  this.disconnectSSE();
@@ -9036,6 +9143,9 @@ const _AegisInAppManager = class _AegisInAppManager {
9036
9143
  * matches config.product_id (string or string[])
9037
9144
  * - `delay` : client-side setTimeout — evaluated at armeng
9038
9145
  * time (kicked off from `displayCampaign`), not here.
9146
+ * - `exit_intent` / `scroll_depth` / `time_on_page` / `inactivity` : the
9147
+ * TriggerEngine event named by `behaviouralTriggerEvent`;
9148
+ * AegisMessageRuntime arms the engine and forwards it.
9039
9149
  */
9040
9150
  onClientEvent(eventName, eventData = {}) {
9041
9151
  for (const c of this.campaigns) {
@@ -9065,8 +9175,10 @@ const _AegisInAppManager = class _AegisInAppManager {
9065
9175
  );
9066
9176
  return wanted.includes(actual);
9067
9177
  }
9068
- default:
9069
- return false;
9178
+ default: {
9179
+ const behavioural = behaviouralTriggerEvent(trigger);
9180
+ return behavioural !== null && behavioural === eventName;
9181
+ }
9070
9182
  }
9071
9183
  }
9072
9184
  displayCampaign(campaign) {
@@ -9290,11 +9402,7 @@ const _AegisInAppManager = class _AegisInAppManager {
9290
9402
  break;
9291
9403
  case "spin_wheel":
9292
9404
  case "scratch_card":
9293
- if (this.onInteractiveCampaign) {
9294
- this.onInteractiveCampaign(campaign);
9295
- } else {
9296
- this.renderGamificationOverlay(campaign, ic, bg, text);
9297
- }
9405
+ this.renderGamificationOverlay(campaign, ic, bg, text);
9298
9406
  break;
9299
9407
  default:
9300
9408
  this.log(`Unknown interactive sub_type: ${campaign.sub_type}`, "warn");
@@ -13275,39 +13383,6 @@ class AegisWidgetManager {
13275
13383
  this.preloadWidgetAssets();
13276
13384
  }
13277
13385
  }
13278
- /**
13279
- * Render an in-app campaign whose sub_type routes it to the
13280
- * gamification-widget code path (spin_wheel / scratch_card).
13281
- * InAppCampaign is adapted to the minimal WidgetConfig shape that
13282
- * the internal renderers already understand — no rewrite needed.
13283
- *
13284
- * AegisMessageRuntime wires this method as AegisInAppManager's
13285
- * `onInteractiveCampaign` callback so impressions from the inbox
13286
- * prefetch bundle actually land on screen.
13287
- */
13288
- renderInteractiveCampaign(campaign) {
13289
- if (this.isDestroyed) return;
13290
- if (typeof document === "undefined") return;
13291
- const subType = campaign.sub_type;
13292
- if (subType !== "spin_wheel" && subType !== "scratch_card") {
13293
- this.log(`renderInteractiveCampaign: unsupported sub_type "${subType}"`, true);
13294
- return;
13295
- }
13296
- const widget = {
13297
- widget_id: campaign.id,
13298
- widget_type: subType,
13299
- name: campaign.title,
13300
- config: campaign.interactive_config ?? {},
13301
- priority: campaign.priority ?? 0
13302
- };
13303
- if (this.renderedWidgets.has(widget.widget_id)) return;
13304
- this.renderedWidgets.add(widget.widget_id);
13305
- if (subType === "spin_wheel") {
13306
- this.renderSpinWheel(widget);
13307
- } else {
13308
- this.renderScratchCard(widget);
13309
- }
13310
- }
13311
13386
  /**
13312
13387
  * Tear down everything the manager installed: trigger listeners (if
13313
13388
  * we own the TriggerEngine), injected style sheets, and any rendered
@@ -13483,15 +13558,6 @@ class AegisWidgetManager {
13483
13558
  this.log(`Preloading exit intent image: ${imageUrl}`);
13484
13559
  }
13485
13560
  }
13486
- const spinWheelConfig = this.prefetchWidgetConfigs.spin_wheel;
13487
- if (spinWheelConfig == null ? void 0 : spinWheelConfig.enabled) {
13488
- const imageUrl = spinWheelConfig.image_url;
13489
- if (imageUrl) {
13490
- const img = new Image();
13491
- img.src = imageUrl;
13492
- this.log(`Preloading spin wheel image: ${imageUrl}`);
13493
- }
13494
- }
13495
13561
  } catch (error) {
13496
13562
  this.log(`Error preloading widget assets: ${error}`, true);
13497
13563
  }
@@ -13588,67 +13654,7 @@ class AegisWidgetManager {
13588
13654
  if (!this.enablePrefetch || !this.triggerEngine) {
13589
13655
  return;
13590
13656
  }
13591
- const spinWheelConfig = this.prefetchWidgetConfigs.spin_wheel;
13592
13657
  const exitIntentConfig = this.prefetchWidgetConfigs.exit_intent;
13593
- if (spinWheelConfig && spinWheelConfig.enabled) {
13594
- const isMobile2 = this.isMobileDevice();
13595
- const mobileConfig2 = (exitIntentConfig == null ? void 0 : exitIntentConfig.mobile_triggers) || {};
13596
- const handleSpinWheelIntent = () => {
13597
- const widgetId = `prefetch_spin_wheel_${Date.now()}`;
13598
- if (this.renderedWidgets.has(widgetId)) {
13599
- return;
13600
- }
13601
- this.renderedWidgets.add(widgetId);
13602
- const detectedCart = this.detectPlatformCart();
13603
- if (spinWheelConfig.type === "cart_recovery" && detectedCart) {
13604
- this.cartData = detectedCart;
13605
- this.renderSpinWheelWidget(widgetId, spinWheelConfig);
13606
- this.sendCartAbandonmentBeacon();
13607
- } else if (spinWheelConfig.type === "lead_gen") {
13608
- this.renderSpinWheelWidget(widgetId, spinWheelConfig);
13609
- }
13610
- };
13611
- if (isMobile2) {
13612
- const scrollVelocityEnabled = mobileConfig2.scroll_velocity !== false;
13613
- if (scrollVelocityEnabled) {
13614
- this.triggerEngine.registerScrollVelocity({
13615
- threshold: mobileConfig2.scroll_threshold || 0.5,
13616
- minScrollPosition: mobileConfig2.scroll_min_position || 100,
13617
- cooldown: mobileConfig2.scroll_cooldown || 5e3
13618
- });
13619
- this.triggerEngine.on("mobile_exit_intent", handleSpinWheelIntent);
13620
- this.log("Registered mobile scroll velocity trigger for spin wheel");
13621
- }
13622
- const idleTimerEnabled = mobileConfig2.idle_timer !== false;
13623
- if (idleTimerEnabled) {
13624
- const idleSeconds = mobileConfig2.idle_seconds || 15;
13625
- this.triggerEngine.registerInactivity(idleSeconds);
13626
- this.triggerEngine.on(`inactivity_${idleSeconds}`, handleSpinWheelIntent);
13627
- this.log(`Registered mobile idle timer trigger for spin wheel: ${idleSeconds}s`);
13628
- }
13629
- const visibilityChangeEnabled = mobileConfig2.visibility_change !== false;
13630
- if (visibilityChangeEnabled) {
13631
- const detectedCart = this.detectPlatformCart();
13632
- if (detectedCart && detectedCart.cart_items && detectedCart.cart_items.length > 0) {
13633
- this.triggerEngine.registerVisibilityChange();
13634
- this.triggerEngine.on("visibility_hidden", handleSpinWheelIntent);
13635
- this.log("Registered mobile visibility change trigger for spin wheel (cart detected)");
13636
- }
13637
- }
13638
- const backButtonEnabled = mobileConfig2.back_button === true;
13639
- if (backButtonEnabled) {
13640
- this.triggerEngine.registerBackButton();
13641
- this.triggerEngine.on("back_button", handleSpinWheelIntent);
13642
- this.log("Registered mobile back button trigger for spin wheel");
13643
- }
13644
- this.log(`Setup mobile spin wheel: type=${spinWheelConfig.type}, priority=${spinWheelConfig.priority}`);
13645
- } else {
13646
- this.triggerEngine.registerExitIntent();
13647
- this.triggerEngine.on("exit_intent", handleSpinWheelIntent);
13648
- this.log(`Setup desktop spin wheel exit intent: type=${spinWheelConfig.type}, priority=${spinWheelConfig.priority}`);
13649
- }
13650
- return;
13651
- }
13652
13658
  if (!exitIntentConfig || !exitIntentConfig.enabled) {
13653
13659
  this.log("No exit intent config found in prefetch");
13654
13660
  return;
@@ -13765,12 +13771,6 @@ class AegisWidgetManager {
13765
13771
  case "chat_bubble":
13766
13772
  this.renderChatBubble(widget);
13767
13773
  break;
13768
- case "spin_wheel":
13769
- this.renderSpinWheel(widget);
13770
- break;
13771
- case "scratch_card":
13772
- this.renderScratchCard(widget);
13773
- break;
13774
13774
  case "toast":
13775
13775
  this.renderToast(widget);
13776
13776
  break;
@@ -13839,531 +13839,6 @@ class AegisWidgetManager {
13839
13839
  bubble.setAttribute("data-aegis-widget-root", "");
13840
13840
  document.body.appendChild(bubble);
13841
13841
  }
13842
- renderSpinWheel(widget) {
13843
- const overlay = document.createElement("div");
13844
- overlay.className = "aegis-gamification-overlay";
13845
- overlay.setAttribute("data-widget-id", widget.widget_id);
13846
- overlay.style.cssText = `
13847
- position: fixed;
13848
- top: 0;
13849
- left: 0;
13850
- right: 0;
13851
- bottom: 0;
13852
- background: rgba(0, 0, 0, 0.7);
13853
- z-index: 1000000;
13854
- display: flex;
13855
- align-items: center;
13856
- justify-content: center;
13857
- animation: aegisFadeIn 0.3s ease-out;
13858
- `;
13859
- const modal = document.createElement("div");
13860
- modal.className = "aegis-spin-wheel-modal";
13861
- modal.style.cssText = `
13862
- background: white;
13863
- border-radius: 16px;
13864
- padding: 32px;
13865
- max-width: 500px;
13866
- width: 90%;
13867
- box-shadow: 0 10px 40px rgba(0,0,0,0.3);
13868
- text-align: center;
13869
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
13870
- animation: aegisScaleIn 0.3s ease-out;
13871
- `;
13872
- const title = document.createElement("h2");
13873
- title.textContent = widget.config.title || "Spin to Win!";
13874
- title.style.cssText = "margin: 0 0 16px 0; font-size: 24px; font-weight: 700; color: #1a1a1a;";
13875
- modal.appendChild(title);
13876
- const description = document.createElement("p");
13877
- description.textContent = widget.config.description || "Try your luck and win exclusive prizes!";
13878
- description.style.cssText = "margin: 0 0 24px 0; font-size: 14px; color: #666;";
13879
- modal.appendChild(description);
13880
- const segments = Array.isArray(
13881
- widget.config.segments
13882
- ) ? widget.config.segments : [
13883
- { label: "Prize 1", color: "#ff6b6b" },
13884
- { label: "Prize 2", color: "#feca57" },
13885
- { label: "Prize 3", color: "#48dbfb" },
13886
- { label: "Prize 4", color: "#ff6348" }
13887
- ];
13888
- const SVG_NS2 = "http://www.w3.org/2000/svg";
13889
- const wheel = document.createElementNS(SVG_NS2, "svg");
13890
- wheel.setAttribute("viewBox", "-160 -160 320 320");
13891
- wheel.setAttribute("width", "300");
13892
- wheel.setAttribute("height", "300");
13893
- wheel.style.cssText = "margin: 0 auto 24px; display: block;";
13894
- const n = segments.length;
13895
- const anglePer = 2 * Math.PI / n;
13896
- const radius = 140;
13897
- for (let i = 0; i < n; i++) {
13898
- const start = i * anglePer - Math.PI / 2;
13899
- const end = start + anglePer;
13900
- const x1 = Math.cos(start) * radius;
13901
- const y1 = Math.sin(start) * radius;
13902
- const x2 = Math.cos(end) * radius;
13903
- const y2 = Math.sin(end) * radius;
13904
- const largeArc = anglePer > Math.PI ? 1 : 0;
13905
- const path = document.createElementNS(SVG_NS2, "path");
13906
- path.setAttribute(
13907
- "d",
13908
- `M 0 0 L ${x1.toFixed(2)} ${y1.toFixed(2)} A ${radius} ${radius} 0 ${largeArc} 1 ${x2.toFixed(2)} ${y2.toFixed(2)} Z`
13909
- );
13910
- path.setAttribute("fill", this.sanitizeColor(segments[i].color || "#cccccc"));
13911
- path.setAttribute("stroke", "#ffffff");
13912
- path.setAttribute("stroke-width", "2");
13913
- wheel.appendChild(path);
13914
- const labelAngle = start + anglePer / 2;
13915
- const lx = Math.cos(labelAngle) * radius * 0.65;
13916
- const ly = Math.sin(labelAngle) * radius * 0.65;
13917
- const text = document.createElementNS(SVG_NS2, "text");
13918
- text.setAttribute("x", lx.toFixed(2));
13919
- text.setAttribute("y", ly.toFixed(2));
13920
- text.setAttribute("fill", "#ffffff");
13921
- text.setAttribute("font-size", "13");
13922
- text.setAttribute("font-weight", "600");
13923
- text.setAttribute("text-anchor", "middle");
13924
- text.setAttribute("dominant-baseline", "middle");
13925
- text.textContent = segments[i].label || `#${i + 1}`;
13926
- wheel.appendChild(text);
13927
- }
13928
- modal.appendChild(wheel);
13929
- const spinButton = document.createElement("button");
13930
- spinButton.textContent = "SPIN NOW!";
13931
- spinButton.style.cssText = `
13932
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
13933
- color: white;
13934
- border: none;
13935
- padding: 16px 48px;
13936
- border-radius: 8px;
13937
- font-weight: 700;
13938
- font-size: 16px;
13939
- cursor: pointer;
13940
- margin-bottom: 16px;
13941
- `;
13942
- spinButton.addEventListener("click", async () => {
13943
- spinButton.disabled = true;
13944
- spinButton.textContent = "SPINNING...";
13945
- try {
13946
- const prize = await this.generatePrize(widget.config.config_id);
13947
- wheel.style.animation = "aegisSpin 2s ease-out";
13948
- setTimeout(() => {
13949
- this.showPrizeResult(modal, prize);
13950
- }, 2e3);
13951
- } catch (error) {
13952
- this.log(`Error generating prize: ${error}`, true);
13953
- spinButton.disabled = false;
13954
- spinButton.textContent = "TRY AGAIN";
13955
- }
13956
- });
13957
- modal.appendChild(spinButton);
13958
- const closeButton = document.createElement("button");
13959
- closeButton.textContent = "×";
13960
- closeButton.setAttribute("aria-label", "Close");
13961
- closeButton.style.cssText = `
13962
- position: absolute;
13963
- top: 16px;
13964
- right: 16px;
13965
- background: transparent;
13966
- border: none;
13967
- font-size: 28px;
13968
- cursor: pointer;
13969
- color: #999;
13970
- `;
13971
- closeButton.addEventListener("click", () => {
13972
- this.trackEvent(widget.widget_id, "dismiss");
13973
- document.body.removeChild(overlay);
13974
- this.renderedWidgets.delete(widget.widget_id);
13975
- });
13976
- modal.style.position = "relative";
13977
- modal.appendChild(closeButton);
13978
- overlay.appendChild(modal);
13979
- this.addAnimationStyles();
13980
- overlay.setAttribute("data-aegis-widget-root", "");
13981
- document.body.appendChild(overlay);
13982
- }
13983
- renderSpinWheelWidget(widgetId, config) {
13984
- if (this.isDestroyed) return;
13985
- const cart = this.detectPlatformCart();
13986
- if (config.type === "cart_recovery" && !cart) {
13987
- this.log("Spin wheel requires cart, but none detected");
13988
- return;
13989
- }
13990
- if (cart) {
13991
- this.cartData = cart;
13992
- }
13993
- const customization = this.cssCustomization.spinWheel || {};
13994
- const accentColor = customization.accentColor || config.accent_color || "#FF6B6B";
13995
- const backgroundColor = customization.backgroundColor || config.background_color || "#FFFFFF";
13996
- const textColor = customization.textColor || config.text_color || "#333333";
13997
- const buttonColor = customization.buttonColor || config.button_color || accentColor;
13998
- const wheelColors = customization.wheelColors || ["#FF6B6B", "#4ECDC4", "#FFE66D", "#95E1D3"];
13999
- const overlay = document.createElement("div");
14000
- overlay.className = "aegis-spin-wheel-overlay";
14001
- overlay.setAttribute("data-widget-id", widgetId);
14002
- overlay.style.cssText = `
14003
- position: fixed;
14004
- top: 0;
14005
- left: 0;
14006
- width: 100%;
14007
- height: 100%;
14008
- background: rgba(0, 0, 0, 0.7);
14009
- display: flex;
14010
- align-items: center;
14011
- justify-content: center;
14012
- z-index: 999999;
14013
- animation: aegisFadeIn 0.3s ease;
14014
- `;
14015
- const modal = document.createElement("div");
14016
- modal.className = "aegis-spin-wheel-modal";
14017
- modal.style.cssText = `
14018
- background: ${this.sanitizeColor(backgroundColor)};
14019
- border-radius: 16px;
14020
- padding: 32px;
14021
- max-width: 500px;
14022
- width: 90%;
14023
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
14024
- position: relative;
14025
- animation: aegisScaleIn 0.4s ease;
14026
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
14027
- `;
14028
- const closeBtn = document.createElement("button");
14029
- closeBtn.innerHTML = lucideSvg("x", { size: 16 });
14030
- closeBtn.setAttribute("aria-label", "Close");
14031
- closeBtn.className = "aegis-spin-wheel-close";
14032
- closeBtn.style.cssText = `
14033
- position: absolute;
14034
- top: 16px;
14035
- right: 16px;
14036
- background: none;
14037
- border: none;
14038
- font-size: 24px;
14039
- cursor: pointer;
14040
- color: #999;
14041
- `;
14042
- closeBtn.onclick = () => {
14043
- this.trackEvent(widgetId, "dismiss", { type: "spin_wheel" });
14044
- document.body.removeChild(overlay);
14045
- this.renderedWidgets.delete(widgetId);
14046
- };
14047
- modal.appendChild(closeBtn);
14048
- const title = document.createElement("h2");
14049
- title.textContent = this.interpolateCartVariables(config.title || "Spin to Win!");
14050
- title.style.cssText = `
14051
- margin: 0 0 16px 0;
14052
- font-size: 28px;
14053
- font-weight: bold;
14054
- text-align: center;
14055
- color: ${this.sanitizeColor(textColor)};
14056
- `;
14057
- modal.appendChild(title);
14058
- if (cart) {
14059
- const subtitle = document.createElement("p");
14060
- subtitle.textContent = `Complete your ${cart.cart_currency} ${cart.cart_total.toFixed(2)} order and save!`;
14061
- subtitle.style.cssText = `
14062
- margin: 0 0 24px 0;
14063
- font-size: 16px;
14064
- text-align: center;
14065
- color: #666;
14066
- `;
14067
- modal.appendChild(subtitle);
14068
- }
14069
- const wheelContainer = document.createElement("div");
14070
- wheelContainer.className = "aegis-wheel-container";
14071
- wheelContainer.style.cssText = `
14072
- width: 300px;
14073
- height: 300px;
14074
- margin: 0 auto 24px auto;
14075
- background: conic-gradient(
14076
- from 0deg,
14077
- ${wheelColors[0]} 0deg 90deg,
14078
- ${wheelColors[1]} 90deg 180deg,
14079
- ${wheelColors[2]} 180deg 270deg,
14080
- ${wheelColors[3]} 270deg 360deg
14081
- );
14082
- border-radius: 50%;
14083
- position: relative;
14084
- display: flex;
14085
- align-items: center;
14086
- justify-content: center;
14087
- `;
14088
- const form = document.createElement("form");
14089
- form.className = "aegis-spin-form";
14090
- form.style.cssText = "display: block;";
14091
- const phoneInput = document.createElement("input");
14092
- phoneInput.type = "tel";
14093
- phoneInput.placeholder = "Enter your phone number";
14094
- phoneInput.required = true;
14095
- phoneInput.style.cssText = `
14096
- width: 100%;
14097
- padding: 14px;
14098
- border: 2px solid #ddd;
14099
- border-radius: 8px;
14100
- font-size: 16px;
14101
- margin-bottom: 12px;
14102
- box-sizing: border-box;
14103
- `;
14104
- form.appendChild(phoneInput);
14105
- const emailInput = document.createElement("input");
14106
- emailInput.type = "email";
14107
- emailInput.placeholder = "Enter your email (optional)";
14108
- emailInput.style.cssText = `
14109
- width: 100%;
14110
- padding: 14px;
14111
- border: 2px solid #ddd;
14112
- border-radius: 8px;
14113
- font-size: 16px;
14114
- margin-bottom: 12px;
14115
- box-sizing: border-box;
14116
- `;
14117
- form.appendChild(emailInput);
14118
- const nameInput = document.createElement("input");
14119
- nameInput.type = "text";
14120
- nameInput.placeholder = "Enter your name (optional)";
14121
- nameInput.style.cssText = `
14122
- width: 100%;
14123
- padding: 14px;
14124
- border: 2px solid #ddd;
14125
- border-radius: 8px;
14126
- font-size: 16px;
14127
- margin-bottom: 12px;
14128
- box-sizing: border-box;
14129
- `;
14130
- form.appendChild(nameInput);
14131
- const collectBirthday = config.collect_birthday === true;
14132
- let birthdayInput = null;
14133
- if (collectBirthday) {
14134
- const birthdayId = `aegis-spin-dob-${widgetId}`;
14135
- const birthdayLabel = document.createElement("label");
14136
- birthdayLabel.htmlFor = birthdayId;
14137
- birthdayLabel.textContent = "Birthday (optional) — for a birthday treat";
14138
- birthdayLabel.style.cssText = `
14139
- display: block;
14140
- font-size: 12px;
14141
- font-weight: 600;
14142
- opacity: 0.75;
14143
- margin-bottom: 4px;
14144
- color: ${this.sanitizeColor(textColor)};
14145
- `;
14146
- form.appendChild(birthdayLabel);
14147
- birthdayInput = document.createElement("input");
14148
- birthdayInput.id = birthdayId;
14149
- birthdayInput.type = "date";
14150
- birthdayInput.max = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
14151
- birthdayInput.style.cssText = `
14152
- width: 100%;
14153
- padding: 14px;
14154
- border: 2px solid #ddd;
14155
- border-radius: 8px;
14156
- font-size: 16px;
14157
- margin-bottom: 12px;
14158
- box-sizing: border-box;
14159
- `;
14160
- form.appendChild(birthdayInput);
14161
- }
14162
- const submitButton = document.createElement("button");
14163
- submitButton.type = "submit";
14164
- submitButton.textContent = "Spin the Wheel!";
14165
- submitButton.style.cssText = `
14166
- width: 100%;
14167
- padding: 14px;
14168
- background: ${this.sanitizeColor(buttonColor)};
14169
- color: white;
14170
- border: none;
14171
- border-radius: 8px;
14172
- font-size: 16px;
14173
- font-weight: 700;
14174
- cursor: pointer;
14175
- `;
14176
- form.appendChild(submitButton);
14177
- const errorMessage = document.createElement("div");
14178
- errorMessage.className = "aegis-spin-error";
14179
- errorMessage.style.cssText = `
14180
- color: #d32f2f;
14181
- font-size: 14px;
14182
- margin-top: 12px;
14183
- text-align: center;
14184
- display: none;
14185
- `;
14186
- form.appendChild(errorMessage);
14187
- form.addEventListener("submit", async (e) => {
14188
- e.preventDefault();
14189
- const phone = phoneInput.value.trim();
14190
- const email = emailInput.value.trim();
14191
- const name = nameInput.value.trim();
14192
- const dateOfBirth = birthdayInput ? birthdayInput.value.trim() : "";
14193
- if (!this.validatePhone(phone)) {
14194
- errorMessage.textContent = "Please enter a valid phone number (e.g., +1234567890)";
14195
- errorMessage.style.display = "block";
14196
- return;
14197
- }
14198
- if (email && !this.validateEmail(email)) {
14199
- errorMessage.textContent = "Please enter a valid email address";
14200
- errorMessage.style.display = "block";
14201
- return;
14202
- }
14203
- errorMessage.style.display = "none";
14204
- submitButton.disabled = true;
14205
- submitButton.textContent = "Spinning...";
14206
- wheelContainer.style.animation = "aegisSpin 2s ease-out";
14207
- setTimeout(async () => {
14208
- try {
14209
- const prize = await this.submitSpinWheel({
14210
- phone,
14211
- email,
14212
- name,
14213
- dateOfBirth,
14214
- cart,
14215
- widgetId
14216
- });
14217
- this.showSpinWheelPrize(modal, prize);
14218
- this.trackEvent(widgetId, "submit", {
14219
- type: "spin_wheel",
14220
- prize_label: prize.prize_label,
14221
- has_email: !!email
14222
- });
14223
- } catch (error) {
14224
- this.log(`Error submitting spin wheel: ${error}`, true);
14225
- errorMessage.textContent = "Failed to submit. Please try again.";
14226
- errorMessage.style.display = "block";
14227
- submitButton.disabled = false;
14228
- submitButton.textContent = "Spin the Wheel!";
14229
- wheelContainer.style.animation = "";
14230
- }
14231
- }, 2e3);
14232
- });
14233
- modal.appendChild(wheelContainer);
14234
- modal.appendChild(form);
14235
- overlay.appendChild(modal);
14236
- this.addAnimationStyles();
14237
- overlay.setAttribute("data-aegis-widget-root", "");
14238
- document.body.appendChild(overlay);
14239
- this.trackEvent(widgetId, "show", { type: "spin_wheel" });
14240
- }
14241
- validatePhone(phone) {
14242
- const e164Regex = /^\+?[1-9]\d{1,14}$/;
14243
- return e164Regex.test(phone.replace(/[\s()-]/g, ""));
14244
- }
14245
- validateEmail(email) {
14246
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
14247
- return emailRegex.test(email);
14248
- }
14249
- async submitSpinWheel(data) {
14250
- var _a, _b, _c, _d, _e;
14251
- const url = `${this.apiHost}/v1/widgets/spin-wheel/submit`;
14252
- const headers = {
14253
- "X-Aegis-Write-Key": this.writeKey,
14254
- "Content-Type": "application/json"
14255
- };
14256
- if (this.organizationId) headers["X-Organization-ID"] = this.organizationId;
14257
- const geoRegion = this.detectGeoRegion();
14258
- const deviceType = this.getDeviceType();
14259
- const utmParams = this.getUTMParams();
14260
- const body = {
14261
- phone: data.phone.replace(/[\s()-]/g, ""),
14262
- email: data.email || void 0,
14263
- name: data.name || void 0,
14264
- // Omitted when blank — the server distinguishes "not given" from a value
14265
- // it could not parse, and neither is an empty string.
14266
- date_of_birth: data.dateOfBirth || void 0,
14267
- cart_id: ((_a = data.cart) == null ? void 0 : _a.cart_id) || `web_${Date.now()}`,
14268
- cart_token: (_b = data.cart) == null ? void 0 : _b.cart_id,
14269
- cart_total: ((_c = data.cart) == null ? void 0 : _c.cart_total) || 0,
14270
- cart_currency: ((_d = data.cart) == null ? void 0 : _d.cart_currency) || "USD",
14271
- cart_items: ((_e = data.cart) == null ? void 0 : _e.cart_items) || [],
14272
- // Redacted: a stored cart URL is re-sent later as a recovery link, and a
14273
- // credential in it (`?t=` / `?k=`) must not outlive this page view.
14274
- cart_url: redactUrl(window.location.href),
14275
- platform: "web",
14276
- geo_region: geoRegion,
14277
- device_type: deviceType,
14278
- session_id: this.getSessionId() || void 0,
14279
- anonymous_id: this.getAnonymousId() || void 0,
14280
- utm_source: utmParams.utm_source,
14281
- utm_medium: utmParams.utm_medium,
14282
- utm_campaign: utmParams.utm_campaign
14283
- };
14284
- const response = await fetch(url, {
14285
- method: "POST",
14286
- headers,
14287
- body: JSON.stringify(body),
14288
- credentials: "include"
14289
- });
14290
- if (!response.ok) {
14291
- const errorText = await response.text();
14292
- throw new Error(`Failed to submit spin wheel: ${response.status} - ${errorText}`);
14293
- }
14294
- return await response.json();
14295
- }
14296
- showSpinWheelPrize(modal, prize) {
14297
- modal.innerHTML = "";
14298
- const resultContainer = document.createElement("div");
14299
- resultContainer.style.cssText = "text-align: center; padding: 40px 20px;";
14300
- const emoji = document.createElement("div");
14301
- emoji.textContent = "🎉";
14302
- emoji.style.cssText = "font-size: 60px; margin-bottom: 20px;";
14303
- resultContainer.appendChild(emoji);
14304
- const title = document.createElement("h2");
14305
- title.textContent = "Congratulations!";
14306
- title.style.cssText = "margin: 0 0 12px 0; font-size: 28px; font-weight: 700; color: #1a73e8;";
14307
- resultContainer.appendChild(title);
14308
- const prizeLabel = document.createElement("p");
14309
- prizeLabel.textContent = prize.prize_label;
14310
- prizeLabel.style.cssText = "margin: 0 0 20px 0; font-size: 20px; font-weight: 600; color: #333;";
14311
- resultContainer.appendChild(prizeLabel);
14312
- if (prize.coupon_code) {
14313
- const couponContainer = document.createElement("div");
14314
- couponContainer.style.cssText = `
14315
- background: #f5f5f5;
14316
- padding: 16px;
14317
- border-radius: 8px;
14318
- margin-bottom: 20px;
14319
- border: 2px dashed #1a73e8;
14320
- `;
14321
- const couponLabel = document.createElement("div");
14322
- couponLabel.textContent = "Your coupon code:";
14323
- couponLabel.style.cssText = "font-size: 12px; color: #666; margin-bottom: 8px;";
14324
- couponContainer.appendChild(couponLabel);
14325
- const couponCode = document.createElement("div");
14326
- couponCode.textContent = prize.coupon_code;
14327
- couponCode.style.cssText = "font-size: 24px; font-weight: 700; color: #1a73e8; letter-spacing: 2px;";
14328
- couponContainer.appendChild(couponCode);
14329
- resultContainer.appendChild(couponContainer);
14330
- }
14331
- const message = document.createElement("p");
14332
- message.textContent = "Check your WhatsApp/SMS for your discount code and cart link!";
14333
- message.style.cssText = "margin: 0 0 20px 0; font-size: 14px; color: #666;";
14334
- resultContainer.appendChild(message);
14335
- const closeButton = document.createElement("button");
14336
- closeButton.textContent = "Close";
14337
- closeButton.style.cssText = `
14338
- background: #1a73e8;
14339
- color: white;
14340
- border: none;
14341
- padding: 12px 32px;
14342
- border-radius: 8px;
14343
- font-weight: 600;
14344
- font-size: 14px;
14345
- cursor: pointer;
14346
- `;
14347
- closeButton.addEventListener("click", () => {
14348
- const overlay = modal.parentElement;
14349
- if (overlay && document.body.contains(overlay)) {
14350
- document.body.removeChild(overlay);
14351
- }
14352
- });
14353
- resultContainer.appendChild(closeButton);
14354
- modal.appendChild(resultContainer);
14355
- }
14356
- detectGeoRegion() {
14357
- const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
14358
- if (timezone.includes("America")) return "north_america";
14359
- if (timezone.includes("Europe")) return "europe";
14360
- if (timezone.includes("Asia/Kolkata") || timezone.includes("Asia/Calcutta")) return "india";
14361
- if (timezone.includes("Asia/Singapore") || timezone.includes("Asia/Bangkok") || timezone.includes("Asia/Jakarta")) return "southeast_asia";
14362
- if (timezone.includes("Asia/Dubai") || timezone.includes("Asia/Riyadh")) return "middle_east";
14363
- if (timezone.includes("America") && (timezone.includes("Sao_Paulo") || timezone.includes("Buenos_Aires"))) return "latin_america";
14364
- if (timezone.includes("Australia") || timezone.includes("Pacific/Auckland")) return "oceania";
14365
- return "north_america";
14366
- }
14367
13842
  getUTMParams() {
14368
13843
  const params = new URLSearchParams(window.location.search);
14369
13844
  return {
@@ -14372,119 +13847,6 @@ class AegisWidgetManager {
14372
13847
  utm_campaign: params.get("utm_campaign") || void 0
14373
13848
  };
14374
13849
  }
14375
- getSessionId() {
14376
- return sessionStorage.getItem("aegis_session_id");
14377
- }
14378
- getAnonymousId() {
14379
- return localStorage.getItem("aegis_anonymous_id");
14380
- }
14381
- renderScratchCard(widget) {
14382
- const overlay = document.createElement("div");
14383
- overlay.className = "aegis-gamification-overlay";
14384
- overlay.setAttribute("data-widget-id", widget.widget_id);
14385
- overlay.style.cssText = `
14386
- position: fixed;
14387
- top: 0;
14388
- left: 0;
14389
- right: 0;
14390
- bottom: 0;
14391
- background: rgba(0, 0, 0, 0.7);
14392
- z-index: 1000000;
14393
- display: flex;
14394
- align-items: center;
14395
- justify-content: center;
14396
- animation: aegisFadeIn 0.3s ease-out;
14397
- `;
14398
- const modal = document.createElement("div");
14399
- modal.className = "aegis-scratch-card-modal";
14400
- modal.style.cssText = `
14401
- background: white;
14402
- border-radius: 16px;
14403
- padding: 32px;
14404
- max-width: 500px;
14405
- width: 90%;
14406
- box-shadow: 0 10px 40px rgba(0,0,0,0.3);
14407
- text-align: center;
14408
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
14409
- animation: aegisScaleIn 0.3s ease-out;
14410
- `;
14411
- const title = document.createElement("h2");
14412
- title.textContent = widget.config.title || "Scratch & Win!";
14413
- title.style.cssText = "margin: 0 0 16px 0; font-size: 24px; font-weight: 700; color: #1a1a1a;";
14414
- modal.appendChild(title);
14415
- const description = document.createElement("p");
14416
- description.textContent = widget.config.description || "Scratch to reveal your prize!";
14417
- description.style.cssText = "margin: 0 0 24px 0; font-size: 14px; color: #666;";
14418
- modal.appendChild(description);
14419
- const canvas = document.createElement("canvas");
14420
- canvas.width = 300;
14421
- canvas.height = 200;
14422
- canvas.style.cssText = `
14423
- border-radius: 8px;
14424
- cursor: pointer;
14425
- margin: 0 auto 24px;
14426
- display: block;
14427
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
14428
- `;
14429
- modal.appendChild(canvas);
14430
- const ctx = canvas.getContext("2d");
14431
- if (ctx) {
14432
- ctx.fillStyle = "#c0c0c0";
14433
- ctx.fillRect(0, 0, canvas.width, canvas.height);
14434
- ctx.fillStyle = "#888";
14435
- ctx.font = "20px Arial";
14436
- ctx.textAlign = "center";
14437
- ctx.fillText("Scratch here!", canvas.width / 2, canvas.height / 2);
14438
- let isScratching = false;
14439
- const scratch = (x, y) => {
14440
- ctx.globalCompositeOperation = "destination-out";
14441
- ctx.beginPath();
14442
- ctx.arc(x, y, 20, 0, Math.PI * 2);
14443
- ctx.fill();
14444
- };
14445
- canvas.addEventListener("mousedown", () => {
14446
- isScratching = true;
14447
- });
14448
- canvas.addEventListener("mouseup", () => {
14449
- isScratching = false;
14450
- });
14451
- canvas.addEventListener("mousemove", (e) => {
14452
- if (isScratching) {
14453
- const rect = canvas.getBoundingClientRect();
14454
- scratch(e.clientX - rect.left, e.clientY - rect.top);
14455
- }
14456
- });
14457
- }
14458
- const scratchButton = document.createElement("button");
14459
- scratchButton.textContent = "REVEAL PRIZE";
14460
- scratchButton.style.cssText = `
14461
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
14462
- color: white;
14463
- border: none;
14464
- padding: 16px 48px;
14465
- border-radius: 8px;
14466
- font-weight: 700;
14467
- font-size: 16px;
14468
- cursor: pointer;
14469
- `;
14470
- scratchButton.addEventListener("click", async () => {
14471
- scratchButton.disabled = true;
14472
- scratchButton.textContent = "REVEALING...";
14473
- try {
14474
- const prize = await this.generatePrize(widget.config.config_id);
14475
- this.showPrizeResult(modal, prize);
14476
- } catch (error) {
14477
- this.log(`Error generating prize: ${error}`, true);
14478
- scratchButton.disabled = false;
14479
- scratchButton.textContent = "TRY AGAIN";
14480
- }
14481
- });
14482
- modal.appendChild(scratchButton);
14483
- overlay.appendChild(modal);
14484
- this.addAnimationStyles();
14485
- overlay.setAttribute("data-aegis-widget-root", "");
14486
- document.body.appendChild(overlay);
14487
- }
14488
13850
  renderToast(widget) {
14489
13851
  const { message, icon, duration } = widget.config;
14490
13852
  const position = widget.position || "bottom_left";
@@ -14757,83 +14119,6 @@ class AegisWidgetManager {
14757
14119
  overlay.setAttribute("data-aegis-widget-root", "");
14758
14120
  document.body.appendChild(overlay);
14759
14121
  }
14760
- async generatePrize(configId) {
14761
- const url = `${this.apiHost}/v1/widgets/gamification/generate-prize`;
14762
- const headers = {
14763
- "X-Aegis-Write-Key": this.writeKey,
14764
- "Content-Type": "application/json"
14765
- };
14766
- if (this.userId) headers["X-User-ID"] = this.userId;
14767
- if (this.contactId) headers["X-Contact-ID"] = this.contactId;
14768
- if (this.contactId && this.contactToken) headers["X-Contact-Token"] = this.contactToken;
14769
- if (this.organizationId) headers["X-Organization-ID"] = this.organizationId;
14770
- const response = await fetch(url, {
14771
- method: "POST",
14772
- headers,
14773
- body: JSON.stringify({ config_id: configId }),
14774
- credentials: "include"
14775
- });
14776
- if (!response.ok) {
14777
- throw new Error(`Failed to generate prize: ${response.status}`);
14778
- }
14779
- return await response.json();
14780
- }
14781
- showPrizeResult(modal, prize) {
14782
- modal.innerHTML = "";
14783
- const resultContainer = document.createElement("div");
14784
- resultContainer.style.cssText = "text-align: center; padding: 40px 20px;";
14785
- const emoji = document.createElement("div");
14786
- emoji.textContent = "🎉";
14787
- emoji.style.cssText = "font-size: 60px; margin-bottom: 20px;";
14788
- resultContainer.appendChild(emoji);
14789
- const title = document.createElement("h2");
14790
- title.textContent = "Congratulations!";
14791
- title.style.cssText = "margin: 0 0 12px 0; font-size: 28px; font-weight: 700; color: #1a73e8;";
14792
- resultContainer.appendChild(title);
14793
- const prizeLabel = document.createElement("p");
14794
- prizeLabel.textContent = prize.prize_label;
14795
- prizeLabel.style.cssText = "margin: 0 0 20px 0; font-size: 20px; font-weight: 600; color: #333;";
14796
- resultContainer.appendChild(prizeLabel);
14797
- if (prize.coupon_code) {
14798
- const couponContainer = document.createElement("div");
14799
- couponContainer.style.cssText = `
14800
- background: #f5f5f5;
14801
- padding: 16px;
14802
- border-radius: 8px;
14803
- margin-bottom: 20px;
14804
- border: 2px dashed #1a73e8;
14805
- `;
14806
- const couponLabel = document.createElement("div");
14807
- couponLabel.textContent = "Your coupon code:";
14808
- couponLabel.style.cssText = "font-size: 12px; color: #666; margin-bottom: 8px;";
14809
- couponContainer.appendChild(couponLabel);
14810
- const couponCode = document.createElement("div");
14811
- couponCode.textContent = prize.coupon_code;
14812
- couponCode.style.cssText = "font-size: 24px; font-weight: 700; color: #1a73e8; letter-spacing: 2px;";
14813
- couponContainer.appendChild(couponCode);
14814
- resultContainer.appendChild(couponContainer);
14815
- }
14816
- const closeButton = document.createElement("button");
14817
- closeButton.textContent = "Close";
14818
- closeButton.style.cssText = `
14819
- background: #1a73e8;
14820
- color: white;
14821
- border: none;
14822
- padding: 12px 32px;
14823
- border-radius: 8px;
14824
- font-weight: 600;
14825
- font-size: 14px;
14826
- cursor: pointer;
14827
- `;
14828
- closeButton.addEventListener("click", () => {
14829
- const overlay = modal.parentElement;
14830
- if (overlay && document.body.contains(overlay)) {
14831
- document.body.removeChild(overlay);
14832
- }
14833
- });
14834
- resultContainer.appendChild(closeButton);
14835
- modal.appendChild(resultContainer);
14836
- }
14837
14122
  async submitFeedback(widgetId, data) {
14838
14123
  var _a;
14839
14124
  const url = `${this.apiHost}/v1/widgets/track-event`;
@@ -15232,6 +14517,152 @@ class AegisWidgetManager {
15232
14517
  }
15233
14518
  }
15234
14519
  }
14520
+ const MICRO_INTENT_TRIGGER_TYPE = "micro_intent";
14521
+ const THRESHOLD_SIGNALS = {
14522
+ time_on_page: DEFAULT_TIME_ON_PAGE_SECONDS,
14523
+ scroll_depth: DEFAULT_SCROLL_DEPTH_PERCENT,
14524
+ inactivity: DEFAULT_IDLE_SECONDS
14525
+ };
14526
+ const THRESHOLD_EVENT = /^(time_on_page|scroll_depth|inactivity)_(\d+(?:\.\d+)?)$/;
14527
+ function microIntentRule(trigger) {
14528
+ if (!trigger || trigger.type !== MICRO_INTENT_TRIGGER_TYPE) return null;
14529
+ const cfg = trigger.config || {};
14530
+ const fireOn = cfg.fire_on;
14531
+ const when = cfg.when;
14532
+ if (!Array.isArray(fireOn) || fireOn.length === 0) return null;
14533
+ if (!fireOn.every((s) => typeof s === "string")) return null;
14534
+ if (!when || typeof when !== "object") return null;
14535
+ const priority = Number(cfg.priority);
14536
+ return {
14537
+ fire_on: fireOn,
14538
+ when,
14539
+ priority: Number.isFinite(priority) ? priority : 0,
14540
+ suppress_competing: cfg.suppress_competing === true
14541
+ };
14542
+ }
14543
+ function armedMicroIntentCampaigns(campaigns) {
14544
+ const armed = [];
14545
+ for (const c of campaigns) {
14546
+ const rule = microIntentRule(c.client_trigger);
14547
+ if (rule) armed.push({ id: c.id, rule });
14548
+ }
14549
+ return armed;
14550
+ }
14551
+ function signalThresholds(rules, signal) {
14552
+ const out = /* @__PURE__ */ new Set();
14553
+ const visit = (expr) => {
14554
+ if ("operands" in expr) {
14555
+ expr.operands.forEach(visit);
14556
+ return;
14557
+ }
14558
+ if (expr.signal !== signal || typeof expr.value !== "number") return;
14559
+ const crossing = expr.op === "gt" || expr.op === "lte" ? Math.floor(expr.value) + 1 : Math.ceil(expr.value);
14560
+ if (crossing > 0) out.add(crossing);
14561
+ };
14562
+ for (const rule of rules) {
14563
+ visit(rule.when);
14564
+ if (rule.fire_on.includes(signal) && !hasLeaf(rule.when, signal)) out.add(THRESHOLD_SIGNALS[signal]);
14565
+ }
14566
+ return [...out].sort((a, b) => a - b);
14567
+ }
14568
+ function hasLeaf(expr, signal) {
14569
+ if ("operands" in expr) return expr.operands.some((o) => hasLeaf(o, signal));
14570
+ return expr.signal === signal;
14571
+ }
14572
+ class MicroIntentArming {
14573
+ constructor(cfg) {
14574
+ this.registered = /* @__PURE__ */ new Set();
14575
+ this.listening = false;
14576
+ this.onEngineEvent = (event) => this.handle(event.type);
14577
+ this.cfg = cfg;
14578
+ }
14579
+ /** Replace the armed rules with the smart triggers among `campaigns`. */
14580
+ arm(campaigns) {
14581
+ const armed = armedMicroIntentCampaigns(campaigns);
14582
+ this.cfg.evaluator.setArmed(armed);
14583
+ if (armed.length === 0) return;
14584
+ this.listen();
14585
+ const rules = armed.map((a) => a.rule);
14586
+ const { engine } = this.cfg;
14587
+ for (const signal of Object.keys(THRESHOLD_SIGNALS)) {
14588
+ for (const n of signalThresholds(rules, signal)) {
14589
+ if (!this.once(`${signal}_${n}`)) continue;
14590
+ if (signal === "time_on_page") engine.registerTimeOnPage(n);
14591
+ else if (signal === "scroll_depth") engine.registerScrollDepth(n);
14592
+ else engine.registerInactivity(n);
14593
+ }
14594
+ }
14595
+ if (rules.some((r) => r.fire_on.includes("exit_intent")) && this.once("exit_intent")) {
14596
+ if (this.cfg.isMobile) engine.registerScrollVelocity();
14597
+ else engine.registerExitIntent();
14598
+ }
14599
+ if (rules.some((r) => r.fire_on.includes("back_button")) && this.once("back_button")) {
14600
+ engine.registerBackButton();
14601
+ }
14602
+ }
14603
+ /** The host reported a conversion: rules gated on NOT converted stop matching. */
14604
+ noteConversion() {
14605
+ this.cfg.evaluator.updateSignal("converted_in_session", true);
14606
+ }
14607
+ destroy() {
14608
+ if (this.listening) this.cfg.engine.off("*", this.onEngineEvent);
14609
+ this.listening = false;
14610
+ }
14611
+ once(key) {
14612
+ if (this.registered.has(key)) return false;
14613
+ this.registered.add(key);
14614
+ return true;
14615
+ }
14616
+ /** Subscribe once, and seed the page-start values: nothing scrolled, no
14617
+ * time elapsed, no exit yet, conversion as the manager recorded it. A null
14618
+ * here would make every AND/NOT over these signals unprovable. */
14619
+ listen() {
14620
+ if (this.listening) return;
14621
+ this.listening = true;
14622
+ const { evaluator } = this.cfg;
14623
+ const seen = evaluator.getSnapshot();
14624
+ const seed = {
14625
+ time_on_page: 0,
14626
+ scroll_depth: 0,
14627
+ inactivity: 0,
14628
+ exit_intent: false,
14629
+ back_button: false,
14630
+ converted_in_session: this.cfg.hasConverted()
14631
+ };
14632
+ for (const [signal, value] of Object.entries(seed)) {
14633
+ if (seen[signal] === void 0 || seen[signal] === null) evaluator.updateSignal(signal, value);
14634
+ }
14635
+ this.cfg.engine.on("*", this.onEngineEvent);
14636
+ }
14637
+ handle(eventType) {
14638
+ const { evaluator } = this.cfg;
14639
+ const threshold = THRESHOLD_EVENT.exec(eventType);
14640
+ if (threshold) {
14641
+ const signal = threshold[1];
14642
+ const n = Number(threshold[2]);
14643
+ const prev = evaluator.getSnapshot()[signal];
14644
+ const value = signal !== "inactivity" && typeof prev === "number" ? Math.max(prev, n) : n;
14645
+ evaluator.updateSignal(signal, value);
14646
+ this.decide(signal);
14647
+ return;
14648
+ }
14649
+ if (eventType === "exit_intent" || eventType === "mobile_exit_intent" && this.cfg.isMobile) {
14650
+ evaluator.updateSignal("exit_intent", true);
14651
+ this.decide("exit_intent");
14652
+ return;
14653
+ }
14654
+ if (eventType === "back_button") {
14655
+ evaluator.updateSignal("back_button", true);
14656
+ this.decide("back_button");
14657
+ }
14658
+ }
14659
+ decide(signal) {
14660
+ const decision = this.cfg.evaluator.onSignalChanged(signal);
14661
+ if (decision.kind === "fire" && decision.campaignId && this.cfg.show(decision.campaignId)) {
14662
+ this.cfg.evaluator.markFired(decision.campaignId);
14663
+ }
14664
+ }
14665
+ }
15235
14666
  class HttpClient {
15236
14667
  constructor(config) {
15237
14668
  this.config = config;
@@ -15389,8 +14820,9 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15389
14820
  constructor(config) {
15390
14821
  var _a;
15391
14822
  this.initialized = false;
14823
+ this.armedInAppTriggers = /* @__PURE__ */ new Set();
15392
14824
  {
15393
- const v = "1.40.0";
14825
+ const v = "1.42.0";
15394
14826
  try {
15395
14827
  globalThis.__AEGIS_SDK_VERSION__ = v;
15396
14828
  } catch {
@@ -15409,6 +14841,8 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15409
14841
  }
15410
14842
  const triggerEngine = config.triggerEngine ?? new TriggerEngine();
15411
14843
  const ownsTriggerEngine = !config.triggerEngine;
14844
+ this.triggerEngine = triggerEngine;
14845
+ this.ownsTriggerEngine = ownsTriggerEngine;
15412
14846
  this.contactId = config.contactId;
15413
14847
  this.contactToken = config.contactToken;
15414
14848
  this.pairingApiHost = config.apiHost ?? "";
@@ -15445,12 +14879,6 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15445
14879
  propertyId: config.propertyId,
15446
14880
  debugMode: config.debugMode,
15447
14881
  enableSSE: config.enableSSE,
15448
- // Route gamification sub_types straight to the widget renderer —
15449
- // replaces the old CustomEvent('aegis:render-widget') bridge which
15450
- // had no listener and silently dropped impressions.
15451
- onInteractiveCampaign: (campaign) => {
15452
- this.widgets.renderInteractiveCampaign(campaign);
15453
- },
15454
14882
  getWorkspaceId: config.getWorkspaceId,
15455
14883
  // First-party cart reader — renderers (progress-bar) read live cart
15456
14884
  // state from here instead of `window.*` cart globals. See setCartState().
@@ -15489,13 +14917,44 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15489
14917
  debugMode: config.debugMode,
15490
14918
  ...config.chat
15491
14919
  });
15492
- const proactive = ((_a = config.chat) == null ? void 0 : _a.proactiveTriggers) ?? [];
15493
- for (const evt of proactive) {
15494
- triggerEngine.on(evt, () => {
14920
+ const proactive = (((_a = config.chat) == null ? void 0 : _a.proactiveTriggers) ?? []).map(triggerFromEngineEvent);
14921
+ armBehaviouralTriggers(
14922
+ triggerEngine,
14923
+ proactive,
14924
+ /* @__PURE__ */ new Set(),
14925
+ () => {
15495
14926
  var _a2;
15496
14927
  return (_a2 = this.chat) == null ? void 0 : _a2.openPanel();
15497
- });
15498
- }
14928
+ },
14929
+ _AegisMessageRuntime.hasNoHover()
14930
+ );
14931
+ }
14932
+ this.microIntent = new MicroIntentArming({
14933
+ engine: triggerEngine,
14934
+ evaluator: this.intentRuleEvaluator,
14935
+ show: (campaignId) => this.inApp.showTriggeredCampaign(campaignId),
14936
+ hasConverted: () => this.inApp.hasConvertedThisSession(),
14937
+ isMobile: _AegisMessageRuntime.hasNoHover()
14938
+ });
14939
+ this.unsubscribeCampaignsLoaded = this.inApp.onCampaignsLoaded((campaigns) => {
14940
+ armBehaviouralTriggers(
14941
+ triggerEngine,
14942
+ campaigns.map((c) => c.client_trigger),
14943
+ this.armedInAppTriggers,
14944
+ (eventName) => this.inApp.onClientEvent(eventName),
14945
+ _AegisMessageRuntime.hasNoHover()
14946
+ );
14947
+ this.microIntent.arm(campaigns);
14948
+ });
14949
+ }
14950
+ /** No hovering pointer (phones, tablets): exit intent is a fast scroll up,
14951
+ * not the mouse leaving the window. */
14952
+ static hasNoHover() {
14953
+ var _a, _b;
14954
+ try {
14955
+ return typeof window !== "undefined" && ((_b = (_a = window.matchMedia) == null ? void 0 : _a.call(window, "(hover: none)")) == null ? void 0 : _b.matches) === true;
14956
+ } catch {
14957
+ return false;
15499
14958
  }
15500
14959
  }
15501
14960
  /**
@@ -15512,6 +14971,9 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15512
14971
  this.widgets.initialize()
15513
14972
  ]);
15514
14973
  (_a = this.chat) == null ? void 0 : _a.initialize();
14974
+ if (this.ownsTriggerEngine) {
14975
+ this.triggerEngine.start();
14976
+ }
15515
14977
  if (this.contactId) {
15516
14978
  void this.contactScores.fetchForContact(this.contactId, { contactToken: this.contactToken });
15517
14979
  }
@@ -15712,6 +15174,7 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15712
15174
  notifyConversion(goalName) {
15713
15175
  var _a, _b;
15714
15176
  (_b = (_a = this.inApp).notifyConversion) == null ? void 0 : _b.call(_a, goalName);
15177
+ if (goalName) this.microIntent.noteConversion();
15715
15178
  }
15716
15179
  /**
15717
15180
  * Tear down both managers. Used by React component unmounts + during
@@ -15721,6 +15184,8 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15721
15184
  */
15722
15185
  destroy() {
15723
15186
  var _a, _b, _c;
15187
+ this.unsubscribeCampaignsLoaded();
15188
+ this.microIntent.destroy();
15724
15189
  (_b = (_a = this.inApp).destroy) == null ? void 0 : _b.call(_a);
15725
15190
  this.widgets.destroy();
15726
15191
  (_c = this.chat) == null ? void 0 : _c.destroy();
@@ -15729,8 +15194,8 @@ const _AegisMessageRuntime = class _AegisMessageRuntime {
15729
15194
  /**
15730
15195
  * Current armed campaigns visible to the InAppManager. Accessible for
15731
15196
  * the Prefetch Inspector and for debugging. WidgetManager's prefetched
15732
- * spin_wheel / scratch_card configs are NOT in this list — they live
15733
- * in `this.widgets` and have their own lifecycle.
15197
+ * exit-intent popup config is NOT in this list — it lives in
15198
+ * `this.widgets` and has its own lifecycle.
15734
15199
  */
15735
15200
  getCampaigns() {
15736
15201
  return this.inApp.campaigns ?? [];