@magicfeedback/popup-sdk 1.0.11 → 1.0.12

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
@@ -1721,10 +1721,18 @@ var require_magicfeedback_sdk_browser = __commonJS({
1721
1721
  // src/index.ts
1722
1722
  var index_exports = {};
1723
1723
  __export(index_exports, {
1724
- DeepdotsPopups: () => DeepdotsPopups
1724
+ DeepdotsPopups: () => DeepdotsPopups,
1725
+ POPUP_TRIGGER_CONDITION_STATUSES: () => POPUP_TRIGGER_CONDITION_STATUSES
1725
1726
  });
1726
1727
  module.exports = __toCommonJS(index_exports);
1727
1728
 
1729
+ // src/types/index.ts
1730
+ var POPUP_TRIGGER_CONDITION_STATUSES = [
1731
+ "SHOWED",
1732
+ "PARTIAL",
1733
+ "COMPLETED"
1734
+ ];
1735
+
1728
1736
  // src/ui/renderPopup.ts
1729
1737
  var import_native = __toESM(require_magicfeedback_sdk_browser());
1730
1738
  function ensureMagicFeedbackStyles(popup) {
@@ -2186,6 +2194,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
2186
2194
  updateButtons("completed");
2187
2195
  return;
2188
2196
  }
2197
+ emit("popup_clicked", surveyId, { action: "partial" });
2189
2198
  if (total > 1 && progress > 0 && progress < total) {
2190
2199
  updateButtons("in_progress_next");
2191
2200
  } else {
@@ -2222,6 +2231,9 @@ function setupTrigger(popups, trigger) {
2222
2231
  case "exit":
2223
2232
  setupExitTrigger(popups, trigger);
2224
2233
  break;
2234
+ case "event":
2235
+ setupEventTrigger(popups, trigger);
2236
+ break;
2225
2237
  case "click":
2226
2238
  setupClickTrigger(popups, trigger);
2227
2239
  break;
@@ -2230,7 +2242,7 @@ function setupTrigger(popups, trigger) {
2230
2242
  function setupTimeTrigger(popups, trigger) {
2231
2243
  const delay = trigger.value || 5e3;
2232
2244
  setTimeout(() => {
2233
- popups.triggerSurvey(trigger.surveyId);
2245
+ popups.triggerSurvey(trigger.surveyId, trigger.popupId);
2234
2246
  }, delay);
2235
2247
  popups.debug(`Time trigger set for ${delay}ms`);
2236
2248
  }
@@ -2239,7 +2251,7 @@ function setupScrollTrigger(popups, trigger) {
2239
2251
  const handler = () => {
2240
2252
  const scrollPercentage = window.scrollY / (document.documentElement.scrollHeight - window.innerHeight) * 100;
2241
2253
  if (scrollPercentage >= threshold) {
2242
- popups.triggerSurvey(trigger.surveyId);
2254
+ popups.triggerSurvey(trigger.surveyId, trigger.popupId);
2243
2255
  window.removeEventListener("scroll", handler);
2244
2256
  }
2245
2257
  };
@@ -2247,86 +2259,103 @@ function setupScrollTrigger(popups, trigger) {
2247
2259
  popups.debug(`Scroll trigger set for ${threshold}%`);
2248
2260
  }
2249
2261
  function setupExitTrigger(popups, trigger) {
2250
- let fired = false;
2251
- let lastUrl = typeof window !== "undefined" ? window.location.href : "";
2252
- const historyRef = typeof window !== "undefined" ? window.history : null;
2253
- const originalPushState = historyRef ? historyRef.pushState.bind(historyRef) : null;
2254
- const originalReplaceState = historyRef ? historyRef.replaceState.bind(historyRef) : null;
2255
- const cleanup = () => {
2256
- document.removeEventListener("click", handleNavClick, true);
2257
- window.removeEventListener("popstate", handlePopState);
2258
- if (historyRef && originalPushState) historyRef.pushState = originalPushState;
2259
- if (historyRef && originalReplaceState) historyRef.replaceState = originalReplaceState;
2262
+ if (typeof window === "undefined") return;
2263
+ const delaySeconds = parseExitDelaySeconds(trigger.value);
2264
+ const historyRef = window.history;
2265
+ const originalPushState = historyRef.pushState.bind(historyRef);
2266
+ const originalReplaceState = historyRef.replaceState.bind(historyRef);
2267
+ let lastUrl = normalizeUrl(window.location.href);
2268
+ let lastQueuedKey = "";
2269
+ let lastQueuedAt = 0;
2270
+ const queueLeave = (fromRaw, toRaw) => {
2271
+ const from = normalizeUrl(fromRaw);
2272
+ const to = normalizeUrl(toRaw);
2273
+ if (!from || !to || from === to) return;
2274
+ const key = `${from}=>${to}`;
2275
+ const now = Date.now();
2276
+ if (key === lastQueuedKey && now - lastQueuedAt < 150) return;
2277
+ lastQueuedKey = key;
2278
+ lastQueuedAt = now;
2279
+ popups.queueExitPopup(trigger.surveyId, delaySeconds, from, trigger.popupId);
2260
2280
  };
2261
- const fireOnce = () => {
2262
- if (fired) return;
2263
- fired = true;
2264
- popups.triggerSurvey(trigger.surveyId);
2265
- cleanup();
2281
+ const handleNavigation = (to) => {
2282
+ const from = lastUrl;
2283
+ queueLeave(from, to);
2284
+ lastUrl = normalizeUrl(to);
2266
2285
  };
2267
- const handleNavClick = (e) => {
2268
- if (fired) return;
2269
- if (e.defaultPrevented) return;
2270
- if (e.button !== 0) return;
2271
- if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
2272
- const target = e.target?.closest?.("a");
2273
- if (!target) return;
2274
- const href = target.getAttribute("href");
2275
- if (!href || href.startsWith("#")) return;
2276
- const targetAttr = target.getAttribute("target");
2277
- if (targetAttr && targetAttr !== "_self") return;
2278
- let url;
2279
- try {
2280
- url = new URL(href, window.location.href);
2281
- } catch {
2286
+ const handleDocumentClick = (event) => {
2287
+ if (event.defaultPrevented) return;
2288
+ if (event.button !== 0) return;
2289
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
2290
+ const anchor = event.target?.closest?.("a");
2291
+ if (!(anchor instanceof HTMLAnchorElement)) return;
2292
+ const href = anchor.getAttribute("href");
2293
+ if (!href || href.startsWith("javascript:") || href.startsWith("mailto:") || href.startsWith("tel:")) {
2282
2294
  return;
2283
2295
  }
2284
- if (url.origin !== window.location.origin) return;
2285
- e.preventDefault();
2286
- fireOnce();
2287
- };
2288
- const shouldBlockUrl = (rawUrl) => {
2289
- if (!rawUrl) return false;
2290
- let url;
2296
+ const target = anchor.getAttribute("target");
2297
+ if (target && target !== "_self") return;
2298
+ let destination;
2291
2299
  try {
2292
- url = rawUrl instanceof URL ? rawUrl : new URL(rawUrl, window.location.href);
2300
+ destination = new URL(href, window.location.href);
2293
2301
  } catch {
2294
- return false;
2302
+ return;
2295
2303
  }
2296
- if (url.origin !== window.location.origin) return false;
2297
- if (url.href === window.location.href) return false;
2298
- return true;
2304
+ if (destination.origin !== window.location.origin) return;
2305
+ queueLeave(window.location.href, destination.href);
2306
+ };
2307
+ const handleHashChange = (event) => {
2308
+ const to = event.newURL || window.location.href;
2309
+ const from = event.oldURL || lastUrl;
2310
+ queueLeave(from, to);
2311
+ lastUrl = normalizeUrl(to);
2299
2312
  };
2300
2313
  const handlePopState = () => {
2301
- if (fired) return;
2302
- fireOnce();
2303
- if (historyRef && originalPushState && lastUrl) {
2304
- originalPushState(null, document.title, lastUrl);
2305
- }
2314
+ handleNavigation(window.location.href);
2306
2315
  };
2307
- if (historyRef && originalPushState && originalReplaceState) {
2308
- historyRef.pushState = function(data, unused, url) {
2309
- if (!fired && shouldBlockUrl(url)) {
2310
- fireOnce();
2311
- return;
2312
- }
2313
- const result = originalPushState(data, unused, url);
2314
- lastUrl = window.location.href;
2315
- return result;
2316
- };
2317
- historyRef.replaceState = function(data, unused, url) {
2318
- if (!fired && shouldBlockUrl(url)) {
2319
- fireOnce();
2320
- return;
2321
- }
2322
- const result = originalReplaceState(data, unused, url);
2323
- lastUrl = window.location.href;
2324
- return result;
2325
- };
2326
- }
2327
- document.addEventListener("click", handleNavClick, true);
2316
+ historyRef.pushState = function(data, unused, url) {
2317
+ const from = window.location.href;
2318
+ const result = originalPushState(data, unused, url);
2319
+ queueLeave(from, window.location.href);
2320
+ lastUrl = normalizeUrl(window.location.href);
2321
+ return result;
2322
+ };
2323
+ historyRef.replaceState = function(data, unused, url) {
2324
+ const from = window.location.href;
2325
+ const result = originalReplaceState(data, unused, url);
2326
+ queueLeave(from, window.location.href);
2327
+ lastUrl = normalizeUrl(window.location.href);
2328
+ return result;
2329
+ };
2330
+ document.addEventListener("click", handleDocumentClick, true);
2331
+ window.addEventListener("hashchange", handleHashChange);
2328
2332
  window.addEventListener("popstate", handlePopState);
2329
- popups.debug("Exit intent trigger set (navigation/spa)");
2333
+ popups.debug("Exit trigger set (deferred to next route)", {
2334
+ surveyId: trigger.surveyId,
2335
+ delaySeconds
2336
+ });
2337
+ }
2338
+ function setupEventTrigger(popups, trigger) {
2339
+ popups.debug("Event trigger registered", {
2340
+ popupId: trigger.popupId,
2341
+ surveyId: trigger.surveyId,
2342
+ eventName: String(trigger.value || "")
2343
+ });
2344
+ }
2345
+ function parseExitDelaySeconds(value) {
2346
+ if (typeof value === "number" && Number.isFinite(value)) {
2347
+ return Math.max(0, value);
2348
+ }
2349
+ if (typeof value === "string" && value.trim()) {
2350
+ const parsed = Number(value);
2351
+ if (Number.isFinite(parsed)) return Math.max(0, parsed);
2352
+ }
2353
+ return 0;
2354
+ }
2355
+ function normalizeUrl(value) {
2356
+ if (!value) return "";
2357
+ const withoutIndex = value.replace(/\/index\.html(?=($|[?#]))/i, "");
2358
+ return withoutIndex.length > 1 && withoutIndex.endsWith("/") ? withoutIndex.slice(0, -1) : withoutIndex;
2330
2359
  }
2331
2360
  function setupClickTrigger(popups, trigger) {
2332
2361
  const targetId = typeof trigger.value === "string" ? trigger.value : "";
@@ -2342,7 +2371,7 @@ function setupClickTrigger(popups, trigger) {
2342
2371
  }
2343
2372
  const handler = (e) => {
2344
2373
  if (e.defaultPrevented) return;
2345
- popups.triggerSurvey(trigger.surveyId);
2374
+ popups.triggerSurvey(trigger.surveyId, trigger.popupId);
2346
2375
  el.removeEventListener("click", handler);
2347
2376
  };
2348
2377
  el.addEventListener("click", handler);
@@ -2464,6 +2493,8 @@ function createDefaultRenderer() {
2464
2493
  }
2465
2494
 
2466
2495
  // src/core/deepdots-popups.ts
2496
+ var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
2497
+ var DAY_IN_MS = 24 * 60 * 60 * 1e3;
2467
2498
  var DeepdotsPopups = class {
2468
2499
  constructor() {
2469
2500
  this.config = null;
@@ -2477,10 +2508,12 @@ var DeepdotsPopups = class {
2477
2508
  this.popupsLoaded = false;
2478
2509
  this.pendingAutoLaunch = false;
2479
2510
  this.answeredSurveys = /* @__PURE__ */ new Set();
2511
+ this.surveyProgress = /* @__PURE__ */ new Map();
2480
2512
  this.lastShown = /* @__PURE__ */ new Map();
2481
2513
  // popupId -> timestamp mostrado
2482
2514
  this.surveyToPopupId = /* @__PURE__ */ new Map();
2483
2515
  // surveyId -> popupId
2516
+ this.deferredExitTimers = [];
2484
2517
  this.baseUrl = "";
2485
2518
  this.env = "production";
2486
2519
  }
@@ -2505,15 +2538,17 @@ var DeepdotsPopups = class {
2505
2538
  if (this.renderer.init) this.renderer.init();
2506
2539
  this.setupPopupContainer();
2507
2540
  if (this.config.mode === "client") {
2508
- this.popupDefinitions = this.config.popups || [];
2541
+ this.popupDefinitions = this.validatePopupDefinitions(this.config.popups || []);
2509
2542
  this.popupsLoaded = true;
2510
2543
  this.configureTriggersFromDefinitions();
2544
+ this.processDeferredExitQueue();
2511
2545
  } else {
2512
2546
  this.fetchPopupsFromServer().then((defs) => {
2513
- this.popupDefinitions = defs;
2547
+ this.popupDefinitions = this.validatePopupDefinitions(defs);
2514
2548
  this.popupsLoaded = true;
2515
- this.log("Popups loaded from server (fake)", defs);
2549
+ this.log("Popups loaded from server (fake)", this.popupDefinitions);
2516
2550
  this.configureTriggersFromDefinitions();
2551
+ this.processDeferredExitQueue();
2517
2552
  if (this.pendingAutoLaunch) {
2518
2553
  this.startTriggers();
2519
2554
  }
@@ -2539,7 +2574,6 @@ var DeepdotsPopups = class {
2539
2574
  }
2540
2575
  /** Show a popup immediately (supports legacy ShowOptions and new PopupDefinition) */
2541
2576
  show(options) {
2542
- console.log("Show popup called with options:", options);
2543
2577
  if (!this.initialized) {
2544
2578
  throw new Error("SDK not initialized. Call init() first.");
2545
2579
  }
@@ -2548,6 +2582,7 @@ var DeepdotsPopups = class {
2548
2582
  const def = options;
2549
2583
  this.log("Showing popup (definition)", def);
2550
2584
  this.surveyToPopupId.set(def.surveyId, def.id);
2585
+ this.lastShown.set(def.id, Date.now());
2551
2586
  this.renderPopup(def.surveyId, def.productId, def.actions);
2552
2587
  this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
2553
2588
  } else {
@@ -2559,7 +2594,7 @@ var DeepdotsPopups = class {
2559
2594
  }
2560
2595
  /** Mostrar popup por id de definición (usa surveyId interno) */
2561
2596
  showByPopupId(popupId) {
2562
- const def = this.popupDefinitions.find((p) => p.id === popupId);
2597
+ const def = this.popupDefinitions.find((popup) => popup.id === popupId);
2563
2598
  if (!def) {
2564
2599
  this.log("Popup definition not found", popupId);
2565
2600
  return;
@@ -2577,75 +2612,89 @@ var DeepdotsPopups = class {
2577
2612
  /** Deriva triggers desde las definiciones de popup */
2578
2613
  configureTriggersFromDefinitions() {
2579
2614
  const derived = [];
2580
- const validDefs = this.validatePopupDefinitions(this.popupDefinitions);
2581
- console.log("validDefs", validDefs);
2582
- validDefs.forEach((def) => {
2583
- if (!def.triggers) return;
2584
- const t = def.triggers;
2585
- let type;
2586
- switch (t.type) {
2587
- case "time_on_page":
2588
- type = "time";
2589
- break;
2590
- case "scroll":
2591
- type = "scroll";
2592
- break;
2593
- case "exit":
2594
- type = "exit";
2595
- break;
2596
- case "click":
2597
- type = "click";
2598
- break;
2599
- default:
2600
- this.debug("Unsupported trigger type", t.type);
2615
+ this.debug("Validated popup definitions", this.popupDefinitions);
2616
+ this.popupDefinitions.forEach((def) => {
2617
+ def.triggers.forEach((trigger) => {
2618
+ const type = this.mapPopupTriggerType(trigger.type);
2619
+ if (!type) {
2620
+ this.debug("Unsupported trigger type", trigger.type);
2601
2621
  return;
2602
- }
2603
- const value = type === "time" && typeof t.value === "number" ? t.value * 1e3 : t.value;
2604
- derived.push({ type, value, surveyId: def.surveyId });
2622
+ }
2623
+ const value = type === "time" && typeof trigger.value === "number" ? trigger.value * 1e3 : trigger.value;
2624
+ derived.push({ type, value, surveyId: def.surveyId, popupId: def.id });
2625
+ });
2605
2626
  });
2606
2627
  if (derived.length) {
2607
2628
  this.configureTriggers(derived);
2608
2629
  }
2609
2630
  }
2610
2631
  /** Lógica para evaluar condiciones antes de mostrar una encuesta */
2611
- triggerSurvey(surveyId) {
2612
- const def = this.popupDefinitions.find((p) => p.surveyId === surveyId);
2632
+ triggerSurvey(surveyId, popupId) {
2633
+ const def = this.findPopupDefinition(surveyId, popupId);
2613
2634
  if (!def) {
2614
- this.debug("No popup definition for surveyId", surveyId);
2635
+ this.debug("No popup definition for trigger", { surveyId, popupId });
2615
2636
  return;
2616
2637
  }
2617
2638
  if (!this.shouldShow(def)) {
2618
2639
  this.debug("Conditions prevented showing popup", def.id);
2619
2640
  return;
2620
2641
  }
2642
+ this.showDefinition(def);
2643
+ }
2644
+ /** Trigger popups configured with trigger.type = 'event' and matching trigger.value */
2645
+ triggerEvent(eventName) {
2646
+ if (!this.initialized) {
2647
+ throw new Error("SDK not initialized. Call init() first.");
2648
+ }
2649
+ const normalized = String(eventName || "").trim();
2650
+ if (!normalized) {
2651
+ this.debug("Ignoring empty event trigger name");
2652
+ return;
2653
+ }
2654
+ const candidates = this.popupDefinitions.filter((def) => {
2655
+ return def.triggers.some((trigger) => {
2656
+ const triggerValue = String(trigger.value ?? "").trim();
2657
+ return trigger.type === "event" && triggerValue === normalized;
2658
+ });
2659
+ });
2660
+ if (!candidates.length) {
2661
+ this.debug("No event popup definitions found", { eventName: normalized });
2662
+ return;
2663
+ }
2664
+ const matched = candidates.find((def) => this.shouldShow(def));
2665
+ if (!matched) {
2666
+ this.debug("Event popup blocked by conditions/segments", { eventName: normalized });
2667
+ return;
2668
+ }
2669
+ this.showDefinition(matched);
2670
+ }
2671
+ showDefinition(def) {
2621
2672
  this.show(def);
2622
- this.lastShown.set(def.id, Date.now());
2623
2673
  }
2624
- shouldShow(def) {
2625
- if (!this.matchesSegmentsPath(def)) {
2674
+ shouldShow(def, pathUrl, skipPathCheck = false) {
2675
+ if (!skipPathCheck && !this.matchesSegmentsPath(def, pathUrl)) {
2676
+ return false;
2677
+ }
2678
+ const cooldowns = def.cooldown || [];
2679
+ if (cooldowns.length && !cooldowns.every((cooldown) => this.evaluateCooldown(def, cooldown))) {
2626
2680
  return false;
2627
2681
  }
2628
- const conditions = def.triggers.condition || [];
2629
- if (!conditions.length) return true;
2630
- return conditions.every((c) => this.evaluateCondition(def, c));
2682
+ const legacyConditions = def.legacyConditions || [];
2683
+ if (!legacyConditions.length) return true;
2684
+ return legacyConditions.every((condition) => this.evaluateLegacyCondition(def, condition));
2631
2685
  }
2632
- matchesSegmentsPath(def) {
2686
+ matchesSegmentsPath(def, pathUrl) {
2633
2687
  const paths = def.segments?.path;
2634
2688
  if (!paths || paths.length === 0) return true;
2635
2689
  if (typeof window === "undefined" || !window.location) {
2636
2690
  this.debug("No window.location available for path comparison", { popupId: def.id, paths });
2637
2691
  return true;
2638
2692
  }
2639
- const currentPath = window.location.pathname || "";
2640
- const currentHref = window.location.href || "";
2641
- const normalize = (value) => {
2642
- if (value.length > 1 && value.endsWith("/")) return value.slice(0, -1);
2643
- return value;
2644
- };
2645
- const normalizedPath = normalize(currentPath);
2646
- const normalizedHref = normalize(currentHref);
2693
+ const normalizedHref = this.normalizeUrl(pathUrl || window.location.href || "");
2694
+ const currentUrl = this.safeParseUrl(normalizedHref);
2695
+ const normalizedPath = this.normalizeUrl(currentUrl?.pathname || window.location.pathname || "");
2647
2696
  const matches = paths.some((rawCandidate) => {
2648
- const candidate = normalize(rawCandidate);
2697
+ const candidate = this.normalizeUrl(rawCandidate);
2649
2698
  let match = false;
2650
2699
  if (candidate.startsWith("http://") || candidate.startsWith("https://")) {
2651
2700
  match = normalizedHref === candidate;
@@ -2668,33 +2717,166 @@ var DeepdotsPopups = class {
2668
2717
  }
2669
2718
  return matches;
2670
2719
  }
2671
- evaluateCondition(def, condition) {
2720
+ evaluateCooldown(def, condition) {
2721
+ switch (condition.answered) {
2722
+ case "SHOWED":
2723
+ return this.hasCooldownElapsed(this.lastShown.get(def.id), condition.cooldownDays);
2724
+ case "PARTIAL":
2725
+ case "COMPLETED": {
2726
+ const progress = this.surveyProgress.get(def.surveyId);
2727
+ if (!progress || progress.status !== condition.answered) {
2728
+ return true;
2729
+ }
2730
+ return this.hasCooldownElapsed(progress.timestamp, condition.cooldownDays);
2731
+ }
2732
+ default:
2733
+ return true;
2734
+ }
2735
+ }
2736
+ evaluateLegacyCondition(def, condition) {
2672
2737
  if (!condition.answered && this.answeredSurveys.has(def.surveyId)) {
2673
2738
  return false;
2674
2739
  }
2675
- if (condition.cooldownDays) {
2676
- const last = this.lastShown.get(def.id);
2677
- if (last) {
2678
- const msSince = Date.now() - last;
2679
- const required = condition.cooldownDays * 24 * 60 * 60 * 1e3;
2680
- if (msSince < required) return false;
2681
- }
2740
+ return this.hasCooldownElapsed(this.lastShown.get(def.id), condition.cooldownDays);
2741
+ }
2742
+ hasCooldownElapsed(timestamp, cooldownDays) {
2743
+ if (!timestamp || !cooldownDays) {
2744
+ return true;
2682
2745
  }
2683
- return true;
2746
+ return Date.now() - timestamp >= cooldownDays * DAY_IN_MS;
2684
2747
  }
2685
2748
  /** Marcar encuesta como contestada externamente */
2686
2749
  markSurveyAnswered(surveyId) {
2687
2750
  this.answeredSurveys.add(surveyId);
2751
+ this.markSurveyProgress(surveyId, "COMPLETED");
2752
+ }
2753
+ /** Queue an exit popup so it can render after navigation */
2754
+ queueExitPopup(surveyId, delaySeconds, sourceUrl, popupId) {
2755
+ const def = this.findPopupDefinition(surveyId, popupId);
2756
+ if (!def) {
2757
+ this.debug("No popup definition for exit trigger", { surveyId, popupId });
2758
+ return;
2759
+ }
2760
+ const originUrl = sourceUrl || (typeof window !== "undefined" ? window.location.href : "");
2761
+ if (!originUrl) {
2762
+ this.debug("Exit popup skipped: missing source URL", { surveyId });
2763
+ return;
2764
+ }
2765
+ if (!this.shouldShow(def, originUrl)) {
2766
+ this.debug("Exit popup skipped by conditions/path", { popupId: def.id, sourceUrl: originUrl });
2767
+ return;
2768
+ }
2769
+ const safeDelayMs = Number.isFinite(delaySeconds) ? Math.max(0, delaySeconds * 1e3) : 0;
2770
+ const deferred = {
2771
+ id: def.id,
2772
+ surveyId: def.surveyId,
2773
+ sourceUrl: this.normalizeUrl(originUrl),
2774
+ dueAt: Date.now() + safeDelayMs
2775
+ };
2776
+ const queue = this.getDeferredExitQueue().filter((item) => !(item.id === deferred.id && item.sourceUrl === deferred.sourceUrl));
2777
+ queue.push(deferred);
2778
+ this.setDeferredExitQueue(queue);
2779
+ this.scheduleDeferredExitPopup(deferred);
2780
+ this.debug("Exit popup queued", deferred);
2781
+ }
2782
+ processDeferredExitQueue() {
2783
+ const queue = this.getDeferredExitQueue();
2784
+ if (!queue.length) return;
2785
+ const pending = [];
2786
+ const now = Date.now();
2787
+ queue.forEach((item) => {
2788
+ const def = this.popupDefinitions.find((popup) => popup.id === item.id && popup.surveyId === item.surveyId);
2789
+ if (!def) {
2790
+ return;
2791
+ }
2792
+ if (item.dueAt <= now) {
2793
+ this.tryShowDeferredExitPopup(item);
2794
+ return;
2795
+ }
2796
+ pending.push(item);
2797
+ this.scheduleDeferredExitPopup(item);
2798
+ });
2799
+ this.setDeferredExitQueue(pending);
2800
+ }
2801
+ scheduleDeferredExitPopup(item) {
2802
+ if (typeof window === "undefined") return;
2803
+ const delay = Math.max(0, item.dueAt - Date.now());
2804
+ const timer = window.setTimeout(() => this.tryShowDeferredExitPopup(item), delay);
2805
+ this.deferredExitTimers.push(timer);
2806
+ }
2807
+ tryShowDeferredExitPopup(item) {
2808
+ const def = this.popupDefinitions.find((popup) => popup.id === item.id && popup.surveyId === item.surveyId);
2809
+ if (!def) {
2810
+ this.removeDeferredExitPopup(item);
2811
+ return;
2812
+ }
2813
+ const currentUrl = typeof window !== "undefined" ? this.normalizeUrl(window.location.href || "") : "";
2814
+ if (!currentUrl || currentUrl === this.normalizeUrl(item.sourceUrl)) {
2815
+ this.removeDeferredExitPopup(item);
2816
+ this.debug("Exit popup dropped because route did not change", item);
2817
+ return;
2818
+ }
2819
+ if (!this.shouldShow(def, void 0, true)) {
2820
+ this.removeDeferredExitPopup(item);
2821
+ this.debug("Exit popup skipped at render-time conditions", item);
2822
+ return;
2823
+ }
2824
+ this.showDefinition(def);
2825
+ this.removeDeferredExitPopup(item);
2826
+ }
2827
+ findPopupDefinition(surveyId, popupId) {
2828
+ if (popupId) {
2829
+ const byId = this.popupDefinitions.find((popup) => popup.id === popupId);
2830
+ if (byId) return byId;
2831
+ }
2832
+ return this.popupDefinitions.find((popup) => popup.surveyId === surveyId);
2833
+ }
2834
+ removeDeferredExitPopup(item) {
2835
+ const queue = this.getDeferredExitQueue().filter((entry) => !(entry.id === item.id && entry.sourceUrl === item.sourceUrl));
2836
+ this.setDeferredExitQueue(queue);
2837
+ }
2838
+ getDeferredExitQueue() {
2839
+ if (typeof window === "undefined") return [];
2840
+ try {
2841
+ const raw = window.sessionStorage.getItem(EXIT_QUEUE_STORAGE_KEY);
2842
+ if (!raw) return [];
2843
+ const parsed = JSON.parse(raw);
2844
+ if (!Array.isArray(parsed)) return [];
2845
+ return parsed.filter((entry) => {
2846
+ if (typeof entry !== "object" || entry === null) return false;
2847
+ const item = entry;
2848
+ return typeof item.id === "string" && typeof item.surveyId === "string" && typeof item.sourceUrl === "string" && typeof item.dueAt === "number" && Number.isFinite(item.dueAt);
2849
+ }).map((item) => ({
2850
+ ...item,
2851
+ sourceUrl: this.normalizeUrl(item.sourceUrl)
2852
+ }));
2853
+ } catch {
2854
+ return [];
2855
+ }
2856
+ }
2857
+ setDeferredExitQueue(queue) {
2858
+ if (typeof window === "undefined") return;
2859
+ try {
2860
+ if (!queue.length) {
2861
+ window.sessionStorage.removeItem(EXIT_QUEUE_STORAGE_KEY);
2862
+ return;
2863
+ }
2864
+ window.sessionStorage.setItem(EXIT_QUEUE_STORAGE_KEY, JSON.stringify(queue));
2865
+ } catch {
2866
+ }
2688
2867
  }
2689
2868
  /** Fetch al servidor para obtener popups */
2690
2869
  async fetchPopupsFromServer() {
2691
2870
  const apiKey = this.config?.apiKey;
2692
2871
  const baseUrl = this.baseUrl;
2872
+ const userId = this.config?.userId;
2693
2873
  if (!apiKey || !baseUrl) {
2694
2874
  this.log("Missing apiKey or baseUrl. Skipping popups fetch.");
2695
2875
  return [];
2696
2876
  }
2697
- const endpoint = `${baseUrl}/sdk/${encodeURIComponent(apiKey)}/popups`;
2877
+ const filter = userId ? { where: { userId } } : null;
2878
+ const query = filter ? `?filter=${encodeURIComponent(JSON.stringify(filter))}` : "";
2879
+ const endpoint = `${baseUrl}/sdk/${encodeURIComponent(apiKey)}/popups${query}`;
2698
2880
  try {
2699
2881
  const response = await fetch(endpoint);
2700
2882
  if (!response.ok) {
@@ -2707,7 +2889,7 @@ var DeepdotsPopups = class {
2707
2889
  this.log("Unexpected popups payload", parsed);
2708
2890
  return [];
2709
2891
  }
2710
- console.log("res: ", parsed, this.validatePopupDefinitions(parsed));
2892
+ this.debug("Fetched popups payload", parsed);
2711
2893
  return parsed;
2712
2894
  } catch (error) {
2713
2895
  this.log("Error fetching popups", error);
@@ -2814,6 +2996,9 @@ var DeepdotsPopups = class {
2814
2996
  emitEvent(type, surveyId, data) {
2815
2997
  const event = { type, surveyId, timestamp: Date.now(), data };
2816
2998
  this.log("Event emitted", event);
2999
+ if (type === "popup_clicked" && data?.action === "partial") {
3000
+ this.markSurveyProgress(surveyId, "PARTIAL");
3001
+ }
2817
3002
  if (type === "survey_completed") {
2818
3003
  this.markSurveyAnswered(surveyId);
2819
3004
  }
@@ -2855,26 +3040,103 @@ var DeepdotsPopups = class {
2855
3040
  this.renderer.init();
2856
3041
  }
2857
3042
  }
2858
- // Type guards
3043
+ normalizeUrl(value) {
3044
+ if (!value) return "";
3045
+ const withoutIndex = value.replace(/\/index\.html(?=($|[?#]))/i, "");
3046
+ return withoutIndex.length > 1 && withoutIndex.endsWith("/") ? withoutIndex.slice(0, -1) : withoutIndex;
3047
+ }
3048
+ safeParseUrl(url) {
3049
+ if (!url) return null;
3050
+ try {
3051
+ return new URL(url, typeof window !== "undefined" ? window.location.href : void 0);
3052
+ } catch {
3053
+ return null;
3054
+ }
3055
+ }
3056
+ mapPopupTriggerType(type) {
3057
+ switch (type) {
3058
+ case "time_on_page":
3059
+ return "time";
3060
+ case "scroll":
3061
+ return "scroll";
3062
+ case "exit":
3063
+ return "exit";
3064
+ case "click":
3065
+ return "click";
3066
+ case "event":
3067
+ return "event";
3068
+ default:
3069
+ return null;
3070
+ }
3071
+ }
3072
+ markSurveyProgress(surveyId, status) {
3073
+ const current = this.surveyProgress.get(surveyId);
3074
+ if (current?.status === "COMPLETED" && status !== "COMPLETED") {
3075
+ return;
3076
+ }
3077
+ this.surveyProgress.set(surveyId, { status, timestamp: Date.now() });
3078
+ }
2859
3079
  isPopupDefinition(value) {
2860
3080
  if (typeof value !== "object" || value === null) return false;
2861
- const v = value;
2862
- const trigger = v.triggers || v.triggers;
2863
- if (!trigger) return false;
2864
- return typeof v.id === "string" && typeof v.title === "string" && typeof v.message === "string" && typeof v.surveyId === "string" && typeof v.productId === "string";
3081
+ const popup = value;
3082
+ return typeof popup.id === "string" && typeof popup.title === "string" && typeof popup.message === "string" && typeof popup.surveyId === "string" && typeof popup.productId === "string" && Array.isArray(popup.triggers) && popup.triggers.length > 0 && popup.triggers.every((trigger) => this.isPopupTrigger(trigger)) && (!popup.cooldown || popup.cooldown.every((condition) => this.isPopupTriggerCondition(condition)));
3083
+ }
3084
+ isPopupTrigger(value) {
3085
+ if (typeof value !== "object" || value === null) return false;
3086
+ const trigger = value;
3087
+ return this.isPopupTriggerType(trigger.type) && (typeof trigger.value === "number" || typeof trigger.value === "string");
3088
+ }
3089
+ isPopupTriggerType(value) {
3090
+ return value === "time_on_page" || value === "scroll" || value === "exit" || value === "click" || value === "event";
3091
+ }
3092
+ isPopupTriggerCondition(value) {
3093
+ if (typeof value !== "object" || value === null) return false;
3094
+ const condition = value;
3095
+ return POPUP_TRIGGER_CONDITION_STATUSES.includes(condition.answered) && typeof condition.cooldownDays === "number" && Number.isFinite(condition.cooldownDays) && condition.cooldownDays >= 0;
3096
+ }
3097
+ isLegacyPopupTriggerCondition(value) {
3098
+ if (typeof value !== "object" || value === null) return false;
3099
+ const condition = value;
3100
+ if (condition.answered !== void 0 && typeof condition.answered !== "boolean") {
3101
+ return false;
3102
+ }
3103
+ if (condition.cooldownDays !== void 0) {
3104
+ return typeof condition.cooldownDays === "number" && Number.isFinite(condition.cooldownDays) && condition.cooldownDays >= 0;
3105
+ }
3106
+ return true;
3107
+ }
3108
+ normalizeTriggers(value) {
3109
+ const items = Array.isArray(value) ? value : value ? [value] : [];
3110
+ return items.filter((trigger) => this.isPopupTrigger(trigger)).map((trigger) => ({
3111
+ type: trigger.type,
3112
+ value: trigger.value
3113
+ }));
3114
+ }
3115
+ normalizeCooldown(value) {
3116
+ if (!Array.isArray(value)) return [];
3117
+ return value.filter((condition) => this.isPopupTriggerCondition(condition));
3118
+ }
3119
+ normalizeLegacyConditions(value) {
3120
+ if (!Array.isArray(value)) return [];
3121
+ return value.filter((condition) => this.isLegacyPopupTriggerCondition(condition));
2865
3122
  }
2866
3123
  normalizePopupDefinition(def) {
2867
3124
  if (typeof def !== "object" || def === null) return null;
2868
3125
  const raw = def;
2869
- const trigger = raw.triggers || raw.triggers;
2870
- if (!trigger) return null;
2871
- const condition = trigger.condition ?? raw.conditions ?? [];
3126
+ const rawTriggers = raw.triggers ?? raw.trigger;
3127
+ const triggers = this.normalizeTriggers(rawTriggers);
3128
+ if (!triggers.length) return null;
3129
+ const rawTriggerList = Array.isArray(rawTriggers) ? rawTriggers : rawTriggers ? [rawTriggers] : [];
3130
+ const legacyConditions = [
3131
+ ...this.normalizeLegacyConditions(raw.conditions),
3132
+ ...rawTriggerList.flatMap((trigger) => this.normalizeLegacyConditions(trigger.condition))
3133
+ ];
3134
+ const cooldown = this.normalizeCooldown(raw.cooldown);
2872
3135
  return {
2873
3136
  ...raw,
2874
- triggers: {
2875
- ...trigger,
2876
- condition
2877
- }
3137
+ triggers,
3138
+ cooldown,
3139
+ legacyConditions
2878
3140
  };
2879
3141
  }
2880
3142
  validatePopupDefinitions(defs) {