@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.
@@ -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");
@@ -5609,7 +5712,10 @@ var DeepdotsPopups = class {
5609
5712
  this.navObserver.install();
5610
5713
  this.engagement = new EngagementTracker();
5611
5714
  this.setupAnalyticsFlush();
5612
- this.openSession();
5715
+ this.openSession("cold_start");
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. */
@@ -5661,7 +5770,7 @@ var DeepdotsPopups = class {
5661
5770
  setTrackingEnabled(enabled) {
5662
5771
  if (!enabled) this.closeSession("tracking_disabled");
5663
5772
  this.tracking?.setTrackingEnabled(enabled);
5664
- if (enabled) this.openSession();
5773
+ if (enabled) this.openSession("tracking_enabled");
5665
5774
  if (enabled) this.resolveGeo();
5666
5775
  this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
5667
5776
  }
@@ -5778,7 +5887,7 @@ var DeepdotsPopups = class {
5778
5887
  /** App a foreground (RN: AppState 'active'): reanuda el engagement time y abre sesión nueva. */
5779
5888
  onForeground() {
5780
5889
  this.engagement?.resume();
5781
- this.openSession();
5890
+ this.openSession("foreground");
5782
5891
  }
5783
5892
  /**
5784
5893
  * App a background (RN: AppState 'background'): FIN DE SESIÓN. Cierra pantalla actual
@@ -5826,7 +5935,7 @@ var DeepdotsPopups = class {
5826
5935
  this.answeredSurveys.clear();
5827
5936
  clearPopupState(storage);
5828
5937
  this.log("tracking \xB7 user_change \xB7 user_id:", this.tracking.getUserId());
5829
- this.openSession();
5938
+ this.openSession("user_change");
5830
5939
  }
5831
5940
  /**
5832
5941
  * Cierra la sesión actual: vuelca todo lo que quedaba abierto y envía el último lote
@@ -5852,15 +5961,28 @@ var DeepdotsPopups = class {
5852
5961
  this.log("tracking \xB7 session_end:", reason);
5853
5962
  }
5854
5963
  /**
5855
- * Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente:
5856
- * se llama en init(), al volver a foreground y al conceder el consentimiento.
5964
+ * Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente.
5965
+ * El `reason` dice por qué empieza (ver `SessionStartReason`):
5966
+ * - `cold_start`: init() (carga de la página / arranque de la app RN).
5967
+ * - `foreground`: onForeground() tras un cierre (RN).
5968
+ * - `tracking_enabled`: setTrackingEnabled(true).
5969
+ * - `user_change`: setUserId() o init() con otro userId.
5970
+ * - `idle_timeout`: la pestaña vuelve a verse tras más de 30 min oculta.
5971
+ * - `bfcache_restore`: vuelta desde la bfcache fuera de la ventana de reanudación.
5972
+ * Si hay una sesión suspendida a tiempo emite `deepdots_session_resume` en su lugar, sin reason.
5857
5973
  */
5858
- openSession() {
5974
+ openSession(reason) {
5859
5975
  if (this.sessionOpen) return;
5860
5976
  if (!this.tracking?.isTrackingEnabled()) return;
5977
+ const suspendedMs = this.takeSuspendedSession();
5861
5978
  this.sessionOpen = true;
5862
5979
  this.lastEngagementAt = Date.now();
5863
- this.track("deepdots_session_start", {});
5980
+ if (suspendedMs === null) {
5981
+ this.track("deepdots_session_start", { reason });
5982
+ } else {
5983
+ this.track("deepdots_session_resume", { suspended_ms: suspendedMs });
5984
+ this.log("tracking \xB7 session resumed after", suspendedMs, "ms:", this.analyticsFeedbackSessionId);
5985
+ }
5864
5986
  this.navObserver?.restart();
5865
5987
  if (this.isDocumentVisible()) this.engagement?.resume();
5866
5988
  }
@@ -5923,7 +6045,76 @@ var DeepdotsPopups = class {
5923
6045
  } else {
5924
6046
  this.discardSession();
5925
6047
  }
5926
- this.openSession();
6048
+ this.openSession("idle_timeout");
6049
+ }
6050
+ /**
6051
+ * `pagehide` en web: SUSPENDE la sesión en vez de cerrarla, porque el navegador lo lanza
6052
+ * igual al recargar que al cerrar la pestaña. Vuelca lo pendiente (page_view de la
6053
+ * pantalla, mini-services, engagement) con un `deepdots_session_suspend` y SIN
6054
+ * `completed`, y deja el registro en `sessionStorage` (ver `suspended-session.ts`):
6055
+ * - si la página vuelve a cargar en esta pestaña, `init()` sigue en el mismo registro;
6056
+ * - si la pestaña se cerró, el backend lo cierra a los pocos minutos sin otro lote.
6057
+ *
6058
+ * Devuelve false cuando no se puede suspender — aún no se conoce el registro (el primer
6059
+ * POST sigue en vuelo) o no hay `sessionStorage` — y entonces el caller cierra como antes.
6060
+ */
6061
+ suspendSession() {
6062
+ if (!this.sessionOpen || !this.tracking?.isTrackingEnabled()) return false;
6063
+ const sessionId = this.analyticsFeedbackSessionId;
6064
+ const integration = this.analyticsIntegration;
6065
+ const storage = getSessionStorage();
6066
+ if (!sessionId || !integration || !storage) return false;
6067
+ const at = Date.now();
6068
+ const userId = this.tracking.getUserId();
6069
+ if (!writeSuspendedSession(storage, { sessionId, integration, userId, at })) return false;
6070
+ this.sessionOpen = false;
6071
+ this.suspended = { at, userId };
6072
+ this.navObserver?.stop();
6073
+ this.navStarted = false;
6074
+ this.analytics?.exitAllMiniServices();
6075
+ this.flushEngagement();
6076
+ this.engagement?.pause();
6077
+ this.track("deepdots_session_suspend", { reason: "page_hide" });
6078
+ this.flushAnalytics({ final: true });
6079
+ this.log("tracking \xB7 session suspended:", sessionId);
6080
+ return true;
6081
+ }
6082
+ /**
6083
+ * Recoge la sesión que dejó suspendida la carga anterior de esta pestaña (ver
6084
+ * `suspendSession`). Solo con analytics configurado y el tracking activo: sin ellos no hay
6085
+ * registro en el que seguir.
6086
+ */
6087
+ readResumableSession() {
6088
+ const integration = this.analyticsIntegration;
6089
+ const storage = getSessionStorage();
6090
+ if (!integration || !storage || !this.tracking?.isTrackingEnabled()) return null;
6091
+ return takeSuspendedSession(
6092
+ storage,
6093
+ { integration, userId: this.tracking.getUserId() },
6094
+ Date.now(),
6095
+ ANALYTICS_RESUME_WINDOW_MS
6096
+ );
6097
+ }
6098
+ /**
6099
+ * Consume la sesión suspendida al abrir: los ms que estuvo suspendida si se puede seguir en
6100
+ * ella (mismo usuario, dentro de ANALYTICS_RESUME_WINDOW_MS); null si no había o ya no toca.
6101
+ * En ese caso se olvida su registro sin enviar nada: lo cierra el backend, y la sesión que
6102
+ * se abre ahora va a uno nuevo.
6103
+ */
6104
+ takeSuspendedSession() {
6105
+ const suspended = this.suspended;
6106
+ if (!suspended) return null;
6107
+ this.suspended = null;
6108
+ const storage = getSessionStorage();
6109
+ if (storage) clearSuspendedSession(storage);
6110
+ const suspendedMs = Math.max(0, Date.now() - suspended.at);
6111
+ if (this.analyticsFeedbackSessionId && suspendedMs <= ANALYTICS_RESUME_WINDOW_MS && suspended.userId === (this.tracking?.getUserId() ?? null)) {
6112
+ return suspendedMs;
6113
+ }
6114
+ this.analytics?.discardSession(this.analyticsIdentity());
6115
+ this.analyticsFeedbackSessionId = void 0;
6116
+ this.tracking?.setSessionId(null);
6117
+ return null;
5927
6118
  }
5928
6119
  /** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
5929
6120
  discardSession() {
@@ -5973,12 +6164,12 @@ var DeepdotsPopups = class {
5973
6164
  if (typeof document === "undefined" || typeof window === "undefined") return;
5974
6165
  window.addEventListener("pagehide", () => {
5975
6166
  clearInterval(this.analyticsFlushTimer);
5976
- this.closeSession("page_hide");
6167
+ if (!this.suspendSession()) this.closeSession("page_hide");
5977
6168
  });
5978
6169
  window.addEventListener("pageshow", (event) => {
5979
6170
  if (!event.persisted) return;
5980
6171
  this.startAnalyticsFlushTimer();
5981
- this.openSession();
6172
+ this.openSession("bfcache_restore");
5982
6173
  });
5983
6174
  document.addEventListener("visibilitychange", () => {
5984
6175
  if (document.visibilityState === "hidden") {
@@ -6041,6 +6232,7 @@ var DeepdotsPopups = class {
6041
6232
  }
6042
6233
  /** Lógica para evaluar condiciones antes de mostrar una encuesta */
6043
6234
  triggerSurvey(surveyId, popupId) {
6235
+ if (this.bufferIfAwaitingPopups({ kind: "survey", surveyId, popupId })) return;
6044
6236
  const def = this.findPopupDefinition(surveyId, popupId);
6045
6237
  if (!def) {
6046
6238
  this.debug("No popup definition for trigger", { surveyId, popupId });
@@ -6062,6 +6254,10 @@ var DeepdotsPopups = class {
6062
6254
  this.debug("Ignoring empty event trigger name");
6063
6255
  return;
6064
6256
  }
6257
+ if (this.bufferIfAwaitingPopups({ kind: "event", name: normalized })) return;
6258
+ this.evaluateEvent(normalized);
6259
+ }
6260
+ evaluateEvent(normalized) {
6065
6261
  const candidates = this.popupDefinitions.filter((def) => {
6066
6262
  return def.triggers.some((trigger) => {
6067
6263
  const triggerValue = String(trigger.value ?? "").trim();
@@ -6079,6 +6275,40 @@ var DeepdotsPopups = class {
6079
6275
  }
6080
6276
  this.showDefinition(matched);
6081
6277
  }
6278
+ /**
6279
+ * Guarda `trigger` si las definiciones aún no han llegado. Devuelve `true` si lo guardó (el
6280
+ * llamador no debe evaluarlo ahora) y `false` si no hay carga en vuelo (se evalúa ya, como
6281
+ * siempre: tras cargar, tras un fallo o sin apiKey).
6282
+ */
6283
+ bufferIfAwaitingPopups(trigger) {
6284
+ if (!this.awaitingPopups) return false;
6285
+ if (this.pendingTriggers.length >= MAX_PENDING_TRIGGERS) {
6286
+ const dropped = this.pendingTriggers.shift();
6287
+ this.debug("Pending trigger buffer full; dropped oldest", dropped);
6288
+ }
6289
+ this.pendingTriggers.push(trigger);
6290
+ this.debug("Popups not loaded yet; trigger buffered", trigger);
6291
+ return true;
6292
+ }
6293
+ /** La carga falló: sin definiciones no hay nada que reproducir, se descarta todo. */
6294
+ dropPendingTriggers() {
6295
+ const dropped = this.pendingTriggers.length;
6296
+ this.awaitingPopups = false;
6297
+ this.pendingTriggers = [];
6298
+ if (dropped > 0) this.debug("Popups failed to load; dropped pending triggers", dropped);
6299
+ }
6300
+ /** Reproduce en orden por la evaluación normal (segmentos, cooldowns y condiciones de AHORA). */
6301
+ replayPendingTriggers() {
6302
+ this.awaitingPopups = false;
6303
+ const replay = this.pendingTriggers;
6304
+ this.pendingTriggers = [];
6305
+ if (!replay.length) return;
6306
+ this.debug("Replaying triggers received before popups loaded", replay.length);
6307
+ replay.forEach((trigger) => {
6308
+ if (trigger.kind === "event") this.evaluateEvent(trigger.name);
6309
+ else this.triggerSurvey(trigger.surveyId, trigger.popupId);
6310
+ });
6311
+ }
6082
6312
  showDefinition(def) {
6083
6313
  this.log("Showing popup (definition)", def);
6084
6314
  this.surveyToPopupId.set(def.surveyId, def.id);
@@ -6349,7 +6579,7 @@ var DeepdotsPopups = class {
6349
6579
  } catch {
6350
6580
  }
6351
6581
  }
6352
- /** Fetch al servidor para obtener popups */
6582
+ /** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
6353
6583
  async fetchPopupsFromServer() {
6354
6584
  const apiKey = this.config?.apiKey;
6355
6585
  const baseUrl = this.baseUrl;
@@ -6365,19 +6595,19 @@ var DeepdotsPopups = class {
6365
6595
  const response = await fetch(endpoint);
6366
6596
  if (!response.ok) {
6367
6597
  this.log("Failed to fetch popups", response.status, response.statusText);
6368
- return [];
6598
+ return null;
6369
6599
  }
6370
6600
  const raw = await response.text();
6371
6601
  const parsed = JSON.parse(raw);
6372
6602
  if (!Array.isArray(parsed)) {
6373
6603
  this.log("Unexpected popups payload", parsed);
6374
- return [];
6604
+ return null;
6375
6605
  }
6376
6606
  this.debug("Fetched popups payload", parsed);
6377
6607
  return parsed;
6378
6608
  } catch (error) {
6379
6609
  this.log("Error fetching popups", error);
6380
- return [];
6610
+ return null;
6381
6611
  }
6382
6612
  }
6383
6613
  /** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
@@ -6801,6 +7031,7 @@ function DeepdotsProvider({ config, children }) {
6801
7031
  style: { flex: 1 },
6802
7032
  originWhitelist: ["*"],
6803
7033
  javaScriptEnabled: true,
7034
+ keyboardDisplayRequiresUserAction: false,
6804
7035
  source: { html: survey.html, baseUrl: "https://sdk.deepdots.com/" },
6805
7036
  onMessage: (e) => rendererRef.current?.handleMessage(e.nativeEvent.data)
6806
7037
  }