@magicfeedback/popup-sdk 1.0.10 → 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) {
@@ -1787,7 +1795,7 @@ function ensureResponsiveStyles(popup) {
1787
1795
  `;
1788
1796
  popup.appendChild(style);
1789
1797
  }
1790
- async function renderPopup(container, surveyId, productId, actions, emit, onClose) {
1798
+ async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production") {
1791
1799
  let surveyCompletedEmitted = false;
1792
1800
  let stylesInjected = false;
1793
1801
  const popup = document.createElement("div");
@@ -2071,7 +2079,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
2071
2079
  setLoading(false);
2072
2080
  return;
2073
2081
  }
2074
- import_native.default.init({ debug: true, env: "prod" });
2082
+ import_native.default.init({ debug: true, env: env === "production" ? "prod" : "dev" });
2075
2083
  formInstance = import_native.default.form(surveyId, productId);
2076
2084
  const generateOptions = {
2077
2085
  addButton: false,
@@ -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,12 +2231,18 @@ 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;
2237
+ case "click":
2238
+ setupClickTrigger(popups, trigger);
2239
+ break;
2225
2240
  }
2226
2241
  }
2227
2242
  function setupTimeTrigger(popups, trigger) {
2228
2243
  const delay = trigger.value || 5e3;
2229
2244
  setTimeout(() => {
2230
- popups.triggerSurvey(trigger.surveyId);
2245
+ popups.triggerSurvey(trigger.surveyId, trigger.popupId);
2231
2246
  }, delay);
2232
2247
  popups.debug(`Time trigger set for ${delay}ms`);
2233
2248
  }
@@ -2236,7 +2251,7 @@ function setupScrollTrigger(popups, trigger) {
2236
2251
  const handler = () => {
2237
2252
  const scrollPercentage = window.scrollY / (document.documentElement.scrollHeight - window.innerHeight) * 100;
2238
2253
  if (scrollPercentage >= threshold) {
2239
- popups.triggerSurvey(trigger.surveyId);
2254
+ popups.triggerSurvey(trigger.surveyId, trigger.popupId);
2240
2255
  window.removeEventListener("scroll", handler);
2241
2256
  }
2242
2257
  };
@@ -2244,23 +2259,137 @@ function setupScrollTrigger(popups, trigger) {
2244
2259
  popups.debug(`Scroll trigger set for ${threshold}%`);
2245
2260
  }
2246
2261
  function setupExitTrigger(popups, trigger) {
2247
- const handler = (e) => {
2248
- if (e.clientY <= 0) {
2249
- popups.triggerSurvey(trigger.surveyId);
2250
- document.removeEventListener("mouseout", handler);
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);
2280
+ };
2281
+ const handleNavigation = (to) => {
2282
+ const from = lastUrl;
2283
+ queueLeave(from, to);
2284
+ lastUrl = normalizeUrl(to);
2285
+ };
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:")) {
2294
+ return;
2295
+ }
2296
+ const target = anchor.getAttribute("target");
2297
+ if (target && target !== "_self") return;
2298
+ let destination;
2299
+ try {
2300
+ destination = new URL(href, window.location.href);
2301
+ } catch {
2302
+ return;
2251
2303
  }
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);
2252
2312
  };
2253
- document.addEventListener("mouseout", handler);
2254
- popups.debug("Exit intent trigger set");
2313
+ const handlePopState = () => {
2314
+ handleNavigation(window.location.href);
2315
+ };
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);
2332
+ window.addEventListener("popstate", handlePopState);
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;
2359
+ }
2360
+ function setupClickTrigger(popups, trigger) {
2361
+ const targetId = typeof trigger.value === "string" ? trigger.value : "";
2362
+ if (!targetId) {
2363
+ popups.debug("Click trigger missing element id", trigger);
2364
+ return;
2365
+ }
2366
+ const attach = () => {
2367
+ const el = document.getElementById(targetId);
2368
+ if (!el) {
2369
+ popups.debug("Click trigger target not found", targetId);
2370
+ return false;
2371
+ }
2372
+ const handler = (e) => {
2373
+ if (e.defaultPrevented) return;
2374
+ popups.triggerSurvey(trigger.surveyId, trigger.popupId);
2375
+ el.removeEventListener("click", handler);
2376
+ };
2377
+ el.addEventListener("click", handler);
2378
+ popups.debug(`Click trigger set for #${targetId}`);
2379
+ return true;
2380
+ };
2381
+ if (typeof document === "undefined") return;
2382
+ if (attach()) return;
2383
+ window.addEventListener("DOMContentLoaded", () => attach(), { once: true });
2255
2384
  }
