@magicfeedback/popup-sdk 1.10.0 → 1.11.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.js CHANGED
@@ -5631,6 +5631,9 @@ function ensureFontFace(family, url) {
5631
5631
  }
5632
5632
  if (el.textContent !== css) el.textContent = css;
5633
5633
  }
5634
+ function hasFinePointer() {
5635
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(pointer: fine)").matches;
5636
+ }
5634
5637
  async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId, options) {
5635
5638
  let surveyCompletedEmitted = false;
5636
5639
  let stylesInjected = false;
@@ -6061,6 +6064,13 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
6061
6064
  if (pos.padding) container.style.padding = pos.padding;
6062
6065
  let revealed = false;
6063
6066
  let revealTimer = null;
6067
+ let surveyLoaded = false;
6068
+ let openFocusDone = false;
6069
+ function autofocusOnOpen() {
6070
+ if (openFocusDone || !revealed || !surveyLoaded) return;
6071
+ openFocusDone = true;
6072
+ if (hasFinePointer()) formInstance?.focusFirstQuestion?.();
6073
+ }
6064
6074
  function reveal() {
6065
6075
  if (revealed) return;
6066
6076
  revealed = true;
@@ -6071,6 +6081,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
6071
6081
  if (!container.contains(popup)) return;
6072
6082
  container.style.visibility = "visible";
6073
6083
  container.style.pointerEvents = "";
6084
+ autofocusOnOpen();
6074
6085
  }
6075
6086
  function revealWhenPainted2() {
6076
6087
  if (revealed) return;
@@ -6123,7 +6134,11 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
6123
6134
  addSuccessScreen: false,
6124
6135
  // Surveys multi-idioma: native pide a la API este idioma (el del init o el del
6125
6136
  // dispositivo) y, si el survey no lo tiene, la API sirve el idioma por defecto.
6126
- lang: options?.language
6137
+ lang: options?.language,
6138
+ // Foco en la primera pregunta de escribir tras Start, Siguiente o Atrás. Native lo
6139
+ // aplica después de afterSubmitEvent/onBackEvent, cuando `setLoading(false)` ya ha
6140
+ // quitado el `inert`. La apertura va aparte (ver autofocusOnOpen).
6141
+ autofocus: "navigation"
6127
6142
  };
6128
6143
  generateOptions.onLoadedEvent = ({ formData, lang }) => {
6129
6144
  const shownLang = typeof lang === "string" && lang.trim().length > 0 ? lang : void 0;
@@ -6178,12 +6193,14 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
6178
6193
  updateButtons("in_progress_first");
6179
6194
  }
6180
6195
  updateProgress({ progress: formInstance.progress, total: formInstance.total });
6181
- emit("popup_clicked", surveyId, { action: "loaded" });
6182
6196
  setLoading(false);
6197
+ if (!surveyLoaded) {
6198
+ surveyLoaded = true;
6199
+ autofocusOnOpen();
6200
+ }
6183
6201
  };
6184
6202
  generateOptions.beforeSubmitEvent = () => {
6185
6203
  setLoading(true);
6186
- emit("popup_clicked", surveyId, { action: "before_submit" });
6187
6204
  };
6188
6205
  generateOptions.afterSubmitEvent = ({ error, completed, total, progress, followup }) => {
6189
6206
  const errText = error ? typeof error === "string" ? error : error.message ?? String(error) : "";
@@ -6192,13 +6209,12 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
6192
6209
  if (errText.toLowerCase().includes("no response")) {
6193
6210
  errorHint.textContent = labels.errorRequired;
6194
6211
  errorHint.style.display = "block";
6195
- emit("popup_clicked", surveyId, { action: "validation_error_required" });
6196
6212
  updateNavButtons();
6197
6213
  return;
6198
6214
  }
6199
6215
  errorHint.textContent = labels.errorSubmit;
6200
6216
  errorHint.style.display = "block";
6201
- emit("popup_clicked", surveyId, { action: "submit_error", error: errText });
6217
+ sdkWarn("[Deepdots] survey submit error:", errText);
6202
6218
  return;
6203
6219
  }
