@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.
@@ -396,6 +396,14 @@ declare class DeepdotsPopups {
396
396
  private popupContainer;
397
397
  private popupDefinitions;
398
398
  private popupsLoaded;
399
+ /**
400
+ * `true` mientras el `GET /sdk/{apiKey}/popups` de `init()` está en vuelo. En esa ventana los
401
+ * disparos del host (`triggerEvent`, `triggerSurvey`) no tienen contra qué evaluarse, así que
402
+ * se guardan en `pendingTriggers` y se reproducen al cargar. Click, scroll, time y exit no
403
+ * hacen falta: son listeners que `autoLaunch` instala ya con las definiciones cargadas.
404
+ */
405
+ private awaitingPopups;
406
+ private pendingTriggers;
399
407
  private pendingAutoLaunch;
400
408
  private answeredSurveys;
401
409
  private surveyProgress;
@@ -444,6 +452,13 @@ declare class DeepdotsPopups {
444
452
  private lastEngagementAt;
445
453
  /** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
446
454
  private hiddenAt;
455
+ /**
456
+ * Sesión suspendida por un `pagehide` (ms epoch y `deepdots_user_id`); null si no hay.
457
+ * `openSession` la reanuda si sigue a tiempo (ver ANALYTICS_RESUME_WINDOW_MS).
458
+ */
459
+ private suspended;
460
+ /** Integración de analytics del init: la sesión suspendida solo la hereda la misma. */
461
+ private analyticsIntegration;
447
462
  /**
448
463
  * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
449
464
  * verdad para el context de analytics Y para la segmentación por idioma de los popups.
@@ -586,6 +601,31 @@ declare class DeepdotsPopups {
586
601
  * - Más: ya la cerró el backend → se descarta sin enviar nada.
587
602
  */
588
603
  private onDocumentVisible;
604
+ /**
605
+ * `pagehide` en web: SUSPENDE la sesión en vez de cerrarla, porque el navegador lo lanza
606
+ * igual al recargar que al cerrar la pestaña. Vuelca lo pendiente (page_view de la
607
+ * pantalla, mini-services, engagement) con un `deepdots_session_suspend` y SIN
608
+ * `completed`, y deja el registro en `sessionStorage` (ver `suspended-session.ts`):
609
+ * - si la página vuelve a cargar en esta pestaña, `init()` sigue en el mismo registro;
610
+ * - si la pestaña se cerró, el backend lo cierra a los pocos minutos sin otro lote.
611
+ *
612
+ * Devuelve false cuando no se puede suspender — aún no se conoce el registro (el primer
613
+ * POST sigue en vuelo) o no hay `sessionStorage` — y entonces el caller cierra como antes.
614
+ */
615
+ private suspendSession;
616
+ /**
617
+ * Recoge la sesión que dejó suspendida la carga anterior de esta pestaña (ver
618
+ * `suspendSession`). Solo con analytics configurado y el tracking activo: sin ellos no hay
619
+ * registro en el que seguir.
620
+ */
621
+ private readResumableSession;
622
+ /**
623
+ * Consume la sesión suspendida al abrir: los ms que estuvo suspendida si se puede seguir en
624
+ * ella (mismo usuario, dentro de ANALYTICS_RESUME_WINDOW_MS); null si no había o ya no toca.
625
+ * En ese caso se olvida su registro sin enviar nada: lo cierra el backend, y la sesión que
626
+ * se abre ahora va a uno nuevo.
627
+ */
628
+ private takeSuspendedSession;
589
629
  /** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
590
630
  private discardSession;
591
631
  /**
@@ -611,6 +651,17 @@ declare class DeepdotsPopups {
611
651
  triggerSurvey(surveyId: string, popupId?: string): void;
612
652
  /** Trigger popups configured with trigger.type = 'event' and matching trigger.value */
613
653
  triggerEvent(eventName: string): void;
654
+ private evaluateEvent;
655
+ /**
656
+ * Guarda `trigger` si las definiciones aún no han llegado. Devuelve `true` si lo guardó (el
657
+ * llamador no debe evaluarlo ahora) y `false` si no hay carga en vuelo (se evalúa ya, como
658
+ * siempre: tras cargar, tras un fallo o sin apiKey).
659
+ */
660
+ private bufferIfAwaitingPopups;
661
+ /** La carga falló: sin definiciones no hay nada que reproducir, se descarta todo. */
662
+ private dropPendingTriggers;
663
+ /** Reproduce en orden por la evaluación normal (segmentos, cooldowns y condiciones de AHORA). */
664
+ private replayPendingTriggers;
614
665
  private showDefinition;
615
666
  private shouldShow;
616
667
  private matchesSegmentsLang;
@@ -649,7 +700,7 @@ declare class DeepdotsPopups {
649
700
  private removeDeferredExitPopup;
650
701
  private getDeferredExitQueue;
651
702
  private setDeferredExitQueue;
652
- /** Fetch al servidor para obtener popups */
703
+ /** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
653
704
  private fetchPopupsFromServer;
654
705
  /** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
655
706
  private postContact;
@@ -396,6 +396,14 @@ declare class DeepdotsPopups {
396
396
  private popupContainer;
397
397
  private popupDefinitions;
398
398
  private popupsLoaded;
399
+ /**
400
+ * `true` mientras el `GET /sdk/{apiKey}/popups` de `init()` está en vuelo. En esa ventana los
401
+ * disparos del host (`triggerEvent`, `triggerSurvey`) no tienen contra qué evaluarse, así que
402
+ * se guardan en `pendingTriggers` y se reproducen al cargar. Click, scroll, time y exit no
403
+ * hacen falta: son listeners que `autoLaunch` instala ya con las definiciones cargadas.
404
+ */
405
+ private awaitingPopups;
406
+ private pendingTriggers;
399
407
  private pendingAutoLaunch;
400
408
  private answeredSurveys;
401
409
  private surveyProgress;
@@ -444,6 +452,13 @@ declare class DeepdotsPopups {
444
452
  private lastEngagementAt;
445
453
  /** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
446
454
  private hiddenAt;
455
+ /**
456
+ * Sesión suspendida por un `pagehide` (ms epoch y `deepdots_user_id`); null si no hay.
457
+ * `openSession` la reanuda si sigue a tiempo (ver ANALYTICS_RESUME_WINDOW_MS).
458
+ */
459
+ private suspended;
460
+ /** Integración de analytics del init: la sesión suspendida solo la hereda la misma. */
461
+ private analyticsIntegration;
447
462
  /**
448
463
  * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
449
464
  * verdad para el context de analytics Y para la segmentación por idioma de los popups.
@@ -586,6 +601,31 @@ declare class DeepdotsPopups {
586
601
  * - Más: ya la cerró el backend → se descarta sin enviar nada.
587
602
  */
588
603
  private onDocumentVisible;
604
+ /**
605
+ * `pagehide` en web: SUSPENDE la sesión en vez de cerrarla, porque el navegador lo lanza
606
+ * igual al recargar que al cerrar la pestaña. Vuelca lo pendiente (page_view de la
607
+ * pantalla, mini-services, engagement) con un `deepdots_session_suspend` y SIN
608
+ * `completed`, y deja el registro en `sessionStorage` (ver `suspended-session.ts`):
609
+ * - si la página vuelve a cargar en esta pestaña, `init()` sigue en el mismo registro;
610
+ * - si la pestaña se cerró, el backend lo cierra a los pocos minutos sin otro lote.
611
+ *
612
+ * Devuelve false cuando no se puede suspender — aún no se conoce el registro (el primer
613
+ * POST sigue en vuelo) o no hay `sessionStorage` — y entonces el caller cierra como antes.
614
+ */
615
+ private suspendSession;
616
+ /**
617
+ * Recoge la sesión que dejó suspendida la carga anterior de esta pestaña (ver
618
+ * `suspendSession`). Solo con analytics configurado y el tracking activo: sin ellos no hay
619
+ * registro en el que seguir.
620
+ */
621
+ private readResumableSession;
622
+ /**
623
+ * Consume la sesión suspendida al abrir: los ms que estuvo suspendida si se puede seguir en
624
+ * ella (mismo usuario, dentro de ANALYTICS_RESUME_WINDOW_MS); null si no había o ya no toca.
625
+ * En ese caso se olvida su registro sin enviar nada: lo cierra el backend, y la sesión que
626
+ * se abre ahora va a uno nuevo.
627
+ */
628
+ private takeSuspendedSession;
589
629
  /** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
590
630
  private discardSession;
591
631
  /**
@@ -611,6 +651,17 @@ declare class DeepdotsPopups {
611
651
  triggerSurvey(surveyId: string, popupId?: string): void;
612
652
  /** Trigger popups configured with trigger.type = 'event' and matching trigger.value */
613
653
  triggerEvent(eventName: string): void;
654
+ private evaluateEvent;
655
+ /**
656
+ * Guarda `trigger` si las definiciones aún no han llegado. Devuelve `true` si lo guardó (el
657
+ * llamador no debe evaluarlo ahora) y `false` si no hay carga en vuelo (se evalúa ya, como
658
+ * siempre: tras cargar, tras un fallo o sin apiKey).
659
+ */
660
+ private bufferIfAwaitingPopups;
661
+ /** La carga falló: sin definiciones no hay nada que reproducir, se descarta todo. */
662
+ private dropPendingTriggers;
663
+ /** Reproduce en orden por la evaluación normal (segmentos, cooldowns y condiciones de AHORA). */
664
+ private replayPendingTriggers;
614
665
  private showDefinition;
615
666
  private shouldShow;
616
667
  private matchesSegmentsLang;
@@ -649,7 +700,7 @@ declare class DeepdotsPopups {
649
700
  private removeDeferredExitPopup;
650
701
  private getDeferredExitQueue;
651
702
  private setDeferredExitQueue;
652
- /** Fetch al servidor para obtener popups */
703
+ /** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
653
704
  private fetchPopupsFromServer;
654
705
  /** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
655
706
  private postContact;
@@ -2894,6 +2894,9 @@ function ensureFontFace(family, url) {
2894
2894
  }
2895
2895
  if (el.textContent !== css) el.textContent = css;
2896
2896
  }
2897
+ function hasFinePointer() {
2898
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(pointer: fine)").matches;
2899
+ }
2897
2900
  async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId, options) {
2898
2901
  let surveyCompletedEmitted = false;
2899
2902
  let stylesInjected = false;
@@ -3324,6 +3327,13 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3324
3327
  if (pos.padding) container.style.padding = pos.padding;
3325
3328
  let revealed = false;
3326
3329
  let revealTimer = null;
3330
+ let surveyLoaded = false;
3331
+ let openFocusDone = false;
3332
+ function autofocusOnOpen() {
3333
+ if (openFocusDone || !revealed || !surveyLoaded) return;
3334
+ openFocusDone = true;
3335
+ if (hasFinePointer()) formInstance?.focusFirstQuestion?.();
3336
+ }
3327
3337
  function reveal() {
3328
3338
  if (revealed) return;
3329
3339
  revealed = true;
@@ -3334,6 +3344,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3334
3344
  if (!container.contains(popup)) return;
3335
3345
  container.style.visibility = "visible";
3336
3346
  container.style.pointerEvents = "";
3347
+ autofocusOnOpen();
3337
3348
  }
3338
3349
  function revealWhenPainted2() {
3339
3350
  if (revealed) return;
@@ -3386,7 +3397,11 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3386
3397
  addSuccessScreen: false,
3387
3398
  // Surveys multi-idioma: native pide a la API este idioma (el del init o el del
3388
3399
  // dispositivo) y, si el survey no lo tiene, la API sirve el idioma por defecto.
3389
- lang: options?.language
3400
+ lang: options?.language,
3401
+ // Foco en la primera pregunta de escribir tras Start, Siguiente o Atrás. Native lo
3402
+ // aplica después de afterSubmitEvent/onBackEvent, cuando `setLoading(false)` ya ha
3403
+ // quitado el `inert`. La apertura va aparte (ver autofocusOnOpen).
3404
+ autofocus: "navigation"
3390
3405
  };
3391
3406
  generateOptions.onLoadedEvent = ({ formData, lang }) => {
3392
3407
  const shownLang = typeof lang === "string" && lang.trim().length > 0 ? lang : void 0;
@@ -3441,12 +3456,14 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3441
3456
  updateButtons("in_progress_first");
3442
3457
  }
3443
3458
  updateProgress({ progress: formInstance.progress, total: formInstance.total });
3444
- emit("popup_clicked", surveyId, { action: "loaded" });
3445
3459
  setLoading(false);
3460
+ if (!surveyLoaded) {
3461
+ surveyLoaded = true;
3462
+ autofocusOnOpen();
3463
+ }
3446
3464
  };
3447
3465
  generateOptions.beforeSubmitEvent = () => {
3448
3466
  setLoading(true);
3449
- emit("popup_clicked", surveyId, { action: "before_submit" });
3450
3467
  };
3451
3468
  generateOptions.afterSubmitEvent = ({ error, completed, total, progress, followup }) => {
3452
3469
  const errText = error ? typeof error === "string" ? error : error.message ?? String(error) : "";
@@ -3455,13 +3472,12 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3455
3472
  if (errText.toLowerCase().includes("no response")) {
3456
3473
  errorHint.textContent = labels.errorRequired;
3457
3474
  errorHint.style.display = "block";
3458
- emit("popup_clicked", surveyId, { action: "validation_error_required" });
3459
3475
  updateNavButtons();
3460
3476
  return;
3461
3477
  }
3462
3478
  errorHint.textContent = labels.errorSubmit;
3463
3479
  errorHint.style.display = "block";
3464
- emit("popup_clicked", surveyId, { action: "submit_error", error: errText });
3480
+ sdkWarn("[Deepdots] survey submit error:", errText);
3465
3481
  return;
3466
3482
  }
3467
3483
  errorHint.style.display = "none";
@@ -3480,7 +3496,6 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3480
3496
  updateProgress({ progress, total, followup });
3481
3497
  };
3482
3498
  generateOptions.onBackEvent = ({ progress, error, followup }) => {
3483
- emit("popup_clicked", surveyId, { action: "back" });
3484
3499
  if (!error) pageDepth = Math.max(0, pageDepth - 1);
3485
3500
  updateNavButtons();
3486
3501
  updateProgress({ progress, total: formInstance.total, followup });
@@ -4104,11 +4119,25 @@ ${customCss}
4104
4119
  ${PRIMARY_COLOR_JS}
4105
4120
  ${BUSY_JS}
4106
4121
  ${REVEAL_JS}
4122
+ // Foco en la primera pregunta si es de escribir. Al navegar lo pone @magicfeedback/native
4123
+ // (autofocus:'navigation'); la apertura la decide el popup, que es quien sabe cu\xE1ndo se revela.
4124
+ // Necesita el popup revelado y el survey cargado, que llegan en cualquier orden, y solo con
4125
+ // rat\xF3n o trackpad: en t\xE1ctil sacar\xEDa el teclado sin que el usuario haya tocado nada.
4126
+ var ddRevealed=false, ddSurveyLoaded=false, ddOpenFocusDone=false;
4127
+ function ddAutofocusOnOpen(){
4128
+ if(ddOpenFocusDone || !ddRevealed || !ddSurveyLoaded){ return; }
4129
+ ddOpenFocusDone=true;
4130
+ var fine=typeof window.matchMedia==='function' && window.matchMedia('(pointer: fine)').matches;
4131
+ var f=window.DeepdotsForm;
4132
+ if(fine && f && typeof f.focusFirstQuestion==='function'){ f.focusFirstQuestion(); }
4133
+ }
4107
4134
  // El popup se ense\xF1a cuando el survey est\xE1 pintado; \`ready\` deja al host abrir su propio
4108
4135
  // Modal en ese momento en vez de montar un WebView en blanco.
4109
4136
  var ddReveal=ddCreateReveal(popup, function(){
4110
4137
  document.body.classList.add('dd-ready');
4111
4138
  emitJSON('ready');
4139
+ ddRevealed=true;
4140
+ ddAutofocusOnOpen();
4112
4141
  }, ${REVEAL_TIMEOUT_MS});
4113
4142
 
4114
4143
  function setLoading(isLoading){
@@ -4149,6 +4178,9 @@ ${REVEAL_JS}
4149
4178
  // Surveys multi-idioma: native pide a la API este idioma (el del init o el del
4150
4179
  // dispositivo); si el survey no lo tiene, la API sirve el idioma por defecto.
4151
4180
  lang:${initLangJson}||undefined,
4181
+ // Foco en la primera pregunta de escribir tras Start, Siguiente o Atr\xE1s. Las versiones
4182
+ // de native sin esta opci\xF3n la ignoran.
4183
+ autofocus:'navigation',
4152
4184
  onLoadedEvent:function(args){
4153
4185
  // Idioma en el que native muestra el survey: manda sobre el del init para que el
4154
4186
  // chrome no se quede en ingl\xE9s delante de un survey traducido. Native < 2.3 no lo
@@ -4208,6 +4240,8 @@ ${REVEAL_JS}
4208
4240
  updateProgress({ progress:form.progress, total:form.total, completed:false, followup:false });
4209
4241
  emitJSON('loaded');
4210
4242
  setLoading(false);
4243
+ // La primera carga es la apertura; las siguientes (tras Start) las enfoca native.
4244
+ if(!ddSurveyLoaded){ ddSurveyLoaded=true; ddAutofocusOnOpen(); }
4211
4245
  },
4212
4246
  beforeSubmitEvent:function(){
4213
4247
  setLoading(true);
@@ -4360,6 +4394,7 @@ var ReactNativePopupRenderer = class {
4360
4394
  this.emitReady(surveyId);
4361
4395
  break;
4362
4396
  case "loaded":
4397
+ break;
4363
4398
  case "before_submit":
4364
4399
  case "after_submit":
4365
4400
  case "back":
@@ -4738,7 +4773,7 @@ async function safeBodyText(res) {
4738
4773
  }
4739
4774
  }
4740
4775
  function createFeedbackSink(options) {
4741
- let feedbackSessionId;
4776
+ let feedbackSessionId = options.initialSessionId || void 0;
4742
4777
  let firstPostInFlight = null;
4743
4778
  let generation = 0;
4744
4779
  const url = `${options.baseUrl}/sdk/feedback`;
@@ -4831,6 +4866,50 @@ function createFeedbackSink(options) {
4831
4866
  };
4832
4867
  }
4833
4868
 
4869
+ // src/analytics/suspended-session.ts
4870
+ var SUSPENDED_SESSION_KEY = "deepdots.analytics.suspended";
4871
+ function getSessionStorage() {
4872
+ try {
4873
+ return typeof window !== "undefined" && window.sessionStorage ? window.sessionStorage : null;
4874
+ } catch {
4875
+ return null;
4876
+ }
4877
+ }
4878
+ function writeSuspendedSession(storage, session) {
4879
+ try {
4880
+ storage.setItem(SUSPENDED_SESSION_KEY, JSON.stringify(session));
4881
+ return true;
4882
+ } catch {
4883
+ return false;
4884
+ }
4885
+ }
4886
+ function clearSuspendedSession(storage) {
4887
+ try {
4888
+ storage.removeItem(SUSPENDED_SESSION_KEY);
4889
+ } catch {
4890
+ }
4891
+ }
4892
+ function takeSuspendedSession(storage, match, now, windowMs) {
4893
+ let raw = null;
4894
+ try {
4895
+ raw = storage.getItem(SUSPENDED_SESSION_KEY);
4896
+ } catch {
4897
+ return null;
4898
+ }
4899
+ if (!raw) return null;
4900
+ clearSuspendedSession(storage);
4901
+ try {
4902
+ const s = JSON.parse(raw);
4903
+ if (typeof s.sessionId !== "string" || !s.sessionId) return null;
4904
+ if (typeof s.at !== "number" || now - s.at < 0 || now - s.at > windowMs) return null;
4905
+ if (s.integration !== match.integration) return null;
4906
+ if ((s.userId ?? null) !== match.userId) return null;
4907
+ return { sessionId: s.sessionId, integration: s.integration, userId: s.userId ?? null, at: s.at };
4908
+ } catch {
4909
+ return null;
4910
+ }
4911
+ }
4912
+
4834
4913
  // src/tracking/navigation-observer.ts
4835
4914
  var UUID_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-/i;
4836
4915
  function screenPath(href) {
@@ -5429,8 +5508,10 @@ var ANALYTICS_HEARTBEAT_MS = 5 * 6e4;
5429
5508
  var ANALYTICS_SESSION_TIMEOUT_MS = 30 * 6e4;
5430
5509
  var ANALYTICS_SERVER_IDLE_MS = 60 * 6e4;
5431
5510
  var ANALYTICS_SESSION_DISCARD_MS = ANALYTICS_SERVER_IDLE_MS - 10 * 6e4;
5511
+ var ANALYTICS_RESUME_WINDOW_MS = 6e4;
5432
5512
  var RESERVED_EVENT_PREFIX = "deepdots_";
5433
5513
  var CUSTOM_EVENT_PREFIX = "deepdots_event_";
5514
+ var MAX_PENDING_TRIGGERS = 20;
5434
5515
  var DeepdotsPopups = class {
5435
5516
  constructor() {
5436
5517
  this.config = null;
@@ -5444,6 +5525,14 @@ var DeepdotsPopups = class {
5444
5525
  // deprecated: mantenido para compatibilidad interna
5445
5526
  this.popupDefinitions = [];
5446
5527
  this.popupsLoaded = false;
5528
+ /**
5529
+ * `true` mientras el `GET /sdk/{apiKey}/popups` de `init()` está en vuelo. En esa ventana los
5530
+ * disparos del host (`triggerEvent`, `triggerSurvey`) no tienen contra qué evaluarse, así que
5531
+ * se guardan en `pendingTriggers` y se reproducen al cargar. Click, scroll, time y exit no
5532
+ * hacen falta: son listeners que `autoLaunch` instala ya con las definiciones cargadas.
5533
+ */
5534
+ this.awaitingPopups = false;
5535
+ this.pendingTriggers = [];
5447
5536
  this.pendingAutoLaunch = false;
5448
5537
  // Estado que alimenta las reglas de redisplay. Copia de trabajo en memoria; la fuente
5449
5538
  // duradera es el storage (`popup-state-store`), rehidratada en `init()` — sin eso, una
@@ -5498,6 +5587,13 @@ var DeepdotsPopups = class {
5498
5587
  this.lastEngagementAt = 0;
5499
5588
  /** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
5500
5589
  this.hiddenAt = null;
5590
+ /**
5591
+ * Sesión suspendida por un `pagehide` (ms epoch y `deepdots_user_id`); null si no hay.
5592
+ * `openSession` la reanuda si sigue a tiempo (ver ANALYTICS_RESUME_WINDOW_MS).
5593
+ */
5594
+ this.suspended = null;
5595
+ /** Integración de analytics del init: la sesión suspendida solo la hereda la misma. */
5596
+ this.analyticsIntegration = void 0;
5501
5597
  /**
5502
5598
  * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
5503
5599
  * verdad para el context de analytics Y para la segmentación por idioma de los popups.
@@ -5557,10 +5653,17 @@ var DeepdotsPopups = class {
5557
5653
  }
5558
5654
  }
5559
5655
  this.log("tracking \xB7 user_id:", this.tracking.getUserId(), "\xB7 new_user:", this.tracking.isNewUser(), "\xB7 enabled:", this.tracking.isTrackingEnabled());
5656
+ this.analyticsIntegration = config.analytics?.integration;
5657
+ const resumable = this.readResumableSession();
5658
+ if (resumable) {
5659
+ this.analyticsFeedbackSessionId = resumable.sessionId;
5660
+ this.suspended = { at: resumable.at, userId: resumable.userId };
5661
+ }
5560
5662
  const analyticsSink = config.analytics ? createFeedbackSink({
5561
5663
  baseUrl: this.baseUrl,
5562
5664
  keys: config.analytics,
5563
5665
  log: (...a) => this.log(...a),
5666
+ initialSessionId: resumable?.sessionId,
5564
5667
  onSessionId: (id) => {
5565
5668
  this.analyticsFeedbackSessionId = id;
5566
5669
  this.notifyFeedbackSession(id, "open");
@@ -5610,6 +5713,9 @@ var DeepdotsPopups = class {
5610
5713
  this.engagement = new EngagementTracker();
5611
5714
  this.setupAnalyticsFlush();
5612
5715
  this.openSession();
5716
+ if (resumable && this.analyticsFeedbackSessionId === resumable.sessionId) {
5717
+ this.notifyFeedbackSession(resumable.sessionId, "open");
5718
+ }
5613
5719
  const pendingCrashes = this.crashReporter.drainPendingCrashes();
5614
5720
  if (this.tracking?.isTrackingEnabled()) {
5615
5721
  for (const rec of pendingCrashes) {
@@ -5620,8 +5726,10 @@ var DeepdotsPopups = class {
5620
5726
  this.log("SDK initialized", this.config);
5621
5727
  if (this.renderer.init) this.renderer.init();
5622
5728
  this.setupPopupContainer();
5729
+ this.awaitingPopups = Boolean(this.config.apiKey && this.baseUrl);
5623
5730
  this.fetchPopupsFromServer().then((defs) => {
5624
- this.popupDefinitions = this.validatePopupDefinitions(defs);
5731
+ if (!defs) this.dropPendingTriggers();
5732
+ this.popupDefinitions = this.validatePopupDefinitions(defs ?? []);
5625
5733
  this.popupsLoaded = true;
5626
5734
  this.log("Popups loaded from API", this.popupDefinitions);
5627
5735
  if (this.popupDefinitions.length) this.renderer.preload?.();
@@ -5630,6 +5738,7 @@ var DeepdotsPopups = class {
5630
5738
  if (this.pendingAutoLaunch) {
5631
5739
  this.startTriggers();
5632
5740
  }
5741
+ this.replayPendingTriggers();
5633
5742
  });
5634
5743
  }
5635
5744
  /** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
@@ -5858,9 +5967,15 @@ var DeepdotsPopups = class {
5858
5967
  openSession() {
5859
5968
  if (this.sessionOpen) return;
5860
5969
  if (!this.tracking?.isTrackingEnabled()) return;
5970
+ const suspendedMs = this.takeSuspendedSession();
5861
5971
  this.sessionOpen = true;
5862
5972
  this.lastEngagementAt = Date.now();
5863
- this.track("deepdots_session_start", {});
5973
+ if (suspendedMs === null) {
5974
+ this.track("deepdots_session_start", {});
5975
+ } else {
5976
+ this.track("deepdots_session_resume", { suspended_ms: suspendedMs });
5977
+ this.log("tracking \xB7 session resumed after", suspendedMs, "ms:", this.analyticsFeedbackSessionId);
5978
+ }
5864
5979
  this.navObserver?.restart();
5865
5980
  if (this.isDocumentVisible()) this.engagement?.resume();
5866
5981
  }
@@ -5925,6 +6040,75 @@ var DeepdotsPopups = class {
5925
6040
  }
5926
6041
  this.openSession();
5927
6042
  }
6043
+ /**
6044
+ * `pagehide` en web: SUSPENDE la sesión en vez de cerrarla, porque el navegador lo lanza
6045
+ * igual al recargar que al cerrar la pestaña. Vuelca lo pendiente (page_view de la
6046
+ * pantalla, mini-services, engagement) con un `deepdots_session_suspend` y SIN
6047
+ * `completed`, y deja el registro en `sessionStorage` (ver `suspended-session.ts`):
6048
+ * - si la página vuelve a cargar en esta pestaña, `init()` sigue en el mismo registro;
6049
+ * - si la pestaña se cerró, el backend lo cierra a los pocos minutos sin otro lote.
6050
+ *
6051
+ * Devuelve false cuando no se puede suspender — aún no se conoce el registro (el primer
6052
+ * POST sigue en vuelo) o no hay `sessionStorage` — y entonces el caller cierra como antes.
6053
+ */
6054
+ suspendSession() {
6055
+ if (!this.sessionOpen || !this.tracking?.isTrackingEnabled()) return false;
6056
+ const sessionId = this.analyticsFeedbackSessionId;
6057
+ const integration = this.analyticsIntegration;
6058
+ const storage = getSessionStorage();
6059
+ if (!sessionId || !integration || !storage) return false;
6060
+ const at = Date.now();
6061
+ const userId = this.tracking.getUserId();
6062
+ if (!writeSuspendedSession(storage, { sessionId, integration, userId, at })) return false;
6063
+ this.sessionOpen = false;
6064
+ this.suspended = { at, userId };
6065
+ this.navObserver?.stop();
6066
+ this.navStarted = false;
6067
+ this.analytics?.exitAllMiniServices();
6068
+ this.flushEngagement();
6069
+ this.engagement?.pause();
6070
+ this.track("deepdots_session_suspend", { reason: "page_hide" });
6071
+ this.flushAnalytics({ final: true });
6072
+ this.log("tracking \xB7 session suspended:", sessionId);
6073
+ return true;
6074
+ }
6075
+ /**
6076
+ * Recoge la sesión que dejó suspendida la carga anterior de esta pestaña (ver
6077
+ * `suspendSession`). Solo con analytics configurado y el tracking activo: sin ellos no hay
6078
+ * registro en el que seguir.
6079
+ */
6080
+ readResumableSession() {
6081
+ const integration = this.analyticsIntegration;
6082
+ const storage = getSessionStorage();
6083
+ if (!integration || !storage || !this.tracking?.isTrackingEnabled()) return null;
6084
+ return takeSuspendedSession(
6085
+ storage,
6086
+ { integration, userId: this.tracking.getUserId() },
6087
+ Date.now(),
6088
+ ANALYTICS_RESUME_WINDOW_MS
6089
+ );
6090
+ }
6091
+ /**
6092
+ * Consume la sesión suspendida al abrir: los ms que estuvo suspendida si se puede seguir en
6093
+ * ella (mismo usuario, dentro de ANALYTICS_RESUME_WINDOW_MS); null si no había o ya no toca.
6094
+ * En ese caso se olvida su registro sin enviar nada: lo cierra el backend, y la sesión que
6095
+ * se abre ahora va a uno nuevo.
6096
+ */
6097
+ takeSuspendedSession() {
6098
+ const suspended = this.suspended;
6099
+ if (!suspended) return null;
6100
+ this.suspended = null;
6101
+ const storage = getSessionStorage();
6102
+ if (storage) clearSuspendedSession(storage);
6103
+ const suspendedMs = Math.max(0, Date.now() - suspended.at);
6104
+ if (this.analyticsFeedbackSessionId && suspendedMs <= ANALYTICS_RESUME_WINDOW_MS && suspended.userId === (this.tracking?.getUserId() ?? null)) {
6105
+ return suspendedMs;
6106
+ }
6107
+ this.analytics?.discardSession(this.analyticsIdentity());
6108
+ this.analyticsFeedbackSessionId = void 0;
6109
+ this.tracking?.setSessionId(null);
6110
+ return null;
6111
+ }
5928
6112
  /** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
5929
6113
  discardSession() {
5930
6114
  this.sessionOpen = false;
@@ -5973,7 +6157,7 @@ var DeepdotsPopups = class {
5973
6157
  if (typeof document === "undefined" || typeof window === "undefined") return;
5974
6158
  window.addEventListener("pagehide", () => {
5975
6159
  clearInterval(this.analyticsFlushTimer);
5976
- this.closeSession("page_hide");
6160
+ if (!this.suspendSession()) this.closeSession("page_hide");
5977
6161
  });
5978
6162
  window.addEventListener("pageshow", (event) => {
5979
6163
  if (!event.persisted) return;
@@ -6041,6 +6225,7 @@ var DeepdotsPopups = class {
6041
6225
  }
6042
6226
  /** Lógica para evaluar condiciones antes de mostrar una encuesta */
6043
6227
  triggerSurvey(surveyId, popupId) {
6228
+ if (this.bufferIfAwaitingPopups({ kind: "survey", surveyId, popupId })) return;
6044
6229
  const def = this.findPopupDefinition(surveyId, popupId);
6045
6230
  if (!def) {
6046
6231
  this.debug("No popup definition for trigger", { surveyId, popupId });
@@ -6062,6 +6247,10 @@ var DeepdotsPopups = class {
6062
6247
  this.debug("Ignoring empty event trigger name");
6063
6248
  return;
6064
6249
  }
6250
+ if (this.bufferIfAwaitingPopups({ kind: "event", name: normalized })) return;
6251
+ this.evaluateEvent(normalized);
6252
+ }
6253
+ evaluateEvent(normalized) {
6065
6254
  const candidates = this.popupDefinitions.filter((def) => {
6066
6255
  return def.triggers.some((trigger) => {
6067
6256
  const triggerValue = String(trigger.value ?? "").trim();
@@ -6079,6 +6268,40 @@ var DeepdotsPopups = class {
6079
6268
  }
6080
6269
  this.showDefinition(matched);
6081
6270
  }
6271
+ /**
6272
+ * Guarda `trigger` si las definiciones aún no han llegado. Devuelve `true` si lo guardó (el
6273
+ * llamador no debe evaluarlo ahora) y `false` si no hay carga en vuelo (se evalúa ya, como
6274
+ * siempre: tras cargar, tras un fallo o sin apiKey).
6275
+ */
6276
+ bufferIfAwaitingPopups(trigger) {
6277
+ if (!this.awaitingPopups) return false;
6278
+ if (this.pendingTriggers.length >= MAX_PENDING_TRIGGERS) {
6279
+ const dropped = this.pendingTriggers.shift();
6280
+ this.debug("Pending trigger buffer full; dropped oldest", dropped);
6281
+ }
6282
+ this.pendingTriggers.push(trigger);
6283
+ this.debug("Popups not loaded yet; trigger buffered", trigger);
6284
+ return true;
6285
+ }
6286
+ /** La carga falló: sin definiciones no hay nada que reproducir, se descarta todo. */
6287
+ dropPendingTriggers() {
6288
+ const dropped = this.pendingTriggers.length;
6289
+ this.awaitingPopups = false;
6290
+ this.pendingTriggers = [];
6291
+ if (dropped > 0) this.debug("Popups failed to load; dropped pending triggers", dropped);
6292
+ }
6293
+ /** Reproduce en orden por la evaluación normal (segmentos, cooldowns y condiciones de AHORA). */
6294
+ replayPendingTriggers() {
6295
+ this.awaitingPopups = false;
6296
+ const replay = this.pendingTriggers;
6297
+ this.pendingTriggers = [];
6298
+ if (!replay.length) return;
6299
+ this.debug("Replaying triggers received before popups loaded", replay.length);
6300
+ replay.forEach((trigger) => {
6301
+ if (trigger.kind === "event") this.evaluateEvent(trigger.name);
6302
+ else this.triggerSurvey(trigger.surveyId, trigger.popupId);
6303
+ });
6304
+ }
6082
6305
  showDefinition(def) {
6083
6306
  this.log("Showing popup (definition)", def);
6084
6307
  this.surveyToPopupId.set(def.surveyId, def.id);
@@ -6349,7 +6572,7 @@ var DeepdotsPopups = class {
6349
6572
  } catch {
6350
6573
  }
6351
6574
  }
6352
- /** Fetch al servidor para obtener popups */
6575
+ /** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
6353
6576
  async fetchPopupsFromServer() {
6354
6577
  const apiKey = this.config?.apiKey;
6355
6578
  const baseUrl = this.baseUrl;
@@ -6365,19 +6588,19 @@ var DeepdotsPopups = class {
6365
6588
  const response = await fetch(endpoint);
6366
6589
  if (!response.ok) {
6367
6590
  this.log("Failed to fetch popups", response.status, response.statusText);
6368
- return [];
6591
+ return null;
6369
6592
  }
6370
6593
  const raw = await response.text();
6371
6594
  const parsed = JSON.parse(raw);
6372
6595
  if (!Array.isArray(parsed)) {
6373
6596
  this.log("Unexpected popups payload", parsed);
6374
- return [];
6597
+ return null;
6375
6598
  }
6376
6599
  this.debug("Fetched popups payload", parsed);
6377
6600
  return parsed;
6378
6601
  } catch (error) {
6379
6602
  this.log("Error fetching popups", error);
6380
- return [];
6603
+ return null;
6381
6604
  }
6382
6605
  }
6383
6606
  /** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
@@ -6801,6 +7024,7 @@ function DeepdotsProvider({ config, children }) {
6801
7024
  style: { flex: 1 },
6802
7025
  originWhitelist: ["*"],
6803
7026
  javaScriptEnabled: true,
7027
+ keyboardDisplayRequiresUserAction: false,
6804
7028
  source: { html: survey.html, baseUrl: "https://sdk.deepdots.com/" },
6805
7029
  onMessage: (e) => rendererRef.current?.handleMessage(e.nativeEvent.data)
6806
7030
  }