2256
2385
 
2257
2386
  // src/config/env.ts
2258
2387
  var ENVIRONMENT_MAP = {
2259
2388
  development: {
2260
- apiBaseUrl: "https://api-dev.magicfeedback.com"
2389
+ apiBaseUrl: "https://api-dev.deepdots.com"
2261
2390
  },
2262
2391
  production: {
2263
- apiBaseUrl: "https://api.magicfeedback.com"
2392
+ apiBaseUrl: "https://api.deepdots.com"
2264
2393
  }
2265
2394
  };
2266
2395
  function resolveEnvironment(nodeEnv) {
@@ -2342,10 +2471,10 @@ var BrowserPopupRenderer = class {
2342
2471
  document.body.appendChild(this.container);
2343
2472
  }
2344
2473
  }
2345
- show(surveyId, productId, actions, emit, onClose) {
2474
+ show(surveyId, productId, actions, emit, onClose, env = "production") {
2346
2475
  if (!this.container) this.init();
2347
2476
  if (!this.container) return;
2348
- renderPopup(this.container, surveyId, productId, actions, emit, onClose);
2477
+ renderPopup(this.container, surveyId, productId, actions, emit, onClose, env);
2349
2478
  }
2350
2479
  hide() {
2351
2480
  if (this.container) {
@@ -2364,6 +2493,8 @@ function createDefaultRenderer() {
2364
2493
  }
2365
2494
 
2366
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;
2367
2498
  var DeepdotsPopups = class {
2368
2499
  constructor() {
2369
2500
  this.config = null;
@@ -2377,9 +2508,15 @@ var DeepdotsPopups = class {
2377
2508
  this.popupsLoaded = false;
2378
2509
  this.pendingAutoLaunch = false;
2379
2510
  this.answeredSurveys = /* @__PURE__ */ new Set();
2511
+ this.surveyProgress = /* @__PURE__ */ new Map();
2380
2512
  this.lastShown = /* @__PURE__ */ new Map();
2513
+ // popupId -> timestamp mostrado
2514
+ this.surveyToPopupId = /* @__PURE__ */ new Map();
2515
+ // surveyId -> popupId
2516
+ this.deferredExitTimers = [];
2517
+ this.baseUrl = "";
2518
+ this.env = "production";
2381
2519
  }
2382
- // popupId -> timestamp mostrado
2383
2520
  /** Initialize the SDK with configuration */
2384
2521
  init(config) {
2385
2522
  if (this.initialized) {
@@ -2387,27 +2524,31 @@ var DeepdotsPopups = class {
2387
2524
  return;
2388
2525
  }
2389
2526
  const env = resolveEnvironment(config.nodeEnv);
2527
+ this.baseUrl = env.apiBaseUrl;
2528
+ this.env = config.nodeEnv || "production";
2390
2529
  this.config = {
2391
2530
  apiKey: config.apiKey || void 0,
2392
- baseUrl: env.apiBaseUrl,
2393
2531
  mode: config.mode || "client",
2394
2532
  debug: config.debug || false,
2395
- popups: config.popups || []
2533
+ popups: config.popups || [],
2534
+ userId: config.userId || void 0
2396
2535
  };
2397
2536
  this.initialized = true;
2398
2537
  this.log("SDK initialized", this.config);
2399
2538
  if (this.renderer.init) this.renderer.init();
2400
2539
  this.setupPopupContainer();
2401
2540
  if (this.config.mode === "client") {
2402
- this.popupDefinitions = this.config.popups || [];
2541
+ this.popupDefinitions = this.validatePopupDefinitions(this.config.popups || []);
2403
2542
  this.popupsLoaded = true;
2404
2543
  this.configureTriggersFromDefinitions();
2544
+ this.processDeferredExitQueue();
2405
2545
  } else {
2406
2546
  this.fetchPopupsFromServer().then((defs) => {
2407
- this.popupDefinitions = defs;
2547
+ this.popupDefinitions = this.validatePopupDefinitions(defs);
2408
2548
  this.popupsLoaded = true;
2409
- this.log("Popups loaded from server (fake)", defs);
2549
+ this.log("Popups loaded from server (fake)", this.popupDefinitions);
2410
2550
  this.configureTriggersFromDefinitions();
2551
+ this.processDeferredExitQueue();
2411
2552
  if (this.pendingAutoLaunch) {
2412
2553
  this.startTriggers();
2413
2554
  }
@@ -2433,7 +2574,6 @@ var DeepdotsPopups = class {
2433
2574
  }
2434
2575
  /** Show a popup immediately (supports legacy ShowOptions and new PopupDefinition) */
2435
2576
  show(options) {
2436
- console.log("Show popup called with options:", options);
2437
2577
  if (!this.initialized) {
2438
2578
  throw new Error("SDK not initialized. Call init() first.");
2439
2579
  }
@@ -2441,8 +2581,10 @@ var DeepdotsPopups = class {
2441
2581
  if (isDefinition) {
2442
2582
  const def = options;
2443
2583
  this.log("Showing popup (definition)", def);
2584
+ this.surveyToPopupId.set(def.surveyId, def.id);
2585
+ this.lastShown.set(def.id, Date.now());
2444
2586
  this.renderPopup(def.surveyId, def.productId, def.actions);
2445
- this.emitEvent("popup_shown", def.surveyId);
2587
+ this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
2446
2588
  } else {
2447
2589
  const opt = options;
2448
2590
  this.log("Showing popup (legacy options)", opt);
@@ -2452,7 +2594,7 @@ var DeepdotsPopups = class {
2452
2594
  }
2453
2595
  /** Mostrar popup por id de definición (usa surveyId interno) */
2454
2596
  showByPopupId(popupId) {
2455
- const def = this.popupDefinitions.find((p) => p.id === popupId);
2597
+ const def = this.popupDefinitions.find((popup) => popup.id === popupId);
2456
2598
  if (!def) {
2457
2599
  this.log("Popup definition not found", popupId);
2458
2600
  return;
@@ -2470,92 +2612,315 @@ var DeepdotsPopups = class {
2470
2612
  /** Deriva triggers desde las definiciones de popup */
2471
2613
  configureTriggersFromDefinitions() {
2472
2614
  const derived = [];
2473
- const validDefs = this.validatePopupDefinitions(this.popupDefinitions);
2474
- validDefs.forEach((def) => {
2475
- if (!def.trigger) return;
2476
- const t = def.trigger;
2477
- let type;
2478
- switch (t.type) {
2479
- case "time_on_page":
2480
- type = "time";
2481
- break;
2482
- case "scroll":
2483
- type = "scroll";
2484
- break;
2485
- case "exit":
2486
- type = "exit";
2487
- break;
2488
- default:
2489
- 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);
2490
2621
  return;
2491
- }
2492
- const value = type === "time" ? t.value * 1e3 : t.value;
2493
- 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
+ });
2494
2626
  });
2495
2627
  if (derived.length) {
2496
2628
  this.configureTriggers(derived);
2497
2629
  }
2498
2630
  }
2499
2631
  /** Lógica para evaluar condiciones antes de mostrar una encuesta */
2500
- triggerSurvey(surveyId) {
2501
- const def = this.popupDefinitions.find((p) => p.surveyId === surveyId);
2632
+ triggerSurvey(surveyId, popupId) {
2633
+ const def = this.findPopupDefinition(surveyId, popupId);
2502
2634
  if (!def) {
2503
- this.debug("No popup definition for surveyId", surveyId);
2635
+ this.debug("No popup definition for trigger", { surveyId, popupId });
2504
2636
  return;
2505
2637
  }
2506
2638
  if (!this.shouldShow(def)) {
2507
2639
  this.debug("Conditions prevented showing popup", def.id);
2508
2640
  return;
2509
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) {
2510
2672
  this.show(def);
2511
- this.lastShown.set(def.id, Date.now());
2512
2673
  }
2513
- shouldShow(def) {
2514
- const conditions = def.trigger.condition || [];
2515
- if (!conditions.length) return true;
2516
- return conditions.every((c) => this.evaluateCondition(def, c));
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))) {
2680
+ return false;
2681
+ }
2682
+ const legacyConditions = def.legacyConditions || [];
2683
+ if (!legacyConditions.length) return true;
2684
+ return legacyConditions.every((condition) => this.evaluateLegacyCondition(def, condition));
2517
2685
  }
2518
- evaluateCondition(def, condition) {
2686
+ matchesSegmentsPath(def, pathUrl) {
2687
+ const paths = def.segments?.path;
2688
+ if (!paths || paths.length === 0) return true;
2689
+ if (typeof window === "undefined" || !window.location) {
2690
+ this.debug("No window.location available for path comparison", { popupId: def.id, paths });
2691
+ return true;
2692
+ }
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 || "");
2696
+ const matches = paths.some((rawCandidate) => {
2697
+ const candidate = this.normalizeUrl(rawCandidate);
2698
+ let match = false;
2699
+ if (candidate.startsWith("http://") || candidate.startsWith("https://")) {
2700
+ match = normalizedHref === candidate;
2701
+ } else if (candidate.startsWith("/")) {
2702
+ match = normalizedHref.includes(candidate);
2703
+ } else {
2704
+ match = normalizedPath === candidate;
2705
+ }
2706
+ this.debug("Path comparison", {
2707
+ popupId: def.id,
2708
+ candidate,
2709
+ currentPath: normalizedPath,
2710
+ currentHref: normalizedHref,
2711
+ match
2712
+ });
2713
+ return match;
2714
+ });
2715
+ if (!matches) {
2716
+ this.debug("No path match for popup", { popupId: def.id, paths, currentPath: normalizedPath });
2717
+ }
2718
+ return matches;
2719
+ }
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) {
2519
2737
  if (!condition.answered && this.answeredSurveys.has(def.surveyId)) {
2520
2738
  return false;
2521
2739
  }
2522
- if (condition.cooldownDays) {
2523
- const last = this.lastShown.get(def.id);
2524
- if (last) {
2525
- const msSince = Date.now() - last;
2526
- const required = condition.cooldownDays * 24 * 60 * 60 * 1e3;
2527
- if (msSince < required) return false;
2528
- }
2740
+ return this.hasCooldownElapsed(this.lastShown.get(def.id), condition.cooldownDays);
2741
+ }
2742
+ hasCooldownElapsed(timestamp, cooldownDays) {
2743
+ if (!timestamp || !cooldownDays) {
2744
+ return true;
2529
2745
  }
2530
- return true;
2746
+ return Date.now() - timestamp >= cooldownDays * DAY_IN_MS;
2531
2747
  }
2532
2748
  /** Marcar encuesta como contestada externamente */
2533
2749
  markSurveyAnswered(surveyId) {
2534
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);
2535
2826
  }
2536
- /** Simula fetch al servidor para obtener popups */
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
+ }
2867
+ }
2868
+ /** Fetch al servidor para obtener popups */
2537
2869
  async fetchPopupsFromServer() {
2538
- await new Promise((res) => setTimeout(res, 50));
2539
- return this.generateFakePopups();
2540
- }
2541
- /** Genera datos fake para pruebas en modo server */
2542
- generateFakePopups() {
2543
- return [
2544
- {
2545
- id: "popup-123",
2546
- title: "Would you like to help us improve?",
2547
- message: "<p>Take a quick survey and share your thoughts.</p>",
2548
- trigger: { type: "time_on_page", value: 1, condition: [{ answered: false, cooldownDays: 7 }] },
2549
- actions: {
2550
- accept: { label: "Take Survey", surveyId: "survey-abc" },
2551
- decline: { label: "Not Now", cooldownDays: 7 }
2552
- },
2553
- surveyId: "survey-abc",
2554
- productId: "product-xyz",
2555
- style: { theme: "light", position: "bottom-right", imageUrl: null },
2556
- segments: { lang: ["en"], path: ["/checkout"] }
2870
+ const apiKey = this.config?.apiKey;
2871
+ const baseUrl = this.baseUrl;
2872
+ const userId = this.config?.userId;
2873
+ if (!apiKey || !baseUrl) {
2874
+ this.log("Missing apiKey or baseUrl. Skipping popups fetch.");
2875
+ return [];
2876
+ }
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}`;
2880
+ try {
2881
+ const response = await fetch(endpoint);
2882
+ if (!response.ok) {
2883
+ this.log("Failed to fetch popups", response.status, response.statusText);
2884
+ return [];
2557
2885
  }
2558
- ];
2886
+ const raw = await response.text();
2887
+ const parsed = JSON.parse(raw);
2888
+ if (!Array.isArray(parsed)) {
2889
+ this.log("Unexpected popups payload", parsed);
2890
+ return [];
2891
+ }
2892
+ this.debug("Fetched popups payload", parsed);
2893
+ return parsed;
2894
+ } catch (error) {
2895
+ this.log("Error fetching popups", error);
2896
+ return [];
2897
+ }
2898
+ }
2899
+ /** Notifica eventos de popup a la API */
2900
+ async postPopupEvent(status, popupId, userId) {
2901
+ const apiKey = this.config?.apiKey;
2902
+ const baseUrl = this.baseUrl;
2903
+ if (!apiKey || !baseUrl) {
2904
+ return;
2905
+ }
2906
+ const endpoint = `${baseUrl}/sdk/popups`;
2907
+ try {
2908
+ const response = await fetch(endpoint, {
2909
+ method: "POST",
2910
+ headers: { "Content-Type": "application/json" },
2911
+ body: JSON.stringify({
2912
+ publicKey: apiKey,
2913
+ status,
2914
+ popupId,
2915
+ userId: userId || void 0
2916
+ })
2917
+ });
2918
+ if (!response.ok) {
2919
+ this.log("Failed to post popup event", response.status, response.statusText);
2920
+ }
2921
+ } catch (error) {
2922
+ this.log("Error posting popup event", error);
2923
+ }
2559
2924
  }
2560
2925
  /** Add an event listener */
2561
2926
  on(eventType, listener) {
@@ -2595,11 +2960,26 @@ var DeepdotsPopups = class {
2595
2960
  /** Render the popup UI */
2596
2961
  renderPopup(surveyId, productId, actions) {
2597
2962
  if (this.renderer) {
2598
- this.renderer.show(surveyId, productId, actions, (type, id, payload) => this.emitEvent(type, id, payload), () => this.hidePopup());
2963
+ this.renderer.show(
2964
+ surveyId,
2965
+ productId,
2966
+ actions,
2967
+ (type, id, payload) => this.emitEvent(type, id, payload),
2968
+ () => this.hidePopup(),
2969
+ this.env
2970
+ );
2599
2971
  return;
2600
2972
  }
2601
2973
  if (!this.popupContainer) return;
2602
- renderPopup(this.popupContainer, surveyId, productId, actions, (type, id, payload) => this.emitEvent(type, id, payload), () => this.hidePopup());
2974
+ renderPopup(
2975
+ this.popupContainer,
2976
+ surveyId,
2977
+ productId,
2978
+ actions,
2979
+ (type, id, payload) => this.emitEvent(type, id, payload),
2980
+ () => this.hidePopup(),
2981
+ this.env
2982
+ );
2603
2983
  }
2604
2984
  /** Hide the popup */
2605
2985
  hidePopup() {
@@ -2616,9 +2996,22 @@ var DeepdotsPopups = class {
2616
2996
  emitEvent(type, surveyId, data) {
2617
2997
  const event = { type, surveyId, timestamp: Date.now(), data };
2618
2998
  this.log("Event emitted", event);
2999
+ if (type === "popup_clicked" && data?.action === "partial") {
3000
+ this.markSurveyProgress(surveyId, "PARTIAL");
3001
+ }
2619
3002
  if (type === "survey_completed") {
2620
3003
  this.markSurveyAnswered(surveyId);
2621
3004
  }
3005
+ if (type === "popup_shown" || type === "survey_completed") {
3006
+ const popupIdFromData = data?.popupId;
3007
+ const popupId = popupIdFromData || this.surveyToPopupId.get(surveyId);
3008
+ if (popupId) {
3009
+ const userIdFromData = data?.userId;
3010
+ void this.postPopupEvent(type === "popup_shown" ? "opened" : "completed", popupId, userIdFromData || this.config?.userId);
3011
+ } else {
3012
+ this.debug("No popupId available to post event", { type, surveyId });
3013
+ }
3014
+ }
2622
3015
  const listeners = this.listeners.get(type);
2623
3016
  if (listeners) {
2624
3017
  listeners.forEach((listener) => {
@@ -2647,14 +3040,107 @@ var DeepdotsPopups = class {
2647
3040
  this.renderer.init();
2648
3041
  }
2649
3042
  }
2650
- // 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
+ }
2651
3079
  isPopupDefinition(value) {
2652
3080
  if (typeof value !== "object" || value === null) return false;
2653
- const v = value;
2654
- return typeof v.id === "string" && typeof v.title === "string" && typeof v.message === "string" && typeof v.surveyId === "string" && typeof v.productId === "string" && !!v.trigger;
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));
3122
+ }
3123
+ normalizePopupDefinition(def) {
3124
+ if (typeof def !== "object" || def === null) return null;
3125
+ const raw = def;
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);
3135
+ return {
3136
+ ...raw,
3137
+ triggers,
3138
+ cooldown,
3139
+ legacyConditions
3140
+ };
2655
3141
  }
2656
3142
  validatePopupDefinitions(defs) {
2657
- return defs.filter((d) => this.isPopupDefinition(d));
3143
+ return defs.map((def) => this.normalizePopupDefinition(def)).filter((def) => !!def && this.isPopupDefinition(def));
2658
3144
  }
2659
3145
  };
2660
3146
  //# sourceMappingURL=index.js.map