@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.
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  LABELS,
3
+ PRIMARY_COLOR_JS,
3
4
  RESOLVE_LOCALE_JS,
5
+ REVEAL_TIMEOUT_MS,
4
6
  RTL_LOCALES,
5
7
  STORAGE_KEYS,
6
8
  TrackingManager,
@@ -15,7 +17,7 @@ import {
15
17
  sdkWarn,
16
18
  setLogger,
17
19
  style_default
18
- } from "./chunk-WNIUIOKF.mjs";
20
+ } from "./chunk-322DDMX6.mjs";
19
21
 
20
22
  // src/react-native/index.tsx
21
23
  import { createContext, useContext, useEffect, useRef, useState } from "react";
@@ -211,6 +213,30 @@ function resolveEnvironment(nodeEnv) {
211
213
  }
212
214
 
213
215
  // src/ui/surveyHtml.ts
216
+ var REVEAL_JS = `
217
+ function ddCreateReveal(popup, onReveal, timeoutMs){
218
+ var revealed=false, timer=null;
219
+ function reveal(){
220
+ if(revealed){ return; }
221
+ revealed=true;
222
+ if(timer){ clearTimeout(timer); timer=null; }
223
+ onReveal();
224
+ }
225
+ function revealWhenPainted(){
226
+ if(revealed){ return; }
227
+ var imgs=popup.querySelectorAll('img'), pending=[];
228
+ for(var i=0;i<imgs.length;i++){ if(!imgs[i].complete){ pending.push(imgs[i]); } }
229
+ if(!pending.length){ reveal(); return; }
230
+ var left=pending.length;
231
+ function onSettled(){ left--; if(left<=0){ reveal(); } }
232
+ for(var j=0;j<pending.length;j++){
233
+ pending[j].addEventListener('load', onSettled);
234
+ pending[j].addEventListener('error', onSettled);
235
+ }
236
+ }
237
+ timer=setTimeout(reveal, timeoutMs);
238
+ return { reveal: reveal, whenPainted: revealWhenPainted };
239
+ }`;
214
240
  var POSITION_MAP = {
215
241
  center: { justifyContent: "center", alignItems: "center", padding: "0", background: "rgba(0,0,0,0.5)" },
216
242
  bottom: { justifyContent: "center", alignItems: "flex-end", padding: "16px", background: "transparent" },
@@ -284,7 +310,13 @@ function buildSurveyHtml(opts) {
284
310
  ${style_default}
285
311
  ${fontFaceCss}
286
312
  html,body{margin:0;padding:0;height:100%;font-family:${fontFamilyValue}}
287
- body{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${pos.alignItems};background:${bodyBg};padding:${bodyPadding};box-sizing:border-box}
313
+ /* Apertura diferida: ni la tarjeta ni el velo se pintan hasta que el survey est\xE1 listo
314
+ (\`dd-ready\`). Si el fondo entrara antes, en RN se ver\xEDa el velo oscuro varios cientos de ms
315
+ antes que el contenido, que es la misma mala sensaci\xF3n que el spinner. */
316
+ body{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${pos.alignItems};background:transparent;padding:${bodyPadding};box-sizing:border-box}
317
+ body.dd-ready{background:${bodyBg}}
318
+ #dd-popup{visibility:hidden}
319
+ body.dd-ready #dd-popup{visibility:visible}
288
320
  .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}
289
321
  .deepdots-popup-header{display:flex;justify-content:space-between;align-items:center;gap:12px;width:100%;flex:0 0 auto}
290
322
  /* Neutraliza la regla \`.deepdots-popup h2\` del CSS del survey (uppercase, centrado y
@@ -318,6 +350,13 @@ body{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${
318
350
  #dd-form-wrapper{width:100%;flex:1 1 auto}
319
351
  #mf{width:100%;box-sizing:border-box;visibility:hidden}
320
352
  .deepdots-popup-main *{max-width:100%;box-sizing:border-box}
353
+ /* El margen lateral lo pone la tarjeta: el CSS del survey lo a\xF1ade otra vez en el contenedor, en
354
+ el formulario y en el bloque de cada pregunta, y eso deja el enunciado y las opciones m\xE1s
355
+ adentro que el logo, el t\xEDtulo y la barra de progreso. Solo el lateral; el vertical separa las
356
+ preguntas. Mismas reglas que en el popup DOM (renderPopup.ts). */
357
+ .deepdots-popup .magicfeedback-container,
358
+ .deepdots-popup .magicfeedback-form,
359
+ .deepdots-popup .magicfeedback-div{padding-left:0;padding-right:0}
321
360
  #dd-logo{max-height:40px;max-width:100%;object-fit:contain;display:block;margin:12px 0 0 0}
322
361
  .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}