6204
6220
  errorHint.style.display = "none";
@@ -6217,7 +6233,6 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
6217
6233
  updateProgress({ progress, total, followup });
6218
6234
  };
6219
6235
  generateOptions.onBackEvent = ({ progress, error, followup }) => {
6220
- emit("popup_clicked", surveyId, { action: "back" });
6221
6236
  if (!error) pageDepth = Math.max(0, pageDepth - 1);
6222
6237
  updateNavButtons();
6223
6238
  updateProgress({ progress, total: formInstance.total, followup });
@@ -6864,11 +6879,25 @@ ${customCss}
6864
6879
  ${PRIMARY_COLOR_JS}
6865
6880
  ${BUSY_JS}
6866
6881
  ${REVEAL_JS}
6882
+ // Foco en la primera pregunta si es de escribir. Al navegar lo pone @magicfeedback/native
6883
+ // (autofocus:'navigation'); la apertura la decide el popup, que es quien sabe cu\xE1ndo se revela.
6884
+ // Necesita el popup revelado y el survey cargado, que llegan en cualquier orden, y solo con
6885
+ // rat\xF3n o trackpad: en t\xE1ctil sacar\xEDa el teclado sin que el usuario haya tocado nada.
6886
+ var ddRevealed=false, ddSurveyLoaded=false, ddOpenFocusDone=false;
6887
+ function ddAutofocusOnOpen(){
6888
+ if(ddOpenFocusDone || !ddRevealed || !ddSurveyLoaded){ return; }
6889
+ ddOpenFocusDone=true;
6890
+ var fine=typeof window.matchMedia==='function' && window.matchMedia('(pointer: fine)').matches;
6891
+ var f=window.DeepdotsForm;
6892
+ if(fine && f && typeof f.focusFirstQuestion==='function'){ f.focusFirstQuestion(); }
6893
+ }
6867
6894
  // El popup se ense\xF1a cuando el survey est\xE1 pintado; \`ready\` deja al host abrir su propio
6868
6895
  // Modal en ese momento en vez de montar un WebView en blanco.
6869
6896
  var ddReveal=ddCreateReveal(popup, function(){
6870
6897
  document.body.classList.add('dd-ready');
6871
6898
  emitJSON('ready');
6899
+ ddRevealed=true;
6900
+ ddAutofocusOnOpen();
6872
6901
  }, ${REVEAL_TIMEOUT_MS});
6873
6902
 
6874
6903
  function setLoading(isLoading){
@@ -6909,6 +6938,9 @@ ${REVEAL_JS}
6909
6938
  // Surveys multi-idioma: native pide a la API este idioma (el del init o el del
6910
6939
  // dispositivo); si el survey no lo tiene, la API sirve el idioma por defecto.
6911
6940
  lang:${initLangJson}||undefined,
6941
+ // Foco en la primera pregunta de escribir tras Start, Siguiente o Atr\xE1s. Las versiones
6942
+ // de native sin esta opci\xF3n la ignoran.
6943
+ autofocus:'navigation',
6912
6944
  onLoadedEvent:function(args){
6913
6945
  // Idioma en el que native muestra el survey: manda sobre el del init para que el
6914
6946
  // chrome no se quede en ingl\xE9s delante de un survey traducido. Native < 2.3 no lo
@@ -6968,6 +7000,8 @@ ${REVEAL_JS}
6968
7000
  updateProgress({ progress:form.progress, total:form.total, completed:false, followup:false });
6969
7001
  emitJSON('loaded');
6970
7002
  setLoading(false);
7003
+ // La primera carga es la apertura; las siguientes (tras Start) las enfoca native.
7004
+ if(!ddSurveyLoaded){ ddSurveyLoaded=true; ddAutofocusOnOpen(); }
6971
7005
  },
6972
7006
  beforeSubmitEvent:function(){
6973
7007
  setLoading(true);
@@ -7120,6 +7154,7 @@ var ReactNativePopupRenderer = class {
7120
7154
  this.emitReady(surveyId);
7121
7155
  break;
7122
7156
  case "loaded":
7157
+ break;
7123
7158
  case "before_submit":
7124
7159
  case "after_submit":
7125
7160
  case "back":
@@ -7498,7 +7533,7 @@ async function safeBodyText(res) {
7498
7533
  }
7499
7534
  }
7500
7535
  function createFeedbackSink(options) {
7501
- let feedbackSessionId;
7536
+ let feedbackSessionId = options.initialSessionId || void 0;
7502
7537
  let firstPostInFlight = null;
7503
7538
  let generation = 0;
7504
7539
  const url = `${options.baseUrl}/sdk/feedback`;
@@ -7591,6 +7626,50 @@ function createFeedbackSink(options) {
7591
7626
  };
7592
7627
  }
7593
7628
 
7629
+ // src/analytics/suspended-session.ts
7630
+ var SUSPENDED_SESSION_KEY = "deepdots.analytics.suspended";
7631
+ function getSessionStorage() {
7632
+ try {
7633
+ return typeof window !== "undefined" && window.sessionStorage ? window.sessionStorage : null;
7634
+ } catch {
7635
+ return null;
7636
+ }
7637
+ }
7638
+ function writeSuspendedSession(storage, session) {
7639
+ try {
7640
+ storage.setItem(SUSPENDED_SESSION_KEY, JSON.stringify(session));
7641
+ return true;
7642
+ } catch {
7643
+ return false;
7644
+ }
7645
+ }
7646
+ function clearSuspendedSession(storage) {
7647
+ try {
7648
+ storage.removeItem(SUSPENDED_SESSION_KEY);
7649
+ } catch {
7650
+ }
7651
+ }
7652
+ function takeSuspendedSession(storage, match, now, windowMs) {
7653
+ let raw = null;
7654
+ try {
7655
+ raw = storage.getItem(SUSPENDED_SESSION_KEY);
7656
+ } catch {
7657
+ return null;
7658
+ }
7659
+ if (!raw) return null;
7660
+ clearSuspendedSession(storage);
7661
+ try {
7662
+ const s = JSON.parse(raw);
7663
+ if (typeof s.sessionId !== "string" || !s.sessionId) return null;
7664
+ if (typeof s.at !== "number" || now - s.at < 0 || now - s.at > windowMs) return null;
7665
+ if (s.integration !== match.integration) return null;
7666
+ if ((s.userId ?? null) !== match.userId) return null;
7667
+ return { sessionId: s.sessionId, integration: s.integration, userId: s.userId ?? null, at: s.at };
7668
+ } catch {
7669
+ return null;
7670
+ }
7671
+ }
7672
+
7594
7673
  // src/tracking/navigation-observer.ts
7595
7674
  var UUID_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-/i;
7596
7675
  function screenPath(href) {
@@ -8189,8 +8268,10 @@ var ANALYTICS_HEARTBEAT_MS = 5 * 6e4;
8189
8268
  var ANALYTICS_SESSION_TIMEOUT_MS = 30 * 6e4;
8190
8269
  var ANALYTICS_SERVER_IDLE_MS = 60 * 6e4;
8191
8270
  var ANALYTICS_SESSION_DISCARD_MS = ANALYTICS_SERVER_IDLE_MS - 10 * 6e4;
8271
+ var ANALYTICS_RESUME_WINDOW_MS = 6e4;
8192
8272
  var RESERVED_EVENT_PREFIX = "deepdots_";
8193
8273
  var CUSTOM_EVENT_PREFIX = "deepdots_event_";
8274
+ var MAX_PENDING_TRIGGERS = 20;
8194
8275
  var DeepdotsPopups = class {
8195
8276
  constructor() {
8196
8277
  this.config = null;
@@ -8204,6 +8285,14 @@ var DeepdotsPopups = class {
8204
8285
  // deprecated: mantenido para compatibilidad interna
8205
8286
  this.popupDefinitions = [];
8206
8287
  this.popupsLoaded = false;
8288
+ /**
8289
+ * `true` mientras el `GET /sdk/{apiKey}/popups` de `init()` está en vuelo. En esa ventana los
8290
+ * disparos del host (`triggerEvent`, `triggerSurvey`) no tienen contra qué evaluarse, así que
8291
+ * se guardan en `pendingTriggers` y se reproducen al cargar. Click, scroll, time y exit no
8292
+ * hacen falta: son listeners que `autoLaunch` instala ya con las definiciones cargadas.
8293
+ */
8294
+ this.awaitingPopups = false;
8295
+ this.pendingTriggers = [];
8207
8296
  this.pendingAutoLaunch = false;
8208
8297
  // Estado que alimenta las reglas de redisplay. Copia de trabajo en memoria; la fuente
8209
8298
  // duradera es el storage (`popup-state-store`), rehidratada en `init()` — sin eso, una
@@ -8258,6 +8347,13 @@ var DeepdotsPopups = class {
8258
8347
  this.lastEngagementAt = 0;
8259
8348
  /** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
8260
8349
  this.hiddenAt = null;
8350
+ /**
8351
+ * Sesión suspendida por un `pagehide` (ms epoch y `deepdots_user_id`); null si no hay.
8352
+ * `openSession` la reanuda si sigue a tiempo (ver ANALYTICS_RESUME_WINDOW_MS).
8353
+ */
8354
+ this.suspended = null;
8355
+ /** Integración de analytics del init: la sesión suspendida solo la hereda la misma. */
8356
+ this.analyticsIntegration = void 0;
8261
8357
  /**
8262
8358
  * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
8263
8359
  * verdad para el context de analytics Y para la segmentación por idioma de los popups.
@@ -8317,10 +8413,17 @@ var DeepdotsPopups = class {
8317
8413
  }
8318
8414
  }
8319
8415
  this.log("tracking \xB7 user_id:", this.tracking.getUserId(), "\xB7 new_user:", this.tracking.isNewUser(), "\xB7 enabled:", this.tracking.isTrackingEnabled());
8416
+ this.analyticsIntegration = config.analytics?.integration;
8417
+ const resumable = this.readResumableSession();
8418
+ if (resumable) {
8419
+ this.analyticsFeedbackSessionId = resumable.sessionId;
8420
+ this.suspended = { at: resumable.at, userId: resumable.userId };
8421
+ }
8320
8422
  const analyticsSink = config.analytics ? createFeedbackSink({
8321
8423
  baseUrl: this.baseUrl,
8322
8424
  keys: config.analytics,
8323
8425
  log: (...a) => this.log(...a),
8426
+ initialSessionId: resumable?.sessionId,
8324
8427
  onSessionId: (id) => {
8325
8428
  this.analyticsFeedbackSessionId = id;
8326
8429
  this.notifyFeedbackSession(id, "open");
@@ -8370,6 +8473,9 @@ var DeepdotsPopups = class {
8370
8473
  this.engagement = new EngagementTracker();
8371
8474
  this.setupAnalyticsFlush();
8372
8475
  this.openSession();
8476
+ if (resumable && this.analyticsFeedbackSessionId === resumable.sessionId) {
8477
+ this.notifyFeedbackSession(resumable.sessionId, "open");
8478
+ }
8373
8479
  const pendingCrashes = this.crashReporter.drainPendingCrashes();
8374
8480
  if (this.tracking?.isTrackingEnabled()) {
8375
8481
  for (const rec of pendingCrashes) {
@@ -8380,8 +8486,10 @@ var DeepdotsPopups = class {
8380
8486
  this.log("SDK initialized", this.config);
8381
8487
  if (this.renderer.init) this.renderer.init();
8382
8488
  this.setupPopupContainer();
8489
+ this.awaitingPopups = Boolean(this.config.apiKey && this.baseUrl);
8383
8490
  this.fetchPopupsFromServer().then((defs) => {
8384
- this.popupDefinitions = this.validatePopupDefinitions(defs);
8491
+ if (!defs) this.dropPendingTriggers();
8492
+ this.popupDefinitions = this.validatePopupDefinitions(defs ?? []);
8385
8493
  this.popupsLoaded = true;
8386
8494
  this.log("Popups loaded from API", this.popupDefinitions);
8387
8495
  if (this.popupDefinitions.length) this.renderer.preload?.();
@@ -8390,6 +8498,7 @@ var DeepdotsPopups = class {
8390
8498
  if (this.pendingAutoLaunch) {
8391
8499
  this.startTriggers();
8392
8500
  }
8501
+ this.replayPendingTriggers();
8393
8502
  });
8394
8503
  }
8395
8504
  /** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
@@ -8618,9 +8727,15 @@ var DeepdotsPopups = class {
8618
8727
  openSession() {
8619
8728
  if (this.sessionOpen) return;
8620
8729
  if (!this.tracking?.isTrackingEnabled()) return;
8730
+ const suspendedMs = this.takeSuspendedSession();
8621
8731
  this.sessionOpen = true;
8622
8732
  this.lastEngagementAt = Date.now();
8623
- this.track("deepdots_session_start", {});
8733
+ if (suspendedMs === null) {
8734
+ this.track("deepdots_session_start", {});
8735
+ } else {
8736
+ this.track("deepdots_session_resume", { suspended_ms: suspendedMs });
8737
+ this.log("tracking \xB7 session resumed after", suspendedMs, "ms:", this.analyticsFeedbackSessionId);
8738
+ }
8624
8739
  this.navObserver?.restart();
8625
8740
  if (this.isDocumentVisible()) this.engagement?.resume();
8626
8741
  }
@@ -8685,6 +8800,75 @@ var DeepdotsPopups = class {
8685
8800
  }
8686
8801
  this.openSession();
8687
8802
  }
8803
+ /**
8804
+ * `pagehide` en web: SUSPENDE la sesión en vez de cerrarla, porque el navegador lo lanza
8805
+ * igual al recargar que al cerrar la pestaña. Vuelca lo pendiente (page_view de la
8806
+ * pantalla, mini-services, engagement) con un `deepdots_session_suspend` y SIN
8807
+ * `completed`, y deja el registro en `sessionStorage` (ver `suspended-session.ts`):
8808
+ * - si la página vuelve a cargar en esta pestaña, `init()` sigue en el mismo registro;
8809
+ * - si la pestaña se cerró, el backend lo cierra a los pocos minutos sin otro lote.
8810
+ *
8811
+ * Devuelve false cuando no se puede suspender — aún no se conoce el registro (el primer
8812
+ * POST sigue en vuelo) o no hay `sessionStorage` — y entonces el caller cierra como antes.
8813
+ */
8814
+ suspendSession() {
8815
+ if (!this.sessionOpen || !this.tracking?.isTrackingEnabled()) return false;
8816
+ const sessionId = this.analyticsFeedbackSessionId;
8817
+ const integration = this.analyticsIntegration;
8818
+ const storage = getSessionStorage();
8819
+ if (!sessionId || !integration || !storage) return false;
8820
+ const at = Date.now();
8821
+ const userId = this.tracking.getUserId();
8822
+ if (!writeSuspendedSession(storage, { sessionId, integration, userId, at })) return false;
8823
+ this.sessionOpen = false;
8824
+ this.suspended = { at, userId };
8825
+ this.navObserver?.stop();
8826
+ this.navStarted = false;
8827
+ this.analytics?.exitAllMiniServices();
8828
+ this.flushEngagement();
8829
+ this.engagement?.pause();
8830
+ this.track("deepdots_session_suspend", { reason: "page_hide" });
8831
+ this.flushAnalytics({ final: true });
8832
+ this.log("tracking \xB7 session suspended:", sessionId);
8833
+ return true;
8834
+ }
8835
+ /**
8836
+ * Recoge la sesión que dejó suspendida la carga anterior de esta pestaña (ver
8837
+ * `suspendSession`). Solo con analytics configurado y el tracking activo: sin ellos no hay
8838
+ * registro en el que seguir.
8839
+ */
8840
+ readResumableSession() {
8841
+ const integration = this.analyticsIntegration;
8842
+ const storage = getSessionStorage();
8843
+ if (!integration || !storage || !this.tracking?.isTrackingEnabled()) return null;
8844
+ return takeSuspendedSession(
8845
+ storage,
8846
+ { integration, userId: this.tracking.getUserId() },
8847
+ Date.now(),
8848
+ ANALYTICS_RESUME_WINDOW_MS
8849
+ );
8850
+ }
8851
+ /**
8852
+ * Consume la sesión suspendida al abrir: los ms que estuvo suspendida si se puede seguir en
8853
+ * ella (mismo usuario, dentro de ANALYTICS_RESUME_WINDOW_MS); null si no había o ya no toca.
8854
+ * En ese caso se olvida su registro sin enviar nada: lo cierra el backend, y la sesión que
8855
+ * se abre ahora va a uno nuevo.
8856
+ */
8857
+ takeSuspendedSession() {
8858
+ const suspended = this.suspended;
8859
+ if (!suspended) return null;
8860
+ this.suspended = null;
8861
+ const storage = getSessionStorage();
8862
+ if (storage) clearSuspendedSession(storage);
8863
+ const suspendedMs = Math.max(0, Date.now() - suspended.at);
8864
+ if (this.analyticsFeedbackSessionId && suspendedMs <= ANALYTICS_RESUME_WINDOW_MS && suspended.userId === (this.tracking?.getUserId() ?? null)) {
8865
+ return suspendedMs;
8866
+ }
8867
+ this.analytics?.discardSession(this.analyticsIdentity());
8868
+ this.analyticsFeedbackSessionId = void 0;
8869
+ this.tracking?.setSessionId(null);
8870
+ return null;
8871
+ }
8688
8872
  /** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
8689
8873
  discardSession() {
8690
8874
  this.sessionOpen = false;
@@ -8733,7 +8917,7 @@ var DeepdotsPopups = class {
8733
8917
  if (typeof document === "undefined" || typeof window === "undefined") return;
8734
8918
  window.addEventListener("pagehide", () => {
8735
8919
  clearInterval(this.analyticsFlushTimer);
8736
- this.closeSession("page_hide");
8920
+ if (!this.suspendSession()) this.closeSession("page_hide");
8737
8921
  });
8738
8922
  window.addEventListener("pageshow", (event) => {
8739
8923
  if (!event.persisted) return;
@@ -8801,6 +8985,7 @@ var DeepdotsPopups = class {
8801
8985
  }
8802
8986
  /** Lógica para evaluar condiciones antes de mostrar una encuesta */
8803
8987
  triggerSurvey(surveyId, popupId) {
8988
+ if (this.bufferIfAwaitingPopups({ kind: "survey", surveyId, popupId })) return;
8804
8989
  const def = this.findPopupDefinition(surveyId, popupId);
8805
8990
  if (!def) {
8806
8991
  this.debug("No popup definition for trigger", { surveyId, popupId });
@@ -8822,6 +9007,10 @@ var DeepdotsPopups = class {
8822
9007
  this.debug("Ignoring empty event trigger name");
8823
9008
  return;
8824
9009
  }
9010
+ if (this.bufferIfAwaitingPopups({ kind: "event", name: normalized })) return;
9011
+ this.evaluateEvent(normalized);
9012
+ }
9013
+ evaluateEvent(normalized) {
8825
9014
  const candidates = this.popupDefinitions.filter((def) => {
8826
9015
  return def.triggers.some((trigger) => {
8827
9016
  const triggerValue = String(trigger.value ?? "").trim();
@@ -8839,6 +9028,40 @@ var DeepdotsPopups = class {
8839
9028
  }
8840
9029
  this.showDefinition(matched);
8841
9030
  }
9031
+ /**
9032
+ * Guarda `trigger` si las definiciones aún no han llegado. Devuelve `true` si lo guardó (el
9033
+ * llamador no debe evaluarlo ahora) y `false` si no hay carga en vuelo (se evalúa ya, como
9034
+ * siempre: tras cargar, tras un fallo o sin apiKey).
9035
+ */
9036
+ bufferIfAwaitingPopups(trigger) {
9037
+ if (!this.awaitingPopups) return false;
9038
+ if (this.pendingTriggers.length >= MAX_PENDING_TRIGGERS) {
9039
+ const dropped = this.pendingTriggers.shift();
9040
+ this.debug("Pending trigger buffer full; dropped oldest", dropped);
9041
+ }
9042
+ this.pendingTriggers.push(trigger);
9043
+ this.debug("Popups not loaded yet; trigger buffered", trigger);
9044
+ return true;
9045
+ }
9046
+ /** La carga falló: sin definiciones no hay nada que reproducir, se descarta todo. */
9047
+ dropPendingTriggers() {
9048
+ const dropped = this.pendingTriggers.length;
9049
+ this.awaitingPopups = false;
9050
+ this.pendingTriggers = [];
9051
+ if (dropped > 0) this.debug("Popups failed to load; dropped pending triggers", dropped);
9052
+ }
9053
+ /** Reproduce en orden por la evaluación normal (segmentos, cooldowns y condiciones de AHORA). */
9054
+ replayPendingTriggers() {
9055
+ this.awaitingPopups = false;
9056
+ const replay = this.pendingTriggers;
9057
+ this.pendingTriggers = [];
9058
+ if (!replay.length) return;
9059
+ this.debug("Replaying triggers received before popups loaded", replay.length);
9060
+ replay.forEach((trigger) => {
9061
+ if (trigger.kind === "event") this.evaluateEvent(trigger.name);
9062
+ else this.triggerSurvey(trigger.surveyId, trigger.popupId);
9063
+ });
9064
+ }
8842
9065
  showDefinition(def) {
8843
9066
  this.log("Showing popup (definition)", def);
8844
9067
  this.surveyToPopupId.set(def.surveyId, def.id);
@@ -9109,7 +9332,7 @@ var DeepdotsPopups = class {
9109
9332
  } catch {
9110
9333
  }
9111
9334
  }
9112
- /** Fetch al servidor para obtener popups */
9335
+ /** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
9113
9336
  async fetchPopupsFromServer() {
9114
9337
  const apiKey = this.config?.apiKey;
9115
9338
  const baseUrl = this.baseUrl;
@@ -9125,19 +9348,19 @@ var DeepdotsPopups = class {
9125
9348
  const response = await fetch(endpoint);
9126
9349
  if (!response.ok) {
9127
9350
  this.log("Failed to fetch popups", response.status, response.statusText);
9128
- return [];
9351
+ return null;
9129
9352
  }
9130
9353
  const raw = await response.text();
9131
9354
  const parsed = JSON.parse(raw);
9132
9355
  if (!Array.isArray(parsed)) {
9133
9356
  this.log("Unexpected popups payload", parsed);
9134
- return [];
9357
+ return null;
9135
9358
  }
9136
9359
  this.debug("Fetched popups payload", parsed);
9137
9360
  return parsed;
9138
9361
  } catch (error) {
9139
9362
  this.log("Error fetching popups", error);
9140
- return [];
9363
+ return null;
9141
9364
  }
9142
9365
  }
9143
9366
  /** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */