@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.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) {
@@ -1774,7 +1781,7 @@ function ensureResponsiveStyles(popup) {
1774
1781
  `;
1775
1782
  popup.appendChild(style);
1776
1783
  }
1777
- async function renderPopup(container, surveyId, productId, actions, emit, onClose) {
1784
+ async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production") {
1778
1785
  let surveyCompletedEmitted = false;
1779
1786
  let stylesInjected = false;
1780
1787
  const popup = document.createElement("div");
@@ -2058,7 +2065,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
2058
2065
  setLoading(false);
2059
2066
  return;
2060
2067
  }
2061
- import_native.default.init({ debug: true, env: "prod" });
2068
+ import_native.default.init({ debug: true, env: env === "production" ? "prod" : "dev" });
2062
2069
  formInstance = import_native.default.form(surveyId, productId);
2063
2070
  const generateOptions = {
2064
2071
  addButton: false,
@@ -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,12 +2217,18 @@ 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;
2223
+ case "click":
2224
+ setupClickTrigger(popups, trigger);
2225
+ break;
2212
2226
  }
2213
2227
  }
2214
2228
  function setupTimeTrigger(popups, trigger) {
2215
2229
  const delay = trigger.value || 5e3;
2216
2230
  setTimeout(() => {
2217
- popups.triggerSurvey(trigger.surveyId);
2231
+ popups.triggerSurvey(trigger.surveyId, trigger.popupId);
2218
2232
  }, delay);
2219
2233
  popups.debug(`Time trigger set for ${delay}ms`);
2220
2234
  }
@@ -2223,7 +2237,7 @@ function setupScrollTrigger(popups, trigger) {
2223
2237
  const handler = () => {
2224
2238
  const scrollPercentage = window.scrollY / (document.documentElement.scrollHeight - window.innerHeight) * 100;
2225
2239
  if (scrollPercentage >= threshold) {
2226
- popups.triggerSurvey(trigger.surveyId);
2240
+ popups.triggerSurvey(trigger.surveyId, trigger.popupId);
2227
2241
  window.removeEventListener("scroll", handler);
2228
2242
  }
2229
2243
  };
@@ -2231,23 +2245,137 @@ function setupScrollTrigger(popups, trigger) {
2231
2245
  popups.debug(`Scroll trigger set for ${threshold}%`);
2232
2246
  }
2233
2247
  function setupExitTrigger(popups, trigger) {
2234
- const handler = (e) => {
2235
- if (e.clientY <= 0) {
2236
- popups.triggerSurvey(trigger.surveyId);
2237
- document.removeEventListener("mouseout", handler);
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);
2266
+ };
2267
+ const handleNavigation = (to) => {
2268
+ const from = lastUrl;
2269
+ queueLeave(from, to);
2270
+ lastUrl = normalizeUrl(to);
2271
+ };
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:")) {
2280
+ return;
2281
+ }
2282
+ const target = anchor.getAttribute("target");
2283
+ if (target && target !== "_self") return;
2284
+ let destination;
2285
+ try {
2286
+ destination = new URL(href, window.location.href);
2287
+ } catch {
2288
+ return;
2238
2289
  }
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);
2239
2298
  };
2240
- document.addEventListener("mouseout", handler);
2241
- popups.debug("Exit intent trigger set");
2299
+ const handlePopState = () => {
2300
+ handleNavigation(window.location.href);
2301
+ };
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);
2318
+ window.addEventListener("popstate", handlePopState);
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;
2345
+ }
2346
+ function setupClickTrigger(popups, trigger) {
2347
+ const targetId = typeof trigger.value === "string" ? trigger.value : "";
2348
+ if (!targetId) {
2349
+ popups.debug("Click trigger missing element id", trigger);
2350
+ return;
2351
+ }
2352
+ const attach = () => {
2353
+ const el = document.getElementById(targetId);
2354
+ if (!el) {
2355
+ popups.debug("Click trigger target not found", targetId);
2356
+ return false;
2357
+ }
2358
+ const handler = (e) => {
2359
+ if (e.defaultPrevented) return;
2360
+ popups.triggerSurvey(trigger.surveyId, trigger.popupId);
2361
+ el.removeEventListener("click", handler);
2362
+ };
2363
+ el.addEventListener("click", handler);
2364
+ popups.debug(`Click trigger set for #${targetId}`);
2365
+ return true;
2366
+ };
2367
+ if (typeof document === "undefined") return;
2368
+ if (attach()) return;
2369
+ window.addEventListener("DOMContentLoaded", () => attach(), { once: true });
2242
2370
  }