323
362
  /* Pantalla final: el HTML del editor de la plataforma (imagen + texto), centrado. */
@@ -325,7 +364,7 @@ body{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${
325
364
  .deepdots-success img{max-width:100%;height:auto;margin:0 auto 16px auto;display:block}
326
365
  .deepdots-success p{margin:0;font-size:16px;font-weight:600;line-height:1.4}
327
366
  .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}
328
- .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}
367
+ .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}
329
368
  /* Secundario como bot\xF3n de texto: sin borde ni fondo, para que el primario sea la \xFAnica CTA. */
330
369
  #dd-back{background:transparent;color:${textMuted};border:none}
331
370
  #dd-start,#dd-complete,#dd-submit{background:#1E293B;color:#fff;border:none}
@@ -550,9 +589,23 @@ ${customCss}
550
589
  }
551
590
  }
552
591
 
592
+ ${PRIMARY_COLOR_JS}
593
+ ${REVEAL_JS}
594
+ // El popup se ense\xF1a cuando el survey est\xE1 pintado; \`ready\` deja al host abrir su propio
595
+ // Modal en ese momento en vez de montar un WebView en blanco.
596
+ var ddReveal=ddCreateReveal(popup, function(){
597
+ document.body.classList.add('dd-ready');
598
+ emitJSON('ready');
599
+ }, ${REVEAL_TIMEOUT_MS});
600
+
553
601
  function setLoading(isLoading){
554
602
  spinner.style.display=isLoading?'flex':'none';
555
- if(!isLoading){ formHost.style.visibility='visible'; }
603
+ if(!isLoading){
604
+ formHost.style.visibility='visible';
605
+ // Fin de la carga, sea por survey cargado o por error. Entre p\xE1ginas es no-op: la
606
+ // revelaci\xF3n solo act\xFAa la primera vez.
607
+ ddReveal.whenPainted();
608
+ }
556
609
  footer.style.display=isLoading?'none':'flex';
557
610
  backBtn.disabled=isLoading; startBtn.disabled=isLoading; completeBtn.disabled=isLoading; submitBtn.disabled=isLoading;
558
611
  if(isLoading){ updateButtons(null); }
@@ -595,6 +648,10 @@ ${customCss}
595
648
  if(s.loadingBarColor){ progressBar.style.background=s.loadingBarColor; progressFollowUp.style.background=s.loadingBarColor; }
596
649
  if(chromeOn && s.boxBackgroundColor){ popup.style.background=s.boxBackgroundColor; }
597
650
  if(s.contentAlign){ main.style.justifyContent = s.contentAlign==='center' ? 'center' : 'flex-start'; }
651
+ // El survey pinta sus controles con --mf-primary, que sale de primaryColor; si la
652
+ // integraci\xF3n solo configura el del bot\xF3n, el popup mezclar\xEDa el color de la marca
653
+ // con el gris azulado por defecto del paquete.
654
+ ddApplySurveyPrimaryColor(popup, s);
598
655
  if(s.buttonPrimaryColor){
599
656
  submitBtn.style.background=s.buttonPrimaryColor; submitBtn.style.border='none'; submitBtn.style.color='#fff';
600
657
  startBtn.style.background=s.buttonPrimaryColor; startBtn.style.border='none'; startBtn.style.color='#fff';
@@ -686,6 +743,7 @@ ${customCss}
686
743
  }
687
744
 
688
745
  // src/platform/react-native-renderer.ts
746
+ var NATIVE_READY_GRACE_MS = 500;
689
747
  var ReactNativePopupRenderer = class {
690
748
  constructor(options = {}) {
691
749
  this.options = options;
@@ -693,6 +751,8 @@ var ReactNativePopupRenderer = class {
693
751
  this.onCloseFn = null;
694
752
  this.currentSurveyId = null;
695
753
  this.partialEmitted = false;
754
+ this.readyEmitted = false;
755
+ this.readyTimer = null;
696
756
  }
697
757
  init() {
698
758
  }
@@ -701,6 +761,8 @@ var ReactNativePopupRenderer = class {
701
761
  this.onCloseFn = onClose;
702
762
  this.currentSurveyId = surveyId;
703
763
  this.partialEmitted = false;
764
+ this.readyEmitted = false;
765
+ this.armReadyFallback(surveyId);
704
766
  const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);
705
767
  const html = buildSurveyHtml({
706
768
  surveyId,
@@ -728,9 +790,32 @@ var ReactNativePopupRenderer = class {
728
790
  }
729
791
  }
730
792
  hide() {
793
+ this.clearReadyFallback();
731
794
  this.options.onHide?.();
732
795
  this.currentSurveyId = null;
733
796
  }
797
+ /** Respaldo del aviso de listo por si el WebView no llega a ejecutar el HTML. */
798
+ armReadyFallback(surveyId) {
799
+ this.clearReadyFallback();
800
+ if (!this.options.onReady) return;
801
+ this.readyTimer = setTimeout(() => {
802
+ this.readyTimer = null;
803
+ this.emitReady(surveyId);
804
+ }, REVEAL_TIMEOUT_MS + NATIVE_READY_GRACE_MS);
805
+ }
806
+ clearReadyFallback() {
807
+ if (this.readyTimer !== null) {
808
+ clearTimeout(this.readyTimer);
809
+ this.readyTimer = null;
810
+ }
811
+ }
812
+ /** Idempotente: el WebView puede avisar a la vez que vence el respaldo. */
813
+ emitReady(surveyId) {
814
+ if (this.readyEmitted) return;
815
+ this.readyEmitted = true;
816
+ this.clearReadyFallback();
817
+ this.options.onReady?.({ surveyId });
818
+ }
734
819
  /**
735
820
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
736
821
  * eventos del SDK: primera interacción → `popup_clicked` (PARTIAL), completado →
@@ -746,6 +831,9 @@ var ReactNativePopupRenderer = class {
746
831
  name = raw;
747
832
  }
748
833
  switch (name) {
834
+ case "ready":
835
+ this.emitReady(surveyId);
836
+ break;
749
837
  case "loaded":
750
838
  case "before_submit":
751
839
  case "after_submit":
@@ -773,6 +861,33 @@ function createReactNativeRenderer() {
773
861
  return new ReactNativePopupRenderer();
774
862
  }
775
863
 
864
+ // src/ui/preload.ts
865
+ var defaultLoader = () => import("./renderPopup-SNDPX5RX.mjs");
866
+ var pending = null;
867
+ function preloadRenderPopup(loader = defaultLoader) {
868
+ if (pending) return pending;
869
+ try {
870
+ pending = loader().then(() => void 0).catch(() => {
871
+ pending = null;
872
+ });
873
+ } catch {
874
+ pending = null;
875
+ return Promise.resolve();
876
+ }
877
+ return pending;
878
+ }
879
+ function schedulePreloadRenderPopup(loader) {
880
+ const run = () => {
881
+ void preloadRenderPopup(loader);
882
+ };
883
+ const idle = globalThis.requestIdleCallback;
884
+ if (typeof idle === "function") {
885
+ idle(run, { timeout: 2e3 });
886
+ return;
887
+ }
888
+ setTimeout(run, 0);
889
+ }
890
+
776
891
  // src/platform/renderer.ts
777
892
  var NoopPopupRenderer = class {
778
893
  show() {
@@ -807,13 +922,20 @@ var BrowserPopupRenderer = class {
807
922
  document.body.appendChild(this.container);
808
923
  }
809
924
  }
925
+ /**
926
+ * Descarga el chunk de `renderPopup` antes de que ningún trigger dispare, para que abrir el
927
+ * popup no empiece por esperar ~238 KB de JS.
928
+ */
929
+ preload() {
930
+ schedulePreloadRenderPopup();
931
+ }
810
932
  show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId, _renderChrome, options) {
811
933
  if (this.visible) return;
812
934
  if (!this.container || !document.body.contains(this.container)) this.init();
813
935
  if (!this.container) return;
814
936
  this.visible = true;
815
937
  const container = this.container;
816
- void import("./renderPopup-EWGKG53W.mjs").then(({ renderPopup }) => {
938
+ void import("./renderPopup-SNDPX5RX.mjs").then(({ renderPopup }) => {
817
939
  renderPopup(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService, analyticsFeedbackSessionId, options);
818
940
  });
819
941
  }
@@ -821,6 +943,8 @@ var BrowserPopupRenderer = class {
821
943
  this.visible = false;
822
944
  if (this.container) {
823
945
  this.container.style.display = "none";
946
+ this.container.style.visibility = "";
947
+ this.container.style.pointerEvents = "";
824
948
  this.container.innerHTML = "";
825
949
  }
826
950
  }
@@ -1565,6 +1689,75 @@ var CrashReporter = class {
1565
1689
  }
1566
1690
  };
1567
1691
 
1692
+ // src/core/popup-state-store.ts
1693
+ var POPUP_STATE_STORAGE_KEY = "deepdots.popups.state";
1694
+ var MAX_TRACKED_POPUP_ENTRIES = 200;
1695
+ function emptyState() {
1696
+ return { lastShown: {}, progress: {} };
1697
+ }
1698
+ function isValidTimestamp(value) {
1699
+ return typeof value === "number" && Number.isFinite(value);
1700
+ }
1701
+ function capEntries(entries, timestampOf) {
1702
+ if (entries.length <= MAX_TRACKED_POPUP_ENTRIES) return entries;
1703
+ return [...entries].sort((a, b) => timestampOf(b[1]) - timestampOf(a[1])).slice(0, MAX_TRACKED_POPUP_ENTRIES);
1704
+ }
1705
+ function readPopupState(storage) {
1706
+ let raw = null;
1707
+ try {
1708
+ raw = storage.getItem(POPUP_STATE_STORAGE_KEY);
1709
+ } catch {
1710
+ return emptyState();
1711
+ }
1712
+ if (!raw) return emptyState();
1713
+ let parsed;
1714
+ try {
1715
+ parsed = JSON.parse(raw);
1716
+ } catch {
1717
+ return emptyState();
1718
+ }
1719
+ if (typeof parsed !== "object" || parsed === null) return emptyState();
1720
+ const source = parsed;
1721
+ const state = emptyState();
1722
+ if (typeof source.lastShown === "object" && source.lastShown !== null) {
1723
+ const valid = Object.entries(source.lastShown).filter((entry) => isValidTimestamp(entry[1]));
1724
+ capEntries(valid, (timestamp) => timestamp).forEach(([popupId, timestamp]) => {
1725
+ state.lastShown[popupId] = timestamp;
1726
+ });
1727
+ }
1728
+ if (typeof source.progress === "object" && source.progress !== null) {
1729
+ const valid = Object.entries(source.progress).filter((entry) => {
1730
+ const value = entry[1];
1731
+ if (typeof value !== "object" || value === null) return false;
1732
+ const candidate = value;
1733
+ return POPUP_TRIGGER_CONDITION_STATUSES.includes(candidate.status) && isValidTimestamp(candidate.timestamp);
1734
+ });
1735
+ capEntries(valid, (entry) => entry.timestamp).forEach(([surveyId, entry]) => {
1736
+ state.progress[surveyId] = { status: entry.status, timestamp: entry.timestamp };
1737
+ });
1738
+ }
1739
+ return state;
1740
+ }
1741
+ function writePopupState(storage, state) {
1742
+ const capped = emptyState();
1743
+ capEntries(Object.entries(state.lastShown), (timestamp) => timestamp).forEach(([popupId, timestamp]) => {
1744
+ capped.lastShown[popupId] = timestamp;
1745
+ });
1746
+ capEntries(Object.entries(state.progress), (entry) => entry.timestamp).forEach(([surveyId, entry]) => {
1747
+ capped.progress[surveyId] = entry;
1748
+ });
1749
+ try {
1750
+ storage.setItem(POPUP_STATE_STORAGE_KEY, JSON.stringify(capped));
1751
+ } catch {
1752
+ }
1753
+ }
1754
+ function clearPopupState(storage) {
1755
+ try {
1756
+ storage.removeItem(POPUP_STATE_STORAGE_KEY);
1757
+ } catch {
1758
+ }
1759
+ }
1760
+
1568
1761
  // src/analytics/messaging.ts
1569
1762
  var MESSAGE_CHANNELS = ["push", "in_app"];
1570
1763
  function buildMessageParams(stage, o) {
@@ -1652,6 +1845,10 @@ var DeepdotsPopups = class {
1652
1845
  this.popupDefinitions = [];
1653
1846
  this.popupsLoaded = false;
1654
1847
  this.pendingAutoLaunch = false;
1848
+ // Estado que alimenta las reglas de redisplay. Copia de trabajo en memoria; la fuente
1849
+ // duradera es el storage (`popup-state-store`), rehidratada en `init()` — sin eso, una
1850
+ // recarga reiniciaba el cooldown y el popup volvía a salir. `answeredSurveys` se deriva
1851
+ // de las entradas COMPLETED de `surveyProgress` (los dos los escribe `markSurveyAnswered`).
1655
1852
  this.answeredSurveys = /* @__PURE__ */ new Set();
1656
1853
  this.surveyProgress = /* @__PURE__ */ new Map();
1657
1854
  this.lastShown = /* @__PURE__ */ new Map();
@@ -1727,6 +1924,7 @@ var DeepdotsPopups = class {
1727
1924
  };
1728
1925
  const storage = config.storage ?? createDefaultStorage();
1729
1926
  this.storage = storage;
1927
+ this.loadPopupState(storage);
1730
1928
  this.tracking = new TrackingManager({
1731
1929
  storage,
1732
1930
  clientUserId: this.config.userId,
@@ -1816,6 +2014,7 @@ var DeepdotsPopups = class {
1816
2014
  this.popupDefinitions = this.validatePopupDefinitions(defs);
1817
2015
  this.popupsLoaded = true;
1818
2016
  this.log("Popups loaded from API", this.popupDefinitions);
2017
+ if (this.popupDefinitions.length) this.renderer.preload?.();
1819
2018
  this.configureTriggersFromDefinitions();
1820
2019
  this.processDeferredExitQueue();
1821
2020
  if (this.pendingAutoLaunch) {
@@ -1994,6 +2193,10 @@ var DeepdotsPopups = class {
1994
2193
  }) : null;
1995
2194
  this.analytics?.resetUserScope();
1996
2195
  this.messageGuard.reset();
2196
+ this.lastShown.clear();
2197
+ this.surveyProgress.clear();
2198
+ this.answeredSurveys.clear();
2199
+ clearPopupState(storage);
1997
2200
  this.log("tracking \xB7 user_change \xB7 user_id:", this.tracking.getUserId());
1998
2201
  this.openSession();
1999
2202
  }
@@ -2165,6 +2368,7 @@ var DeepdotsPopups = class {
2165
2368
  this.log("Showing popup (definition)", def);
2166
2369
  this.surveyToPopupId.set(def.surveyId, def.id);
2167
2370
  this.lastShown.set(def.id, Date.now());
2371
+ this.persistPopupState();
2168
2372
  this.renderPopup(def.surveyId, def.productId, def.actions, def.style, def.title);
2169
2373
  this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
2170
2374
  }
@@ -2298,7 +2502,7 @@ var DeepdotsPopups = class {
2298
2502
  processDeferredExitQueue() {
2299
2503
  const queue = this.getDeferredExitQueue();
2300
2504
  if (!queue.length) return;
2301
- const pending = [];
2505
+ const pending2 = [];
2302
2506
  const now = Date.now();
2303
2507
  queue.forEach((item) => {
2304
2508
  const def = this.popupDefinitions.find((popup) => popup.id === item.id && popup.surveyId === item.surveyId);
@@ -2309,10 +2513,10 @@ var DeepdotsPopups = class {
2309
2513
  this.tryShowDeferredExitPopup(item);
2310
2514
  return;
2311
2515
  }
2312
- pending.push(item);
2516
+ pending2.push(item);
2313
2517
  this.scheduleDeferredExitPopup(item);
2314
2518
  });
2315
- this.setDeferredExitQueue(pending);
2519
+ this.setDeferredExitQueue(pending2);
2316
2520
  }
2317
2521
  scheduleDeferredExitPopup(item) {
2318
2522
  if (typeof window === "undefined") return;
@@ -2385,7 +2589,7 @@ var DeepdotsPopups = class {
2385
2589
  async fetchPopupsFromServer() {
2386
2590
  const apiKey = this.config?.apiKey;
2387
2591
  const baseUrl = this.baseUrl;
2388
- const userId = this.config?.userId;
2592
+ const userId = this.tracking?.getUserId() ?? this.config?.userId;
2389
2593
  if (!apiKey || !baseUrl) {
2390
2594
  this.log("Missing apiKey or baseUrl. Skipping popups fetch.");
2391
2595
  return [];
@@ -2529,7 +2733,7 @@ var DeepdotsPopups = class {
2529
2733
  }
2530
2734
  if (!this.popupContainer) return;
2531
2735
  const container = this.popupContainer;
2532
- void import("./renderPopup-EWGKG53W.mjs").then(({ renderPopup }) => {
2736
+ void import("./renderPopup-SNDPX5RX.mjs").then(({ renderPopup }) => {
2533
2737
  renderPopup(
2534
2738
  container,
2535
2739
  surveyId,
@@ -2644,6 +2848,29 @@ var DeepdotsPopups = class {
2644
2848
  return;
2645
2849
  }
2646
2850
  this.surveyProgress.set(surveyId, { status, timestamp: Date.now() });
2851
+ this.persistPopupState();
2852
+ }
2853
+ /**
2854
+ * Rehidrata el estado de redisplay desde el storage. `answeredSurveys` se reconstruye
2855
+ * desde las entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
2856
+ */
2857
+ loadPopupState(storage) {
2858
+ const state = readPopupState(storage);
2859
+ Object.entries(state.lastShown).forEach(([popupId, timestamp]) => {
2860
+ this.lastShown.set(popupId, timestamp);
2861
+ });
2862
+ Object.entries(state.progress).forEach(([surveyId, entry]) => {
2863
+ this.surveyProgress.set(surveyId, { status: entry.status, timestamp: entry.timestamp });
2864
+ if (entry.status === "COMPLETED") this.answeredSurveys.add(surveyId);
2865
+ });
2866
+ }
2867
+ /** Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria. */
2868
+ persistPopupState() {
2869
+ if (!this.storage) return;
2870
+ writePopupState(this.storage, {
2871
+ lastShown: Object.fromEntries(this.lastShown),
2872
+ progress: Object.fromEntries(this.surveyProgress)
2873
+ });
2647
2874
  }
2648
2875
  isPopupDefinition(value) {
2649
2876
  if (typeof value !== "object" || value === null) return false;
@@ -2787,16 +3014,24 @@ function DeepdotsProvider({ config, children }) {
2787
3014
  }, []);
2788
3015
  return /* @__PURE__ */ jsxs(DeepdotsContext.Provider, { value: sdk, children: [
2789
3016
  children,
2790
- survey ? /* @__PURE__ */ jsx(Modal, { visible: true, transparent: true, animationType: "slide", children: /* @__PURE__ */ jsx(View, { style: { flex: 1 }, children: /* @__PURE__ */ jsx(
2791
- WebView,
2792
- {
2793
- style: { flex: 1 },
2794
- originWhitelist: ["*"],
2795
- javaScriptEnabled: true,
2796
- source: { html: survey.html, baseUrl: "https://sdk.deepdots.com/" },
2797
- onMessage: (e) => rendererRef.current?.handleMessage(e.nativeEvent.data)
2798
- }
2799
- ) }) }) : null
3017
+ survey ? (
3018
+ // El Modal se abre en cuanto hay popup, y es el HTML del WebView el que se revela solo
3019
+ // cuando el survey está pintado (apertura diferida): con el Modal transparente, hasta ese
3020
+ // momento no se ve nada, ni velo ni spinner. No se difiere el Modal con `onReady` porque
3021
+ // React Native no monta los hijos de un Modal cerrado: el WebView no llegaría a cargar y
3022
+ // el aviso no llegaría nunca. El host que quiera diferir su contenedor debe montar el
3023
+ // WebView invisible (ver el ejemplo de `ReactNativePopupRenderer`).
3024
+ /* @__PURE__ */ jsx(Modal, { visible: true, transparent: true, animationType: "slide", children: /* @__PURE__ */ jsx(View, { style: { flex: 1 }, children: /* @__PURE__ */ jsx(
3025
+ WebView,
3026
+ {
3027
+ style: { flex: 1 },
3028
+ originWhitelist: ["*"],
3029
+ javaScriptEnabled: true,
3030
+ source: { html: survey.html, baseUrl: "https://sdk.deepdots.com/" },
3031
+ onMessage: (e) => rendererRef.current?.handleMessage(e.nativeEvent.data)
3032
+ }
3033
+ ) }) })
3034
+ ) : null
2800
3035
  ] });
2801
3036
  }
2802
3037
  export {