@magicfeedback/popup-sdk 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -592,11 +592,25 @@ ${customCss}
592
592
  ${PRIMARY_COLOR_JS}
593
593
  ${BUSY_JS}
594
594
  ${REVEAL_JS}
595
+ // Foco en la primera pregunta si es de escribir. Al navegar lo pone @magicfeedback/native
596
+ // (autofocus:'navigation'); la apertura la decide el popup, que es quien sabe cu\xE1ndo se revela.
597
+ // Necesita el popup revelado y el survey cargado, que llegan en cualquier orden, y solo con
598
+ // rat\xF3n o trackpad: en t\xE1ctil sacar\xEDa el teclado sin que el usuario haya tocado nada.
599
+ var ddRevealed=false, ddSurveyLoaded=false, ddOpenFocusDone=false;
600
+ function ddAutofocusOnOpen(){
601
+ if(ddOpenFocusDone || !ddRevealed || !ddSurveyLoaded){ return; }
602
+ ddOpenFocusDone=true;
603
+ var fine=typeof window.matchMedia==='function' && window.matchMedia('(pointer: fine)').matches;
604
+ var f=window.DeepdotsForm;
605
+ if(fine && f && typeof f.focusFirstQuestion==='function'){ f.focusFirstQuestion(); }
606
+ }
595
607
  // El popup se ense\xF1a cuando el survey est\xE1 pintado; \`ready\` deja al host abrir su propio
596
608
  // Modal en ese momento en vez de montar un WebView en blanco.
597
609
  var ddReveal=ddCreateReveal(popup, function(){
598
610
  document.body.classList.add('dd-ready');
599
611
  emitJSON('ready');
612
+ ddRevealed=true;
613
+ ddAutofocusOnOpen();
600
614
  }, ${REVEAL_TIMEOUT_MS});
601
615
 
