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