2243
2371
 
2244
2372
  // src/config/env.ts
2245
2373
  var ENVIRONMENT_MAP = {
2246
2374
  development: {
2247
- apiBaseUrl: "https://api-dev.magicfeedback.com"
2375
+ apiBaseUrl: "https://api-dev.deepdots.com"
2248
2376
  },
2249
2377
  production: {
2250
- apiBaseUrl: "https://api.magicfeedback.com"
2378
+ apiBaseUrl: "https://api.deepdots.com"
2251
2379
  }
2252
2380
  };
2253
2381
  function resolveEnvironment(nodeEnv) {
@@ -2329,10 +2457,10 @@ var BrowserPopupRenderer = class {
2329
2457
  document.body.appendChild(this.container);
2330
2458
  }
2331
2459
  }
2332
- show(surveyId, productId, actions, emit, onClose) {
2460
+ show(surveyId, productId, actions, emit, onClose, env = "production") {
2333
2461
  if (!this.container) this.init();
2334
2462
  if (!this.container) return;
2335
- renderPopup(this.container, surveyId, productId, actions, emit, onClose);
2463
+ renderPopup(this.container, surveyId, productId, actions, emit, onClose, env);
2336
2464
  }
2337
2465
  hide() {
2338
2466
  if (this.container) {
@@ -2351,6 +2479,8 @@ function createDefaultRenderer() {
2351
2479
  }
2352
2480
 
2353
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;
2354
2484
  var DeepdotsPopups = class {
2355
2485
  constructor() {
2356
2486
  this.config = null;
@@ -2364,9 +2494,15 @@ var DeepdotsPopups = class {
2364
2494
  this.popupsLoaded = false;
2365
2495
  this.pendingAutoLaunch = false;
2366
2496
  this.answeredSurveys = /* @__PURE__ */ new Set();
2497
+ this.surveyProgress = /* @__PURE__ */ new Map();
2367
2498
  this.lastShown = /* @__PURE__ */ new Map();
2499
+ // popupId -> timestamp mostrado
2500
+ this.surveyToPopupId = /* @__PURE__ */ new Map();
2501
+ // surveyId -> popupId
2502
+ this.deferredExitTimers = [];
2503
+ this.baseUrl = "";
2504
+ this.env = "production";
2368
2505
  }
2369
- // popupId -> timestamp mostrado
2370
2506
  /** Initialize the SDK with configuration */
2371
2507
  init(config) {
2372
2508
  if (this.initialized) {
@@ -2374,27 +2510,31 @@ var DeepdotsPopups = class {
2374
2510
  return;
2375
2511
  }
2376
2512
  const env = resolveEnvironment(config.nodeEnv);
2513
+ this.baseUrl = env.apiBaseUrl;
2514
+ this.env = config.nodeEnv || "production";
2377
2515
  this.config = {
2378
2516
  apiKey: config.apiKey || void 0,
2379
- baseUrl: env.apiBaseUrl,
2380
2517
  mode: config.mode || "client",
2381
2518
  debug: config.debug || false,
2382
- popups: config.popups || []
2519
+ popups: config.popups || [],
2520
+ userId: config.userId || void 0
2383
2521
  };
2384
2522
  this.initialized = true;
2385
2523
  this.log("SDK initialized", this.config);
2386
2524
  if (this.renderer.init) this.renderer.init();
2387
2525
  this.setupPopupContainer();
2388
2526
  if (this.config.mode === "client") {
2389
- this.popupDefinitions = this.config.popups || [];
2527
+ this.popupDefinitions = this.validatePopupDefinitions(this.config.popups || []);
2390
2528
  this.popupsLoaded = true;
2391
2529
  this.configureTriggersFromDefinitions();
2530
+ this.processDeferredExitQueue();
2392
2531
  } else {
2393
2532
  this.fetchPopupsFromServer().then((defs) => {
2394
- this.popupDefinitions = defs;
2533
+ this.popupDefinitions = this.validatePopupDefinitions(defs);
2395
2534
  this.popupsLoaded = true;
2396
- this.log("Popups loaded from server (fake)", defs);
2535
+ this.log("Popups loaded from server (fake)", this.popupDefinitions);
2397
2536
  this.configureTriggersFromDefinitions();
2537
+ this.processDeferredExitQueue();
2398
2538
  if (this.pendingAutoLaunch) {
2399
2539
  this.startTriggers();
2400
2540
  }
@@ -2420,7 +2560,6 @@ var DeepdotsPopups = class {
2420
2560
  }
2421
2561
  /** Show a popup immediately (supports legacy ShowOptions and new PopupDefinition) */
2422
2562
  show(options) {
2423
- console.log("Show popup called with options:", options);
2424
2563
  if (!this.initialized) {
2425
2564
  throw new Error("SDK not initialized. Call init() first.");
2426
2565
  }
@@ -2428,8 +2567,10 @@ var DeepdotsPopups = class {
2428
2567
  if (isDefinition) {
2429
2568
  const def = options;
2430
2569
  this.log("Showing popup (definition)", def);
2570
+ this.surveyToPopupId.set(def.surveyId, def.id);
2571
+ this.lastShown.set(def.id, Date.now());
2431
2572
  this.renderPopup(def.surveyId, def.productId, def.actions);
2432
- this.emitEvent("popup_shown", def.surveyId);
2573
+ this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
2433
2574
  } else {
2434
2575
  const opt = options;
2435
2576
  this.log("Showing popup (legacy options)", opt);
@@ -2439,7 +2580,7 @@ var DeepdotsPopups = class {
2439
2580
  }
2440
2581
  /** Mostrar popup por id de definición (usa surveyId interno) */
2441
2582
  showByPopupId(popupId) {
2442
- const def = this.popupDefinitions.find((p) => p.id === popupId);
2583
+ const def = this.popupDefinitions.find((popup) => popup.id === popupId);
2443
2584
  if (!def) {
2444
2585
  this.log("Popup definition not found", popupId);
2445
2586
  return;
@@ -2457,92 +2598,315 @@ var DeepdotsPopups = class {
2457
2598
  /** Deriva triggers desde las definiciones de popup */
2458
2599
  configureTriggersFromDefinitions() {
2459
2600
  const derived = [];
2460
- const validDefs = this.validatePopupDefinitions(this.popupDefinitions);
2461
- validDefs.forEach((def) => {
2462
- if (!def.trigger) return;
2463
- const t = def.trigger;
2464
- let type;
2465
- switch (t.type) {
2466
- case "time_on_page":
2467
- type = "time";
2468
- break;
2469
- case "scroll":
2470
- type = "scroll";
2471
- break;
2472
- case "exit":
2473
- type = "exit";
2474
- break;
2475
- default:
2476
- 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);
2477
2607
  return;
2478
- }
2479
- const value = type === "time" ? t.value * 1e3 : t.value;
2480
- 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
+ });
2481
2612
  });
2482
2613
  if (derived.length) {
2483
2614
  this.configureTriggers(derived);
2484
2615
  }
2485
2616
  }
2486
2617
  /** Lógica para evaluar condiciones antes de mostrar una encuesta */
2487
- triggerSurvey(surveyId) {
2488
- const def = this.popupDefinitions.find((p) => p.surveyId === surveyId);
2618
+ triggerSurvey(surveyId, popupId) {
2619
+ const def = this.findPopupDefinition(surveyId, popupId);
2489
2620
  if (!def) {
2490
- this.debug("No popup definition for surveyId", surveyId);
2621
+ this.debug("No popup definition for trigger", { surveyId, popupId });
2491
2622
  return;
2492
2623
  }
2493
2624
  if (!this.shouldShow(def)) {
2494
2625
  this.debug("Conditions prevented showing popup", def.id);
2495
2626
  return;
2496
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) {
2497
2658
  this.show(def);
2498
- this.lastShown.set(def.id, Date.now());
2499
2659
  }
2500
- shouldShow(def) {
2501
- const conditions = def.trigger.condition || [];
2502
- if (!conditions.length) return true;
2503
- return conditions.every((c) => this.evaluateCondition(def, c));
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))) {
2666
+ return false;
2667
+ }
2668
+ const legacyConditions = def.legacyConditions || [];
2669
+ if (!legacyConditions.length) return true;
2670
+ return legacyConditions.every((condition) => this.evaluateLegacyCondition(def, condition));
2504
2671
  }
2505
- evaluateCondition(def, condition) {
2672
+ matchesSegmentsPath(def, pathUrl) {
2673
+ const paths = def.segments?.path;
2674
+ if (!paths || paths.length === 0) return true;
2675
+ if (typeof window === "undefined" || !window.location) {
2676
+ this.debug("No window.location available for path comparison", { popupId: def.id, paths });
2677
+ return true;
2678
+ }
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 || "");
2682
+ const matches = paths.some((rawCandidate) => {
2683
+ const candidate = this.normalizeUrl(rawCandidate);
2684
+ let match = false;
2685
+ if (candidate.startsWith("http://") || candidate.startsWith("https://")) {
2686
+ match = normalizedHref === candidate;
2687
+ } else if (candidate.startsWith("/")) {
2688
+ match = normalizedHref.includes(candidate);
2689
+ } else {
2690
+ match = normalizedPath === candidate;
2691
+ }
2692
+ this.debug("Path comparison", {
2693
+ popupId: def.id,
2694
+ candidate,
2695
+ currentPath: normalizedPath,
2696
+ currentHref: normalizedHref,
2697
+ match
2698
+ });
2699
+ return match;
2700
+ });
2701
+ if (!matches) {
2702
+ this.debug("No path match for popup", { popupId: def.id, paths, currentPath: normalizedPath });
2703
+ }
2704
+ return matches;
2705
+ }
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) {
2506
2723
  if (!condition.answered && this.answeredSurveys.has(def.surveyId)) {
2507
2724
  return false;
2508
2725
  }
2509
- if (condition.cooldownDays) {
2510
- const last = this.lastShown.get(def.id);
2511
- if (last) {
2512
- const msSince = Date.now() - last;
2513
- const required = condition.cooldownDays * 24 * 60 * 60 * 1e3;
2514
- if (msSince < required) return false;
2515
- }
2726
+ return this.hasCooldownElapsed(this.lastShown.get(def.id), condition.cooldownDays);
2727
+ }
2728
+ hasCooldownElapsed(timestamp, cooldownDays) {
2729
+ if (!timestamp || !cooldownDays) {
2730
+ return true;
2516
2731
  }
2517
- return true;
2732
+ return Date.now() - timestamp >= cooldownDays * DAY_IN_MS;
2518
2733
  }
2519
2734
  /** Marcar encuesta como contestada externamente */
2520
2735
  markSurveyAnswered(surveyId) {
2521
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);
2522
2812
  }
2523
- /** Simula fetch al servidor para obtener popups */
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
+ }
2853
+ }
2854
+ /** Fetch al servidor para obtener popups */
2524
2855
  async fetchPopupsFromServer() {
2525
- await new Promise((res) => setTimeout(res, 50));
2526
- return this.generateFakePopups();
2527
- }
2528
- /** Genera datos fake para pruebas en modo server */
2529
- generateFakePopups() {
2530
- return [
2531
- {
2532
- id: "popup-123",
2533
- title: "Would you like to help us improve?",
2534
- message: "<p>Take a quick survey and share your thoughts.</p>",
2535
- trigger: { type: "time_on_page", value: 1, condition: [{ answered: false, cooldownDays: 7 }] },
2536
- actions: {
2537
- accept: { label: "Take Survey", surveyId: "survey-abc" },
2538
- decline: { label: "Not Now", cooldownDays: 7 }
2539
- },
2540
- surveyId: "survey-abc",
2541
- productId: "product-xyz",
2542
- style: { theme: "light", position: "bottom-right", imageUrl: null },
2543
- segments: { lang: ["en"], path: ["/checkout"] }
2856
+ const apiKey = this.config?.apiKey;
2857
+ const baseUrl = this.baseUrl;
2858
+ const userId = this.config?.userId;
2859
+ if (!apiKey || !baseUrl) {
2860
+ this.log("Missing apiKey or baseUrl. Skipping popups fetch.");
2861
+ return [];
2862
+ }
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}`;
2866
+ try {
2867
+ const response = await fetch(endpoint);
2868
+ if (!response.ok) {
2869
+ this.log("Failed to fetch popups", response.status, response.statusText);
2870
+ return [];
2544
2871
  }
2545
- ];
2872
+ const raw = await response.text();
2873
+ const parsed = JSON.parse(raw);
2874
+ if (!Array.isArray(parsed)) {
2875
+ this.log("Unexpected popups payload", parsed);
2876
+ return [];
2877
+ }
2878
+ this.debug("Fetched popups payload", parsed);
2879
+ return parsed;
2880
+ } catch (error) {
2881
+ this.log("Error fetching popups", error);
2882
+ return [];
2883
+ }
2884
+ }
2885
+ /** Notifica eventos de popup a la API */
2886
+ async postPopupEvent(status, popupId, userId) {
2887
+ const apiKey = this.config?.apiKey;
2888
+ const baseUrl = this.baseUrl;
2889
+ if (!apiKey || !baseUrl) {
2890
+ return;
2891
+ }
2892
+ const endpoint = `${baseUrl}/sdk/popups`;
2893
+ try {
2894
+ const response = await fetch(endpoint, {
2895
+ method: "POST",
2896
+ headers: { "Content-Type": "application/json" },
2897
+ body: JSON.stringify({
2898
+ publicKey: apiKey,
2899
+ status,
2900
+ popupId,
2901
+ userId: userId || void 0
2902
+ })
2903
+ });
2904
+ if (!response.ok) {
2905
+ this.log("Failed to post popup event", response.status, response.statusText);
2906
+ }
2907
+ } catch (error) {
2908
+ this.log("Error posting popup event", error);
2909
+ }
2546
2910
  }
2547
2911
  /** Add an event listener */
2548
2912
  on(eventType, listener) {
@@ -2582,11 +2946,26 @@ var DeepdotsPopups = class {
2582
2946
  /** Render the popup UI */
2583
2947
  renderPopup(surveyId, productId, actions) {
2584
2948
  if (this.renderer) {
2585
- this.renderer.show(surveyId, productId, actions, (type, id, payload) => this.emitEvent(type, id, payload), () => this.hidePopup());
2949
+ this.renderer.show(
2950
+ surveyId,
2951
+ productId,
2952
+ actions,
2953
+ (type, id, payload) => this.emitEvent(type, id, payload),
2954
+ () => this.hidePopup(),
2955
+ this.env
2956
+ );
2586
2957
  return;
2587
2958
  }
2588
2959
  if (!this.popupContainer) return;
2589
- renderPopup(this.popupContainer, surveyId, productId, actions, (type, id, payload) => this.emitEvent(type, id, payload), () => this.hidePopup());
2960
+ renderPopup(
2961
+ this.popupContainer,
2962
+ surveyId,
2963
+ productId,
2964
+ actions,
2965
+ (type, id, payload) => this.emitEvent(type, id, payload),
2966
+ () => this.hidePopup(),
2967
+ this.env
2968
+ );
2590
2969
  }
2591
2970
  /** Hide the popup */
2592
2971
  hidePopup() {
@@ -2603,9 +2982,22 @@ var DeepdotsPopups = class {
2603
2982
  emitEvent(type, surveyId, data) {
2604
2983
  const event = { type, surveyId, timestamp: Date.now(), data };
2605
2984
  this.log("Event emitted", event);
2985
+ if (type === "popup_clicked" && data?.action === "partial") {
2986
+ this.markSurveyProgress(surveyId, "PARTIAL");
2987
+ }
2606
2988
  if (type === "survey_completed") {
2607
2989
  this.markSurveyAnswered(surveyId);
2608
2990
  }
2991
+ if (type === "popup_shown" || type === "survey_completed") {
2992
+ const popupIdFromData = data?.popupId;
2993
+ const popupId = popupIdFromData || this.surveyToPopupId.get(surveyId);
2994
+ if (popupId) {
2995
+ const userIdFromData = data?.userId;
2996
+ void this.postPopupEvent(type === "popup_shown" ? "opened" : "completed", popupId, userIdFromData || this.config?.userId);
2997
+ } else {
2998
+ this.debug("No popupId available to post event", { type, surveyId });
2999
+ }
3000
+ }
2609
3001
  const listeners = this.listeners.get(type);
2610
3002
  if (listeners) {
2611
3003
  listeners.forEach((listener) => {
@@ -2634,17 +3026,111 @@ var DeepdotsPopups = class {
2634
3026
  this.renderer.init();
2635
3027
  }
2636
3028
  }
2637
- // 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
+ }
2638
3065
  isPopupDefinition(value) {
2639
3066
  if (typeof value !== "object" || value === null) return false;
2640
- const v = value;
2641
- return typeof v.id === "string" && typeof v.title === "string" && typeof v.message === "string" && typeof v.surveyId === "string" && typeof v.productId === "string" && !!v.trigger;
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));
3108
+ }
3109
+ normalizePopupDefinition(def) {
3110
+ if (typeof def !== "object" || def === null) return null;
3111
+ const raw = def;
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);
3121
+ return {
3122
+ ...raw,
3123
+ triggers,
3124
+ cooldown,
3125
+ legacyConditions
3126
+ };
2642
3127
  }
2643
3128
  validatePopupDefinitions(defs) {
2644
- return defs.filter((d) => this.isPopupDefinition(d));
3129
+ return defs.map((def) => this.normalizePopupDefinition(def)).filter((def) => !!def && this.isPopupDefinition(def));
2645
3130
  }
2646
3131
  };
2647
3132
  export {
2648
- DeepdotsPopups
3133
+ DeepdotsPopups,
3134
+ POPUP_TRIGGER_CONDITION_STATUSES
2649
3135
  };
2650
3136
  //# sourceMappingURL=index.mjs.map