602
616
  function setLoading(isLoading){
@@ -637,6 +651,9 @@ ${REVEAL_JS}
637
651
  // Surveys multi-idioma: native pide a la API este idioma (el del init o el del
638
652
  // dispositivo); si el survey no lo tiene, la API sirve el idioma por defecto.
639
653
  lang:${initLangJson}||undefined,
654
+ // Foco en la primera pregunta de escribir tras Start, Siguiente o Atr\xE1s. Las versiones
655
+ // de native sin esta opci\xF3n la ignoran.
656
+ autofocus:'navigation',
640
657
  onLoadedEvent:function(args){
641
658
  // Idioma en el que native muestra el survey: manda sobre el del init para que el
642
659
  // chrome no se quede en ingl\xE9s delante de un survey traducido. Native < 2.3 no lo
@@ -696,6 +713,8 @@ ${REVEAL_JS}
696
713
  updateProgress({ progress:form.progress, total:form.total, completed:false, followup:false });
697
714
  emitJSON('loaded');
698
715
  setLoading(false);
716
+ // La primera carga es la apertura; las siguientes (tras Start) las enfoca native.
717
+ if(!ddSurveyLoaded){ ddSurveyLoaded=true; ddAutofocusOnOpen(); }
699
718
  },
700
719
  beforeSubmitEvent:function(){
701
720
  setLoading(true);
@@ -846,6 +865,7 @@ var ReactNativePopupRenderer = class {
846
865
  this.emitReady(surveyId);
847
866
  break;
848
867
  case "loaded":
868
+ break;
849
869
  case "before_submit":
850
870
  case "after_submit":
851
871
  case "back":
@@ -873,7 +893,7 @@ function createReactNativeRenderer() {
873
893
  }
874
894
 
875
895
  // src/ui/preload.ts
876
- var defaultLoader = () => import("./renderPopup-424HR5LF.mjs");
896
+ var defaultLoader = () => import("./renderPopup-KSZJ5AN4.mjs");
877
897
  var pending = null;
878
898
  function preloadRenderPopup(loader = defaultLoader) {
879
899
  if (pending) return pending;
@@ -946,7 +966,7 @@ var BrowserPopupRenderer = class {
946
966
  if (!this.container) return;
947
967
  this.visible = true;
948
968
  const container = this.container;
949
- void import("./renderPopup-424HR5LF.mjs").then(({ renderPopup }) => {
969
+ void import("./renderPopup-KSZJ5AN4.mjs").then(({ renderPopup }) => {
950
970
  renderPopup(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService, analyticsFeedbackSessionId, options);
951
971
  });
952
972
  }
@@ -1221,7 +1241,7 @@ async function safeBodyText(res) {
1221
1241
  }
1222
1242
  }
1223
1243
  function createFeedbackSink(options) {
1224
- let feedbackSessionId;
1244
+ let feedbackSessionId = options.initialSessionId || void 0;
1225
1245
  let firstPostInFlight = null;
1226
1246
  let generation = 0;
1227
1247
  const url = `${options.baseUrl}/sdk/feedback`;
@@ -1314,6 +1334,50 @@ function createFeedbackSink(options) {
1314
1334
  };
1315
1335
  }
1316
1336
 
1337
+ // src/analytics/suspended-session.ts
1338
+ var SUSPENDED_SESSION_KEY = "deepdots.analytics.suspended";
1339
+ function getSessionStorage() {
1340
+ try {
1341
+ return typeof window !== "undefined" && window.sessionStorage ? window.sessionStorage : null;
1342
+ } catch {
1343
+ return null;
1344
+ }
1345
+ }
1346
+ function writeSuspendedSession(storage, session) {
1347
+ try {
1348
+ storage.setItem(SUSPENDED_SESSION_KEY, JSON.stringify(session));
1349
+ return true;
1350
+ } catch {
1351
+ return false;
1352
+ }
1353
+ }
1354
+ function clearSuspendedSession(storage) {
1355
+ try {
1356
+ storage.removeItem(SUSPENDED_SESSION_KEY);
1357
+ } catch {
1358
+ }
1359
+ }
1360
+ function takeSuspendedSession(storage, match, now, windowMs) {
1361
+ let raw = null;
1362
+ try {
1363
+ raw = storage.getItem(SUSPENDED_SESSION_KEY);
1364
+ } catch {
1365
+ return null;
1366
+ }
1367
+ if (!raw) return null;
1368
+ clearSuspendedSession(storage);
1369
+ try {
1370
+ const s = JSON.parse(raw);
1371
+ if (typeof s.sessionId !== "string" || !s.sessionId) return null;
1372
+ if (typeof s.at !== "number" || now - s.at < 0 || now - s.at > windowMs) return null;
1373
+ if (s.integration !== match.integration) return null;
1374
+ if ((s.userId ?? null) !== match.userId) return null;
1375
+ return { sessionId: s.sessionId, integration: s.integration, userId: s.userId ?? null, at: s.at };
1376
+ } catch {
1377
+ return null;
1378
+ }
1379
+ }
1380
+
1317
1381
  // src/tracking/navigation-observer.ts
1318
1382
  var UUID_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-/i;
1319
1383
  function screenPath(href) {
@@ -1908,8 +1972,10 @@ var ANALYTICS_HEARTBEAT_MS = 5 * 6e4;
1908
1972
  var ANALYTICS_SESSION_TIMEOUT_MS = 30 * 6e4;
1909
1973
  var ANALYTICS_SERVER_IDLE_MS = 60 * 6e4;
1910
1974
  var ANALYTICS_SESSION_DISCARD_MS = ANALYTICS_SERVER_IDLE_MS - 10 * 6e4;
1975
+ var ANALYTICS_RESUME_WINDOW_MS = 6e4;
1911
1976
  var RESERVED_EVENT_PREFIX = "deepdots_";
1912
1977
  var CUSTOM_EVENT_PREFIX = "deepdots_event_";
1978
+ var MAX_PENDING_TRIGGERS = 20;
1913
1979
  var DeepdotsPopups = class {
1914
1980
  constructor() {
1915
1981
  this.config = null;
@@ -1923,6 +1989,14 @@ var DeepdotsPopups = class {
1923
1989
  // deprecated: mantenido para compatibilidad interna
1924
1990
  this.popupDefinitions = [];
1925
1991
  this.popupsLoaded = false;
1992
+ /**
1993
+ * `true` mientras el `GET /sdk/{apiKey}/popups` de `init()` está en vuelo. En esa ventana los
1994
+ * disparos del host (`triggerEvent`, `triggerSurvey`) no tienen contra qué evaluarse, así que
1995
+ * se guardan en `pendingTriggers` y se reproducen al cargar. Click, scroll, time y exit no
1996
+ * hacen falta: son listeners que `autoLaunch` instala ya con las definiciones cargadas.
1997
+ */
1998
+ this.awaitingPopups = false;
1999
+ this.pendingTriggers = [];
1926
2000
  this.pendingAutoLaunch = false;
1927
2001
  // Estado que alimenta las reglas de redisplay. Copia de trabajo en memoria; la fuente
1928
2002
  // duradera es el storage (`popup-state-store`), rehidratada en `init()` — sin eso, una
@@ -1977,6 +2051,13 @@ var DeepdotsPopups = class {
1977
2051
  this.lastEngagementAt = 0;
1978
2052
  /** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
1979
2053
  this.hiddenAt = null;
2054
+ /**
2055
+ * Sesión suspendida por un `pagehide` (ms epoch y `deepdots_user_id`); null si no hay.
2056
+ * `openSession` la reanuda si sigue a tiempo (ver ANALYTICS_RESUME_WINDOW_MS).
2057
+ */
2058
+ this.suspended = null;
2059
+ /** Integración de analytics del init: la sesión suspendida solo la hereda la misma. */
2060
+ this.analyticsIntegration = void 0;
1980
2061
  /**
1981
2062
  * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
1982
2063
  * verdad para el context de analytics Y para la segmentación por idioma de los popups.
@@ -2036,10 +2117,17 @@ var DeepdotsPopups = class {
2036
2117
  }
2037
2118
  }
2038
2119
  this.log("tracking \xB7 user_id:", this.tracking.getUserId(), "\xB7 new_user:", this.tracking.isNewUser(), "\xB7 enabled:", this.tracking.isTrackingEnabled());
2120
+ this.analyticsIntegration = config.analytics?.integration;
2121
+ const resumable = this.readResumableSession();
2122
+ if (resumable) {
2123
+ this.analyticsFeedbackSessionId = resumable.sessionId;
2124
+ this.suspended = { at: resumable.at, userId: resumable.userId };
2125
+ }
2039
2126
  const analyticsSink = config.analytics ? createFeedbackSink({
2040
2127
  baseUrl: this.baseUrl,
2041
2128
  keys: config.analytics,
2042
2129
  log: (...a) => this.log(...a),
2130
+ initialSessionId: resumable?.sessionId,
2043
2131
  onSessionId: (id) => {
2044
2132
  this.analyticsFeedbackSessionId = id;
2045
2133
  this.notifyFeedbackSession(id, "open");
@@ -2088,7 +2176,10 @@ var DeepdotsPopups = class {
2088
2176
  this.navObserver.install();
2089
2177
  this.engagement = new EngagementTracker();
2090
2178
  this.setupAnalyticsFlush();
2091
- this.openSession();
2179
+ this.openSession("cold_start");
2180
+ if (resumable && this.analyticsFeedbackSessionId === resumable.sessionId) {
2181
+ this.notifyFeedbackSession(resumable.sessionId, "open");
2182
+ }
2092
2183
  const pendingCrashes = this.crashReporter.drainPendingCrashes();
2093
2184
  if (this.tracking?.isTrackingEnabled()) {
2094
2185
  for (const rec of pendingCrashes) {
@@ -2099,8 +2190,10 @@ var DeepdotsPopups = class {
2099
2190
  this.log("SDK initialized", this.config);
2100
2191
  if (this.renderer.init) this.renderer.init();
2101
2192
  this.setupPopupContainer();
2193
+ this.awaitingPopups = Boolean(this.config.apiKey && this.baseUrl);
2102
2194
  this.fetchPopupsFromServer().then((defs) => {
2103
- this.popupDefinitions = this.validatePopupDefinitions(defs);
2195
+ if (!defs) this.dropPendingTriggers();
2196
+ this.popupDefinitions = this.validatePopupDefinitions(defs ?? []);
2104
2197
  this.popupsLoaded = true;
2105
2198
  this.log("Popups loaded from API", this.popupDefinitions);
2106
2199
  if (this.popupDefinitions.length) this.renderer.preload?.();
@@ -2109,6 +2202,7 @@ var DeepdotsPopups = class {
2109
2202
  if (this.pendingAutoLaunch) {
2110
2203
  this.startTriggers();
2111
2204
  }
2205
+ this.replayPendingTriggers();
2112
2206
  });
2113
2207
  }
2114
2208
  /** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
@@ -2140,7 +2234,7 @@ var DeepdotsPopups = class {
2140
2234
  setTrackingEnabled(enabled) {
2141
2235
  if (!enabled) this.closeSession("tracking_disabled");
2142
2236
  this.tracking?.setTrackingEnabled(enabled);
2143
- if (enabled) this.openSession();
2237
+ if (enabled) this.openSession("tracking_enabled");
2144
2238
  if (enabled) this.resolveGeo();
2145
2239
  this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
2146
2240
  }
@@ -2257,7 +2351,7 @@ var DeepdotsPopups = class {
2257
2351
  /** App a foreground (RN: AppState 'active'): reanuda el engagement time y abre sesión nueva. */
2258
2352
  onForeground() {
2259
2353
  this.engagement?.resume();
2260
- this.openSession();
2354
+ this.openSession("foreground");
2261
2355
  }
2262
2356
  /**
2263
2357
  * App a background (RN: AppState 'background'): FIN DE SESIÓN. Cierra pantalla actual
@@ -2305,7 +2399,7 @@ var DeepdotsPopups = class {
2305
2399
  this.answeredSurveys.clear();
2306
2400
  clearPopupState(storage);
2307
2401
  this.log("tracking \xB7 user_change \xB7 user_id:", this.tracking.getUserId());
2308
- this.openSession();
2402
+ this.openSession("user_change");
2309
2403
  }
2310
2404
  /**
2311
2405
  * Cierra la sesión actual: vuelca todo lo que quedaba abierto y envía el último lote
@@ -2331,15 +2425,28 @@ var DeepdotsPopups = class {
2331
2425
  this.log("tracking \xB7 session_end:", reason);
2332
2426
  }
2333
2427
  /**
2334
- * Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente:
2335
- * se llama en init(), al volver a foreground y al conceder el consentimiento.
2428
+ * Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente.
2429
+ * El `reason` dice por qué empieza (ver `SessionStartReason`):
2430
+ * - `cold_start`: init() (carga de la página / arranque de la app RN).
2431
+ * - `foreground`: onForeground() tras un cierre (RN).
2432
+ * - `tracking_enabled`: setTrackingEnabled(true).
2433
+ * - `user_change`: setUserId() o init() con otro userId.
2434
+ * - `idle_timeout`: la pestaña vuelve a verse tras más de 30 min oculta.
2435
+ * - `bfcache_restore`: vuelta desde la bfcache fuera de la ventana de reanudación.
2436
+ * Si hay una sesión suspendida a tiempo emite `deepdots_session_resume` en su lugar, sin reason.
2336
2437
  */
2337
- openSession() {
2438
+ openSession(reason) {
2338
2439
  if (this.sessionOpen) return;
2339
2440
  if (!this.tracking?.isTrackingEnabled()) return;
2441
+ const suspendedMs = this.takeSuspendedSession();
2340
2442
  this.sessionOpen = true;
2341
2443
  this.lastEngagementAt = Date.now();
2342
- this.track("deepdots_session_start", {});
2444
+ if (suspendedMs === null) {
2445
+ this.track("deepdots_session_start", { reason });
2446
+ } else {
2447
+ this.track("deepdots_session_resume", { suspended_ms: suspendedMs });
2448
+ this.log("tracking \xB7 session resumed after", suspendedMs, "ms:", this.analyticsFeedbackSessionId);
2449
+ }
2343
2450
  this.navObserver?.restart();
2344
2451
  if (this.isDocumentVisible()) this.engagement?.resume();
2345
2452
  }
@@ -2402,7 +2509,76 @@ var DeepdotsPopups = class {
2402
2509
  } else {
2403
2510
  this.discardSession();
2404
2511
  }
2405
- this.openSession();
2512
+ this.openSession("idle_timeout");
2513
+ }
2514
+ /**
2515
+ * `pagehide` en web: SUSPENDE la sesión en vez de cerrarla, porque el navegador lo lanza
2516
+ * igual al recargar que al cerrar la pestaña. Vuelca lo pendiente (page_view de la
2517
+ * pantalla, mini-services, engagement) con un `deepdots_session_suspend` y SIN
2518
+ * `completed`, y deja el registro en `sessionStorage` (ver `suspended-session.ts`):
2519
+ * - si la página vuelve a cargar en esta pestaña, `init()` sigue en el mismo registro;
2520
+ * - si la pestaña se cerró, el backend lo cierra a los pocos minutos sin otro lote.
2521
+ *
2522
+ * Devuelve false cuando no se puede suspender — aún no se conoce el registro (el primer
2523
+ * POST sigue en vuelo) o no hay `sessionStorage` — y entonces el caller cierra como antes.
2524
+ */
2525
+ suspendSession() {
2526
+ if (!this.sessionOpen || !this.tracking?.isTrackingEnabled()) return false;
2527
+ const sessionId = this.analyticsFeedbackSessionId;
2528
+ const integration = this.analyticsIntegration;
2529
+ const storage = getSessionStorage();
2530
+ if (!sessionId || !integration || !storage) return false;
2531
+ const at = Date.now();
2532
+ const userId = this.tracking.getUserId();
2533
+ if (!writeSuspendedSession(storage, { sessionId, integration, userId, at })) return false;
2534
+ this.sessionOpen = false;
2535
+ this.suspended = { at, userId };
2536
+ this.navObserver?.stop();
2537
+ this.navStarted = false;
2538
+ this.analytics?.exitAllMiniServices();
2539
+ this.flushEngagement();
2540
+ this.engagement?.pause();
2541
+ this.track("deepdots_session_suspend", { reason: "page_hide" });
2542
+ this.flushAnalytics({ final: true });
2543
+ this.log("tracking \xB7 session suspended:", sessionId);
2544
+ return true;
2545
+ }
2546
+ /**
2547
+ * Recoge la sesión que dejó suspendida la carga anterior de esta pestaña (ver
2548
+ * `suspendSession`). Solo con analytics configurado y el tracking activo: sin ellos no hay
2549
+ * registro en el que seguir.
2550
+ */
2551
+ readResumableSession() {
2552
+ const integration = this.analyticsIntegration;
2553
+ const storage = getSessionStorage();
2554
+ if (!integration || !storage || !this.tracking?.isTrackingEnabled()) return null;
2555
+ return takeSuspendedSession(
2556
+ storage,
2557
+ { integration, userId: this.tracking.getUserId() },
2558
+ Date.now(),
2559
+ ANALYTICS_RESUME_WINDOW_MS
2560
+ );
2561
+ }
2562
+ /**
2563
+ * Consume la sesión suspendida al abrir: los ms que estuvo suspendida si se puede seguir en
2564
+ * ella (mismo usuario, dentro de ANALYTICS_RESUME_WINDOW_MS); null si no había o ya no toca.
2565
+ * En ese caso se olvida su registro sin enviar nada: lo cierra el backend, y la sesión que
2566
+ * se abre ahora va a uno nuevo.
2567
+ */
2568
+ takeSuspendedSession() {
2569
+ const suspended = this.suspended;
2570
+ if (!suspended) return null;
2571
+ this.suspended = null;
2572
+ const storage = getSessionStorage();
2573
+ if (storage) clearSuspendedSession(storage);
2574
+ const suspendedMs = Math.max(0, Date.now() - suspended.at);
2575
+ if (this.analyticsFeedbackSessionId && suspendedMs <= ANALYTICS_RESUME_WINDOW_MS && suspended.userId === (this.tracking?.getUserId() ?? null)) {
2576
+ return suspendedMs;
2577
+ }
2578
+ this.analytics?.discardSession(this.analyticsIdentity());
2579
+ this.analyticsFeedbackSessionId = void 0;
2580
+ this.tracking?.setSessionId(null);
2581
+ return null;
2406
2582
  }
2407
2583
  /** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
2408
2584
  discardSession() {
@@ -2452,12 +2628,12 @@ var DeepdotsPopups = class {
2452
2628
  if (typeof document === "undefined" || typeof window === "undefined") return;
2453
2629
  window.addEventListener("pagehide", () => {
2454
2630
  clearInterval(this.analyticsFlushTimer);
2455
- this.closeSession("page_hide");
2631
+ if (!this.suspendSession()) this.closeSession("page_hide");
2456
2632
  });
2457
2633
  window.addEventListener("pageshow", (event) => {
2458
2634
  if (!event.persisted) return;
2459
2635
  this.startAnalyticsFlushTimer();
2460
- this.openSession();
2636
+ this.openSession("bfcache_restore");
2461
2637
  });
2462
2638
  document.addEventListener("visibilitychange", () => {
2463
2639
  if (document.visibilityState === "hidden") {
@@ -2520,6 +2696,7 @@ var DeepdotsPopups = class {
2520
2696
  }
2521
2697
  /** Lógica para evaluar condiciones antes de mostrar una encuesta */
2522
2698
  triggerSurvey(surveyId, popupId) {
2699
+ if (this.bufferIfAwaitingPopups({ kind: "survey", surveyId, popupId })) return;
2523
2700
  const def = this.findPopupDefinition(surveyId, popupId);
2524
2701
  if (!def) {
2525
2702
  this.debug("No popup definition for trigger", { surveyId, popupId });
@@ -2541,6 +2718,10 @@ var DeepdotsPopups = class {
2541
2718
  this.debug("Ignoring empty event trigger name");
2542
2719
  return;
2543
2720
  }
2721
+ if (this.bufferIfAwaitingPopups({ kind: "event", name: normalized })) return;
2722
+ this.evaluateEvent(normalized);
2723
+ }
2724
+ evaluateEvent(normalized) {
2544
2725
  const candidates = this.popupDefinitions.filter((def) => {
2545
2726
  return def.triggers.some((trigger) => {
2546
2727
  const triggerValue = String(trigger.value ?? "").trim();
@@ -2558,6 +2739,40 @@ var DeepdotsPopups = class {
2558
2739
  }
2559
2740
  this.showDefinition(matched);
2560
2741
  }
2742
+ /**
2743
+ * Guarda `trigger` si las definiciones aún no han llegado. Devuelve `true` si lo guardó (el
2744
+ * llamador no debe evaluarlo ahora) y `false` si no hay carga en vuelo (se evalúa ya, como
2745
+ * siempre: tras cargar, tras un fallo o sin apiKey).
2746
+ */
2747
+ bufferIfAwaitingPopups(trigger) {
2748
+ if (!this.awaitingPopups) return false;
2749
+ if (this.pendingTriggers.length >= MAX_PENDING_TRIGGERS) {
2750
+ const dropped = this.pendingTriggers.shift();
2751
+ this.debug("Pending trigger buffer full; dropped oldest", dropped);
2752
+ }
2753
+ this.pendingTriggers.push(trigger);
2754
+ this.debug("Popups not loaded yet; trigger buffered", trigger);
2755
+ return true;
2756
+ }
2757
+ /** La carga falló: sin definiciones no hay nada que reproducir, se descarta todo. */
2758
+ dropPendingTriggers() {
2759
+ const dropped = this.pendingTriggers.length;
2760
+ this.awaitingPopups = false;
2761
+ this.pendingTriggers = [];
2762
+ if (dropped > 0) this.debug("Popups failed to load; dropped pending triggers", dropped);
2763
+ }
2764
+ /** Reproduce en orden por la evaluación normal (segmentos, cooldowns y condiciones de AHORA). */
2765
+ replayPendingTriggers() {
2766
+ this.awaitingPopups = false;
2767
+ const replay = this.pendingTriggers;
2768
+ this.pendingTriggers = [];
2769
+ if (!replay.length) return;
2770
+ this.debug("Replaying triggers received before popups loaded", replay.length);
2771
+ replay.forEach((trigger) => {
2772
+ if (trigger.kind === "event") this.evaluateEvent(trigger.name);
2773
+ else this.triggerSurvey(trigger.surveyId, trigger.popupId);
2774
+ });
2775
+ }
2561
2776
  showDefinition(def) {
2562
2777
  this.log("Showing popup (definition)", def);
2563
2778
  this.surveyToPopupId.set(def.surveyId, def.id);
@@ -2828,7 +3043,7 @@ var DeepdotsPopups = class {
2828
3043
  } catch {
2829
3044
  }
2830
3045
  }
2831
- /** Fetch al servidor para obtener popups */
3046
+ /** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
2832
3047
  async fetchPopupsFromServer() {
2833
3048
  const apiKey = this.config?.apiKey;
2834
3049
  const baseUrl = this.baseUrl;
@@ -2844,19 +3059,19 @@ var DeepdotsPopups = class {
2844
3059
  const response = await fetch(endpoint);
2845
3060
  if (!response.ok) {
2846
3061
  this.log("Failed to fetch popups", response.status, response.statusText);
2847
- return [];
3062
+ return null;
2848
3063
  }
2849
3064
  const raw = await response.text();
2850
3065
  const parsed = JSON.parse(raw);
2851
3066
  if (!Array.isArray(parsed)) {
2852
3067
  this.log("Unexpected popups payload", parsed);
2853
- return [];
3068
+ return null;
2854
3069
  }
2855
3070
  this.debug("Fetched popups payload", parsed);
2856
3071
  return parsed;
2857
3072
  } catch (error) {
2858
3073
  this.log("Error fetching popups", error);
2859
- return [];
3074
+ return null;
2860
3075
  }
2861
3076
  }
2862
3077
  /** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
@@ -2976,7 +3191,7 @@ var DeepdotsPopups = class {
2976
3191
  }
2977
3192
  if (!this.popupContainer) return;
2978
3193
  const container = this.popupContainer;
2979
- void import("./renderPopup-424HR5LF.mjs").then(({ renderPopup }) => {
3194
+ void import("./renderPopup-KSZJ5AN4.mjs").then(({ renderPopup }) => {
2980
3195
  renderPopup(
2981
3196
  container,
2982
3197
  surveyId,