@magicfeedback/popup-sdk 1.7.0 → 1.7.2

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
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  InMemoryStorage,
3
3
  LABELS,
4
+ PRIMARY_COLOR_JS,
4
5
  RESOLVE_LOCALE_JS,
6
+ REVEAL_TIMEOUT_MS,
5
7
  RTL_LOCALES,
6
8
  STORAGE_KEYS,
7
9
  SUPPORTED_LANGUAGES,
@@ -18,7 +20,7 @@ import {
18
20
  sdkWarn,
19
21
  setLogger,
20
22
  style_default
21
- } from "./chunk-W6MPSWMS.mjs";
23
+ } from "./chunk-XHR3MHJO.mjs";
22
24
 
23
25
  // src/types/index.ts
24
26
  var POPUP_TRIGGER_CONDITION_STATUSES = [
@@ -209,6 +211,30 @@ function resolveEnvironment(nodeEnv) {
209
211
  }
210
212
 
211
213
  // src/ui/surveyHtml.ts
214
+ var REVEAL_JS = `
215
+ function ddCreateReveal(popup, onReveal, timeoutMs){
216
+ var revealed=false, timer=null;
217
+ function reveal(){
218
+ if(revealed){ return; }
219
+ revealed=true;
220
+ if(timer){ clearTimeout(timer); timer=null; }
221
+ onReveal();
222
+ }
223
+ function revealWhenPainted(){
224
+ if(revealed){ return; }
225
+ var imgs=popup.querySelectorAll('img'), pending=[];
226
+ for(var i=0;i<imgs.length;i++){ if(!imgs[i].complete){ pending.push(imgs[i]); } }
227
+ if(!pending.length){ reveal(); return; }
228
+ var left=pending.length;
229
+ function onSettled(){ left--; if(left<=0){ reveal(); } }
230
+ for(var j=0;j<pending.length;j++){
231
+ pending[j].addEventListener('load', onSettled);
232
+ pending[j].addEventListener('error', onSettled);
233
+ }
234
+ }
235
+ timer=setTimeout(reveal, timeoutMs);
236
+ return { reveal: reveal, whenPainted: revealWhenPainted };
237
+ }`;
212
238
  var POSITION_MAP = {
213
239
  center: { justifyContent: "center", alignItems: "center", padding: "0", background: "rgba(0,0,0,0.5)" },
214
240
  bottom: { justifyContent: "center", alignItems: "flex-end", padding: "16px", background: "transparent" },
@@ -282,7 +308,13 @@ function buildSurveyHtml(opts) {
282
308
  ${style_default}
283
309
  ${fontFaceCss}
284
310
  html,body{margin:0;padding:0;height:100%;font-family:${fontFamilyValue}}
285
- body{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${pos.alignItems};background:${bodyBg};padding:${bodyPadding};box-sizing:border-box}
311
+ /* Apertura diferida: ni la tarjeta ni el velo se pintan hasta que el survey est\xE1 listo
312
+ (\`dd-ready\`). Si el fondo entrara antes, en RN se ver\xEDa el velo oscuro varios cientos de ms
313
+ antes que el contenido, que es la misma mala sensaci\xF3n que el spinner. */
314
+ body{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${pos.alignItems};background:transparent;padding:${bodyPadding};box-sizing:border-box}
315
+ body.dd-ready{background:${bodyBg}}
316
+ #dd-popup{visibility:hidden}
317
+ body.dd-ready #dd-popup{visibility:visible}
286
318
  .deepdots-popup{position:relative;display:flex;flex-direction:column;justify-content:flex-start;background:${cardBg};color-scheme:${colorScheme};border-radius:${cardRadius};padding:24px;box-shadow:${cardShadow};max-width:${cardMaxWidth};width:${cardWidth};min-height:${cardMinHeight};height:${cardHeight};max-height:${cardMaxHeight};box-sizing:border-box}
287
319
  .deepdots-popup-header{display:flex;justify-content:space-between;align-items:center;gap:12px;width:100%;flex:0 0 auto}
288
320
  /* Neutraliza la regla \`.deepdots-popup h2\` del CSS del survey (uppercase, centrado y
@@ -316,6 +348,13 @@ body{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${
316
348
  #dd-form-wrapper{width:100%;flex:1 1 auto}
317
349
  #mf{width:100%;box-sizing:border-box;visibility:hidden}
318
350
  .deepdots-popup-main *{max-width:100%;box-sizing:border-box}
351
+ /* El margen lateral lo pone la tarjeta: el CSS del survey lo a\xF1ade otra vez en el contenedor, en
352
+ el formulario y en el bloque de cada pregunta, y eso deja el enunciado y las opciones m\xE1s
353
+ adentro que el logo, el t\xEDtulo y la barra de progreso. Solo el lateral; el vertical separa las
354
+ preguntas. Mismas reglas que en el popup DOM (renderPopup.ts). */
355
+ .deepdots-popup .magicfeedback-container,
356
+ .deepdots-popup .magicfeedback-form,
357
+ .deepdots-popup .magicfeedback-div{padding-left:0;padding-right:0}
319
358
  #dd-logo{max-height:40px;max-width:100%;object-fit:contain;display:block;margin:12px 0 0 0}
320
359
  .deepdots-error-hint{display:none;margin:12px 0 0 0;padding:10px 12px;border-radius:6px;background:#FEF3C7;color:#92400E;border:1px solid #FCD34D;font-size:13px}
321
360
  /* Pantalla final: el HTML del editor de la plataforma (imagen + texto), centrado. */
@@ -323,7 +362,7 @@ body{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${
323
362
  .deepdots-success img{max-width:100%;height:auto;margin:0 auto 16px auto;display:block}
324
363
  .deepdots-success p{margin:0;font-size:16px;font-weight:600;line-height:1.4}
325
364
  .deepdots-popup-footer{display:flex;flex-direction:row-reverse;justify-content:space-between;align-items:center;gap:4px;margin-top:auto;width:100%;padding-top:12px}
326
- .dd-nav-btn{display:none;border:none;min-height:44px;padding:12px 24px;border-radius:6px;cursor:pointer;font-size:15px;font-weight:600;align-items:center;justify-content:center;text-align:center}
365
+ .dd-nav-btn{display:none;border:none;min-height:44px;padding:12px 24px;border-radius:999px;cursor:pointer;font-size:15px;font-weight:600;align-items:center;justify-content:center;text-align:center}
327
366
  /* Secundario como bot\xF3n de texto: sin borde ni fondo, para que el primario sea la \xFAnica CTA. */
328
367
  #dd-back{background:transparent;color:${textMuted};border:none}
329
368
  #dd-start,#dd-complete,#dd-submit{background:#1E293B;color:#fff;border:none}
@@ -548,9 +587,23 @@ ${customCss}
548
587
  }
549
588
  }
550
589
 
590
+ ${PRIMARY_COLOR_JS}
591
+ ${REVEAL_JS}
592
+ // El popup se ense\xF1a cuando el survey est\xE1 pintado; \`ready\` deja al host abrir su propio
593
+ // Modal en ese momento en vez de montar un WebView en blanco.
594
+ var ddReveal=ddCreateReveal(popup, function(){
595
+ document.body.classList.add('dd-ready');
596
+ emitJSON('ready');
597
+ }, ${REVEAL_TIMEOUT_MS});
598
+
551
599
  function setLoading(isLoading){
552
600
  spinner.style.display=isLoading?'flex':'none';
553
- if(!isLoading){ formHost.style.visibility='visible'; }
601
+ if(!isLoading){
602
+ formHost.style.visibility='visible';
603
+ // Fin de la carga, sea por survey cargado o por error. Entre p\xE1ginas es no-op: la
604
+ // revelaci\xF3n solo act\xFAa la primera vez.
605
+ ddReveal.whenPainted();
606
+ }
554
607
  footer.style.display=isLoading?'none':'flex';
555
608
  backBtn.disabled=isLoading; startBtn.disabled=isLoading; completeBtn.disabled=isLoading; submitBtn.disabled=isLoading;
556
609
  if(isLoading){ updateButtons(null); }
@@ -593,6 +646,10 @@ ${customCss}
593
646
  if(s.loadingBarColor){ progressBar.style.background=s.loadingBarColor; progressFollowUp.style.background=s.loadingBarColor; }
594
647
  if(chromeOn && s.boxBackgroundColor){ popup.style.background=s.boxBackgroundColor; }
595
648
  if(s.contentAlign){ main.style.justifyContent = s.contentAlign==='center' ? 'center' : 'flex-start'; }
649
+ // El survey pinta sus controles con --mf-primary, que sale de primaryColor; si la
650
+ // integraci\xF3n solo configura el del bot\xF3n, el popup mezclar\xEDa el color de la marca
651
+ // con el gris azulado por defecto del paquete.
652
+ ddApplySurveyPrimaryColor(popup, s);
596
653
  if(s.buttonPrimaryColor){
597
654
  submitBtn.style.background=s.buttonPrimaryColor; submitBtn.style.border='none'; submitBtn.style.color='#fff';
598
655
  startBtn.style.background=s.buttonPrimaryColor; startBtn.style.border='none'; startBtn.style.color='#fff';
@@ -684,6 +741,7 @@ ${customCss}
684
741
  }
685
742
 
686
743
  // src/platform/react-native-renderer.ts
744
+ var NATIVE_READY_GRACE_MS = 500;
687
745
  var ReactNativePopupRenderer = class {
688
746
  constructor(options = {}) {
689
747
  this.options = options;
@@ -691,6 +749,8 @@ var ReactNativePopupRenderer = class {
691
749
  this.onCloseFn = null;
692
750
  this.currentSurveyId = null;
693
751
  this.partialEmitted = false;
752
+ this.readyEmitted = false;
753
+ this.readyTimer = null;
694
754
  }
695
755
  init() {
696
756
  }
@@ -699,6 +759,8 @@ var ReactNativePopupRenderer = class {
699
759
  this.onCloseFn = onClose;
700
760
  this.currentSurveyId = surveyId;
701
761
  this.partialEmitted = false;
762
+ this.readyEmitted = false;
763
+ this.armReadyFallback(surveyId);
702
764
  const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);
703
765
  const html = buildSurveyHtml({
704
766
  surveyId,
@@ -726,9 +788,32 @@ var ReactNativePopupRenderer = class {
726
788
  }
727
789
  }
728
790
  hide() {
791
+ this.clearReadyFallback();
729
792
  this.options.onHide?.();
730
793
  this.currentSurveyId = null;
731
794
  }
795
+ /** Respaldo del aviso de listo por si el WebView no llega a ejecutar el HTML. */
796
+ armReadyFallback(surveyId) {
797
+ this.clearReadyFallback();
798
+ if (!this.options.onReady) return;
799
+ this.readyTimer = setTimeout(() => {
800
+ this.readyTimer = null;
801
+ this.emitReady(surveyId);
802
+ }, REVEAL_TIMEOUT_MS + NATIVE_READY_GRACE_MS);
803
+ }
804
+ clearReadyFallback() {
805
+ if (this.readyTimer !== null) {
806
+ clearTimeout(this.readyTimer);
807
+ this.readyTimer = null;
808
+ }
809
+ }
810
+ /** Idempotente: el WebView puede avisar a la vez que vence el respaldo. */
811
+ emitReady(surveyId) {
812
+ if (this.readyEmitted) return;
813
+ this.readyEmitted = true;
814
+ this.clearReadyFallback();
815
+ this.options.onReady?.({ surveyId });
816
+ }
732
817
  /**
733
818
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
734
819
  * eventos del SDK: primera interacción → `popup_clicked` (PARTIAL), completado →
@@ -744,6 +829,9 @@ var ReactNativePopupRenderer = class {
744
829
  name = raw;
745
830
  }
746
831
  switch (name) {
832
+ case "ready":
833
+ this.emitReady(surveyId);
834
+ break;
747
835
  case "loaded":
748
836
  case "before_submit":
749
837
  case "after_submit":
@@ -771,6 +859,33 @@ function createReactNativeRenderer() {
771
859
  return new ReactNativePopupRenderer();
772
860
  }
773
861
 
862
+ // src/ui/preload.ts
863
+ var defaultLoader = () => import("./renderPopup-5QXL6EJW.mjs");
864
+ var pending = null;
865
+ function preloadRenderPopup(loader = defaultLoader) {
866
+ if (pending) return pending;
867
+ try {
868
+ pending = loader().then(() => void 0).catch(() => {
869
+ pending = null;
870
+ });
871
+ } catch {
872
+ pending = null;
873
+ return Promise.resolve();
874
+ }
875
+ return pending;
876
+ }
877
+ function schedulePreloadRenderPopup(loader) {
878
+ const run = () => {
879
+ void preloadRenderPopup(loader);
880
+ };
881
+ const idle = globalThis.requestIdleCallback;
882
+ if (typeof idle === "function") {
883
+ idle(run, { timeout: 2e3 });
884
+ return;
885
+ }
886
+ setTimeout(run, 0);
887
+ }
888
+
774
889
  // src/platform/renderer.ts
775
890
  var NoopPopupRenderer = class {
776
891
  show() {
@@ -805,13 +920,20 @@ var BrowserPopupRenderer = class {
805
920
  document.body.appendChild(this.container);
806
921
  }
807
922
  }
923
+ /**
924
+ * Descarga el chunk de `renderPopup` antes de que ningún trigger dispare, para que abrir el
925
+ * popup no empiece por esperar ~238 KB de JS.
926
+ */
927
+ preload() {
928
+ schedulePreloadRenderPopup();
929
+ }
808
930
  show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId, _renderChrome, options) {
809
931
  if (this.visible) return;
810
932
  if (!this.container || !document.body.contains(this.container)) this.init();
811
933
  if (!this.container) return;
812
934
  this.visible = true;
813
935
  const container = this.container;
814
- void import("./renderPopup-IHGYFMXO.mjs").then(({ renderPopup }) => {
936
+ void import("./renderPopup-5QXL6EJW.mjs").then(({ renderPopup }) => {
815
937
  renderPopup(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService, analyticsFeedbackSessionId, options);
816
938
  });
817
939
  }
@@ -819,6 +941,8 @@ var BrowserPopupRenderer = class {
819
941
  this.visible = false;
820
942
  if (this.container) {
821
943
  this.container.style.display = "none";
944
+ this.container.style.visibility = "";
945
+ this.container.style.pointerEvents = "";
822
946
  this.container.innerHTML = "";
823
947
  }
824
948
  }
@@ -1563,6 +1687,75 @@ var CrashReporter = class {
1563
1687
  }
1564
1688
  };
1565
1689
 
1690
+ // src/core/popup-state-store.ts
1691
+ var POPUP_STATE_STORAGE_KEY = "deepdots.popups.state";
1692
+ var MAX_TRACKED_POPUP_ENTRIES = 200;
1693
+ function emptyState() {
1694
+ return { lastShown: {}, progress: {} };
1695
+ }
1696
+ function isValidTimestamp(value) {
1697
+ return typeof value === "number" && Number.isFinite(value);
1698
+ }
1699
+ function capEntries(entries, timestampOf) {
1700
+ if (entries.length <= MAX_TRACKED_POPUP_ENTRIES) return entries;
1701
+ return [...entries].sort((a, b) => timestampOf(b[1]) - timestampOf(a[1])).slice(0, MAX_TRACKED_POPUP_ENTRIES);
1702
+ }
1703
+ function readPopupState(storage) {
1704
+ let raw = null;
1705
+ try {
1706
+ raw = storage.getItem(POPUP_STATE_STORAGE_KEY);
1707
+ } catch {
1708
+ return emptyState();
1709
+ }
1710
+ if (!raw) return emptyState();
1711
+ let parsed;
1712
+ try {
1713
+ parsed = JSON.parse(raw);
1714
+ } catch {
1715
+ return emptyState();
1716
+ }
1717
+ if (typeof parsed !== "object" || parsed === null) return emptyState();
1718
+ const source = parsed;
1719
+ const state = emptyState();
1720
+ if (typeof source.lastShown === "object" && source.lastShown !== null) {
1721
+ const valid = Object.entries(source.lastShown).filter((entry) => isValidTimestamp(entry[1]));
1722
+ capEntries(valid, (timestamp) => timestamp).forEach(([popupId, timestamp]) => {
1723
+ state.lastShown[popupId] = timestamp;
1724
+ });
1725
+ }
1726
+ if (typeof source.progress === "object" && source.progress !== null) {
1727
+ const valid = Object.entries(source.progress).filter((entry) => {
1728
+ const value = entry[1];
1729
+ if (typeof value !== "object" || value === null) return false;
1730
+ const candidate = value;
1731
+ return POPUP_TRIGGER_CONDITION_STATUSES.includes(candidate.status) && isValidTimestamp(candidate.timestamp);
1732
+ });
1733
+ capEntries(valid, (entry) => entry.timestamp).forEach(([surveyId, entry]) => {
1734
+ state.progress[surveyId] = { status: entry.status, timestamp: entry.timestamp };
1735
+ });
1736
+ }
1737
+ return state;
1738
+ }
1739
+ function writePopupState(storage, state) {
1740
+ const capped = emptyState();
1741
+ capEntries(Object.entries(state.lastShown), (timestamp) => timestamp).forEach(([popupId, timestamp]) => {
1742
+ capped.lastShown[popupId] = timestamp;
1743
+ });
1744
+ capEntries(Object.entries(state.progress), (entry) => entry.timestamp).forEach(([surveyId, entry]) => {
1745
+ capped.progress[surveyId] = entry;
1746
+ });
1747
+ try {
1748
+ storage.setItem(POPUP_STATE_STORAGE_KEY, JSON.stringify(capped));
1749
+ } catch {
1750
+ }
1751
+ }
1752
+ function clearPopupState(storage) {
1753
+ try {
1754
+ storage.removeItem(POPUP_STATE_STORAGE_KEY);
1755
+ } catch {
1756
+ }
1757
+ }
1758
+
1566
1759
  // src/analytics/messaging.ts
1567
1760
  var MESSAGE_CHANNELS = ["push", "in_app"];
1568
1761
  function buildMessageParams(stage, o) {
@@ -1650,6 +1843,10 @@ var DeepdotsPopups = class {
1650
1843
  this.popupDefinitions = [];
1651
1844
  this.popupsLoaded = false;
1652
1845
  this.pendingAutoLaunch = false;
1846
+ // Estado que alimenta las reglas de redisplay. Copia de trabajo en memoria; la fuente
1847
+ // duradera es el storage (`popup-state-store`), rehidratada en `init()` — sin eso, una
1848
+ // recarga reiniciaba el cooldown y el popup volvía a salir. `answeredSurveys` se deriva
1849
+ // de las entradas COMPLETED de `surveyProgress` (los dos los escribe `markSurveyAnswered`).
1653
1850
  this.answeredSurveys = /* @__PURE__ */ new Set();
1654
1851
  this.surveyProgress = /* @__PURE__ */ new Map();
1655
1852
  this.lastShown = /* @__PURE__ */ new Map();
@@ -1725,6 +1922,7 @@ var DeepdotsPopups = class {
1725
1922
  };
1726
1923
  const storage = config.storage ?? createDefaultStorage();
1727
1924
  this.storage = storage;
1925
+ this.loadPopupState(storage);
1728
1926
  this.tracking = new TrackingManager({
1729
1927
  storage,
1730
1928
  clientUserId: this.config.userId,
@@ -1814,6 +2012,7 @@ var DeepdotsPopups = class {
1814
2012
  this.popupDefinitions = this.validatePopupDefinitions(defs);
1815
2013
  this.popupsLoaded = true;
1816
2014
  this.log("Popups loaded from API", this.popupDefinitions);
2015
+ if (this.popupDefinitions.length) this.renderer.preload?.();
1817
2016
  this.configureTriggersFromDefinitions();
1818
2017
  this.processDeferredExitQueue();
1819
2018
  if (this.pendingAutoLaunch) {
@@ -1992,6 +2191,10 @@ var DeepdotsPopups = class {
1992
2191
  }) : null;
1993
2192
  this.analytics?.resetUserScope();
1994
2193
  this.messageGuard.reset();
2194
+ this.lastShown.clear();
2195
+ this.surveyProgress.clear();
2196
+ this.answeredSurveys.clear();
2197
+ clearPopupState(storage);
1995
2198
  this.log("tracking \xB7 user_change \xB7 user_id:", this.tracking.getUserId());
1996
2199
  this.openSession();
1997
2200
  }
@@ -2163,6 +2366,7 @@ var DeepdotsPopups = class {
2163
2366
  this.log("Showing popup (definition)", def);
2164
2367
  this.surveyToPopupId.set(def.surveyId, def.id);
2165
2368
  this.lastShown.set(def.id, Date.now());
2369
+ this.persistPopupState();
2166
2370
  this.renderPopup(def.surveyId, def.productId, def.actions, def.style, def.title);
2167
2371
  this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
2168
2372
  }
@@ -2296,7 +2500,7 @@ var DeepdotsPopups = class {
2296
2500
  processDeferredExitQueue() {
2297
2501
  const queue = this.getDeferredExitQueue();
2298
2502
  if (!queue.length) return;
2299
- const pending = [];
2503
+ const pending2 = [];
2300
2504
  const now = Date.now();
2301
2505
  queue.forEach((item) => {
2302
2506
  const def = this.popupDefinitions.find((popup) => popup.id === item.id && popup.surveyId === item.surveyId);
@@ -2307,10 +2511,10 @@ var DeepdotsPopups = class {
2307
2511
  this.tryShowDeferredExitPopup(item);
2308
2512
  return;
2309
2513
  }
2310
- pending.push(item);
2514
+ pending2.push(item);
2311
2515
  this.scheduleDeferredExitPopup(item);
2312
2516
  });
2313
- this.setDeferredExitQueue(pending);
2517
+ this.setDeferredExitQueue(pending2);
2314
2518
  }
2315
2519
  scheduleDeferredExitPopup(item) {
2316
2520
  if (typeof window === "undefined") return;
@@ -2383,7 +2587,7 @@ var DeepdotsPopups = class {
2383
2587
  async fetchPopupsFromServer() {
2384
2588
  const apiKey = this.config?.apiKey;
2385
2589
  const baseUrl = this.baseUrl;
2386
- const userId = this.config?.userId;
2590
+ const userId = this.tracking?.getUserId() ?? this.config?.userId;
2387
2591
  if (!apiKey || !baseUrl) {
2388
2592
  this.log("Missing apiKey or baseUrl. Skipping popups fetch.");
2389
2593
  return [];
@@ -2527,7 +2731,7 @@ var DeepdotsPopups = class {
2527
2731
  }
2528
2732
  if (!this.popupContainer) return;
2529
2733
  const container = this.popupContainer;
2530
- void import("./renderPopup-IHGYFMXO.mjs").then(({ renderPopup }) => {
2734
+ void import("./renderPopup-5QXL6EJW.mjs").then(({ renderPopup }) => {
2531
2735
  renderPopup(
2532
2736
  container,
2533
2737
  surveyId,
@@ -2642,6 +2846,29 @@ var DeepdotsPopups = class {
2642
2846
  return;
2643
2847
  }
2644
2848
  this.surveyProgress.set(surveyId, { status, timestamp: Date.now() });
2849
+ this.persistPopupState();
2850
+ }
2851
+ /**
2852
+ * Rehidrata el estado de redisplay desde el storage. `answeredSurveys` se reconstruye
2853
+ * desde las entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
2854
+ */
2855
+ loadPopupState(storage) {
2856
+ const state = readPopupState(storage);
2857
+ Object.entries(state.lastShown).forEach(([popupId, timestamp]) => {
2858
+ this.lastShown.set(popupId, timestamp);
2859
+ });
2860
+ Object.entries(state.progress).forEach(([surveyId, entry]) => {
2861
+ this.surveyProgress.set(surveyId, { status: entry.status, timestamp: entry.timestamp });
2862
+ if (entry.status === "COMPLETED") this.answeredSurveys.add(surveyId);
2863
+ });
2864
+ }
2865
+ /** Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria. */
2866
+ persistPopupState() {
2867
+ if (!this.storage) return;
2868
+ writePopupState(this.storage, {
2869
+ lastShown: Object.fromEntries(this.lastShown),
2870
+ progress: Object.fromEntries(this.surveyProgress)
2871
+ });
2645
2872
  }
2646
2873
  isPopupDefinition(value) {
2647
2874
  if (typeof value !== "object" || value === null) return false;
@@ -2759,8 +2986,10 @@ export {
2759
2986
  MAX_TRACKED_MESSAGES,
2760
2987
  MESSAGE_CHANNELS,
2761
2988
  MessageGuard,
2989
+ NATIVE_READY_GRACE_MS,
2762
2990
  NoopPopupRenderer,
2763
2991
  POPUP_TRIGGER_CONDITION_STATUSES,
2992
+ REVEAL_TIMEOUT_MS,
2764
2993
  RTL_LOCALES,
2765
2994
  ReactNativePopupRenderer,
2766
2995
  SUPPORTED_LANGUAGES,