@magicfeedback/popup-sdk 1.6.0 → 1.7.1

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,4 +1,7 @@
1
1
  import {
2
+ LABELS,
3
+ RESOLVE_LOCALE_JS,
4
+ RTL_LOCALES,
2
5
  STORAGE_KEYS,
3
6
  TrackingManager,
4
7
  __require,
@@ -6,10 +9,13 @@ import {
6
9
  buildFontFamilyValue,
7
10
  buildSurveyIdentity,
8
11
  createDefaultStorage,
12
+ directionFor,
13
+ getLabels,
14
+ resolveActionLabels,
9
15
  sdkWarn,
10
16
  setLogger,
11
17
  style_default
12
- } from "./chunk-Z3LLJMSH.mjs";
18
+ } from "./chunk-WNIUIOKF.mjs";
13
19
 
14
20
  // src/react-native/index.tsx
15
21
  import { createContext, useContext, useEffect, useRef, useState } from "react";
@@ -217,9 +223,13 @@ var POSITION_MAP = {
217
223
  function jsonForScript(value) {
218
224
  return JSON.stringify(value ?? null).replace(/</g, "\\u003C").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
219
225
  }
226
+ function trimmedOrNull(value) {
227
+ const trimmed = typeof value === "string" ? value.trim() : "";
228
+ return trimmed ? value : null;
229
+ }
220
230
  function buildSurveyHtml(opts) {
221
231
  const env = opts.env === "development" ? "dev" : "prod";
222
- const version = opts.version ?? "2.2.8";
232
+ const version = opts.version ?? "2.2.22";
223
233
  const cdn = `https://cdn.jsdelivr.net/npm/@magicfeedback/native@${version}/dist/magicfeedback-sdk.browser.js`;
224
234
  const sid = jsonForScript(opts.surveyId);
225
235
  const pid = jsonForScript(opts.productId);
@@ -251,10 +261,22 @@ function buildSurveyHtml(opts) {
251
261
  const titleJson = jsonForScript(opts.title ?? "");
252
262
  const customCss = (opts.surveyCss ?? "").replace(/<\/\s*style/gi, "<\\/style");
253
263
  const progressPref = opts.showProgressBar === void 0 ? "null" : String(opts.showProgressBar);
254
- const backLabel = jsonForScript(opts.actions?.back?.label ?? "Back");
255
- const startLabel = jsonForScript(opts.actions?.start?.label ?? "Start survey");
256
- const completeLabel = jsonForScript(opts.actions?.complete?.label ?? "Complete survey");
257
- const submitLabel = jsonForScript(opts.actions?.accept?.label ?? "Send");
264
+ const initLabels = getLabels(opts.language);
265
+ const initActionLabels = resolveActionLabels(opts.actions, opts.language);
266
+ const backLabel = jsonForScript(initActionLabels.back);
267
+ const startLabel = jsonForScript(initActionLabels.start);
268
+ const completeLabel = jsonForScript(initActionLabels.complete);
269
+ const submitLabel = jsonForScript(initActionLabels.accept);
270
+ const labelsTableJson = jsonForScript(LABELS);
271
+ const actionOverridesJson = jsonForScript({
272
+ accept: trimmedOrNull(opts.actions?.accept?.label),
273
+ start: trimmedOrNull(opts.actions?.start?.label),
274
+ complete: trimmedOrNull(opts.actions?.complete?.label),
275
+ back: trimmedOrNull(opts.actions?.back?.label)
276
+ });
277
+ const initLangJson = jsonForScript(opts.language ?? "");
278
+ const initDirection = directionFor(opts.language);
279
+ const rtlLocalesJson = jsonForScript(RTL_LOCALES);
258
280
  return `<!DOCTYPE html><html><head>
259
281
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
260
282
  <meta name="color-scheme" content="${colorScheme}"/>
@@ -267,8 +289,8 @@ body{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${
267
289
  .deepdots-popup-header{display:flex;justify-content:space-between;align-items:center;gap:12px;width:100%;flex:0 0 auto}
268
290
  /* Neutraliza la regla \`.deepdots-popup h2\` del CSS del survey (uppercase, centrado y
269
291
  margin-bottom 40px), pensada para los enunciados y no para el t\xEDtulo de la cabecera. */
270
- #dd-title{margin:0;font-family:var(--deepdots-font,'Montserrat',inherit);font-size:17px;font-weight:600;line-height:1.3;color:${textPrimary};text-transform:none;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
271
- #dd-close{background:transparent;border:none;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:8px;cursor:pointer;color:${textPrimary};padding:4px;flex:0 0 auto;margin-left:auto}
292
+ #dd-title{margin:0;font-family:var(--deepdots-font,'Montserrat',inherit);font-size:17px;font-weight:600;line-height:1.3;color:${textPrimary};text-transform:none;text-align:start;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
293
+ #dd-close{background:transparent;border:none;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:8px;cursor:pointer;color:${textPrimary};padding:4px;flex:0 0 auto;margin-inline-start:auto}
272
294
  /* Progreso alineado con la cabecera: sin padding horizontal propio, lo marca la tarjeta.
273
295
  El vertical es el mismo arriba y abajo, y del mismo valor que el padding de la tarjeta. */
274
296
  .deepdots-progress{display:none;flex-direction:column;gap:8px;width:100%;flex:0 0 auto;padding:16px 0;box-sizing:border-box}
@@ -325,10 +347,10 @@ body{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${
325
347
  ${customCss}
326
348
  </style>
327
349
  </head><body>
328
- <div class="${popupClass}" id="dd-popup">
350
+ <div class="${popupClass}" id="dd-popup" dir="${initDirection}">
329
351
  <div class="deepdots-popup-header">
330
352
  <h2 id="dd-title" class="deepdots-popup-title" hidden></h2>
331
- <button type="button" id="dd-close" aria-label="Close popup">
353
+ <button type="button" id="dd-close" aria-label="${initLabels.closeAria}">
332
354
  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
333
355
  <path d="M5 5L19 19M5 19L19 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="butt"/>
334
356
  </svg>
@@ -337,23 +359,23 @@ ${customCss}
337
359
  <div class="deepdots-progress" id="dd-progress">
338
360
  <div class="deepdots-progress-head">
339
361
  <span id="dd-progress-label"><span id="dd-progress-current"></span><span id="dd-progress-total"></span></span>
340
- <span id="dd-progress-followup">Follow-up</span>
362
+ <span id="dd-progress-followup">${initLabels.followUp}</span>
341
363
  </div>
342
364
  <div class="deepdots-progress-track"><div id="dd-progress-bar"></div></div>
343
365
  </div>
344
366
  <div class="deepdots-popup-container-content" id="dd-content">
345
367
  <div class="deepdots-popup-main" id="dd-main">
346
368
  <div id="dd-form-wrapper">
347
- <div class="mf-spinner" id="dd-spinner" role="status" aria-label="Loading survey"><div class="mf-spinner-circle"></div></div>
369
+ <div class="mf-spinner" id="dd-spinner" role="status" aria-label="${initLabels.loadingAria}"><div class="mf-spinner-circle"></div></div>
348
370
  <div id="mf"></div>
349
371
  </div>
350
372
  <div class="deepdots-error-hint" id="dd-error" role="alert" aria-live="polite"></div>
351
373
  </div>
352
374
  <div class="deepdots-popup-footer" id="dd-footer">
353
- <button type="button" class="dd-nav-btn" id="dd-submit">Send</button>
354
- <button type="button" class="dd-nav-btn" id="dd-back">Back</button>
355
- <button type="button" class="dd-nav-btn" id="dd-complete">Complete survey</button>
356
- <button type="button" class="dd-nav-btn" id="dd-start">Start survey</button>
375
+ <button type="button" class="dd-nav-btn" id="dd-submit"></button>
376
+ <button type="button" class="dd-nav-btn" id="dd-back"></button>
377
+ <button type="button" class="dd-nav-btn" id="dd-complete"></button>
378
+ <button type="button" class="dd-nav-btn" id="dd-start"></button>
357
379
  </div>
358
380
  </div>
359
381
  </div>
@@ -382,6 +404,32 @@ ${customCss}
382
404
  var progressTotal=document.getElementById('dd-progress-total');
383
405
  var progressFollowUp=document.getElementById('dd-progress-followup');
384
406
  var progressBar=document.getElementById('dd-progress-bar');
407
+ var closeEl=document.getElementById('dd-close');
408
+
409
+ // --- i18n del chrome -------------------------------------------------------------------
410
+ // La tabla completa viaja embebida porque el idioma del survey (\`formData.lang[0]\`) solo se
411
+ // conoce ya dentro del WebView. \`ddResolveLocale\` es el espejo del \`resolveLocale\` de TS
412
+ // (paridad cubierta por test).
413
+ var DD_LABELS=${labelsTableJson};
414
+ var DD_ACTION_OVERRIDES=${actionOverridesJson};
415
+ ${RESOLVE_LOCALE_JS}
416
+ var DD_LABELS_ACTIVE=DD_LABELS[ddResolveLocale(${initLangJson})];
417
+ var DD_RTL=${rtlLocalesJson};
418
+ function ddIsRtl(lang){ return DD_RTL.indexOf(ddResolveLocale(lang))!==-1; }
419
+ function ddApplyLabels(lang){
420
+ var next=DD_LABELS[ddResolveLocale(lang)];
421
+ if(!next) return;
422
+ DD_LABELS_ACTIVE=next;
423
+ // El survey ya se ha volteado a s\xED mismo: el chrome tiene que acompa\xF1arlo.
424
+ popup.setAttribute('dir', ddIsRtl(lang) ? 'rtl' : 'ltr');
425
+ backBtn.textContent=DD_ACTION_OVERRIDES.back||next.back;
426
+ startBtn.textContent=DD_ACTION_OVERRIDES.start||next.start;
427
+ completeBtn.textContent=DD_ACTION_OVERRIDES.complete||next.complete;
428
+ submitBtn.textContent=DD_ACTION_OVERRIDES.accept||next.accept;
429
+ if(closeEl){ closeEl.setAttribute('aria-label',next.closeAria); }
430
+ if(spinner){ spinner.setAttribute('aria-label',next.loadingAria); }
431
+ progressFollowUp.textContent=next.followUp;
432
+ }
385
433
 
386
434
  function setTitle(t){
387
435
  if(!t) return;
@@ -422,8 +470,8 @@ ${customCss}
422
470
  progressCurrent.textContent=Math.round(pct)+'%';
423
471
  progressTotal.textContent='';
424
472
  } else {
425
- progressCurrent.textContent='Question '+label;
426
- progressTotal.textContent=' of '+total;
473
+ progressCurrent.textContent=DD_LABELS_ACTIVE.question+' '+label;
474
+ progressTotal.textContent=' '+DD_LABELS_ACTIVE.of+' '+total;
427
475
  }
428
476
  progressFollowUp.style.display=p&&p.followup?'inline-block':'none';
429
477
  }
@@ -433,7 +481,7 @@ ${customCss}
433
481
  completeBtn.textContent=${completeLabel};
434
482
  submitBtn.textContent=${submitLabel};
435
483
 
436
- document.getElementById('dd-close').onclick=function(){ emitJSON('popup_close'); };
484
+ closeEl.onclick=function(){ emitJSON('popup_close'); };
437
485
  backBtn.onclick=function(){ if(window.DeepdotsForm && window.DeepdotsForm.back){ window.DeepdotsForm.back(); } };
438
486
  startBtn.onclick=function(){
439
487
  if(window.DeepdotsForm && window.DeepdotsForm.startForm){
@@ -528,6 +576,14 @@ ${customCss}
528
576
  // fallback es un literal gen\xE9rico que ignora style.successMessage.
529
577
  addSuccessScreen:false,
530
578
  onLoadedEvent:function(args){
579
+ // Idioma del survey: manda sobre el del init para que el chrome no se quede en
580
+ // ingl\xE9s delante de un survey traducido.
581
+ var langs=args && args.formData ? args.formData.lang : null;
582
+ if(langs && langs.length){
583
+ for(var li=0;li<langs.length;li++){
584
+ if(typeof langs[li]==='string' && langs[li].trim()){ ddApplyLabels(langs[li]); break; }
585
+ }
586
+ }
531
587
  var s=args && args.formData ? args.formData.style : null;
532
588
  if(s && !stylesInjected){
533
589
  stylesInjected=true;
@@ -554,8 +610,10 @@ ${customCss}
554
610
  if(s.logoSize==='small'){ logoImg.style.maxHeight='30px'; }
555
611
  else if(s.logoSize==='medium'){ logoImg.style.maxHeight='50px'; }
556
612
  else if(s.logoSize==='large'){ logoImg.style.maxHeight='70px'; }
557
- if(s.logoPosition==='left'){ logoImg.style.margin='12px 16px 0 0'; logoImg.style.marginLeft='0'; }
558
- else if(s.logoPosition==='right'){ logoImg.style.margin='12px 0 0 16px'; logoImg.style.marginLeft='auto'; }
613
+ // M\xE1rgenes l\xF3gicos: con un survey RTL el popup va con dir="rtl" y el logo
614
+ // tiene que acompa\xF1ar al chrome. En LTR el resultado es el mismo de antes.
615
+ if(s.logoPosition==='left'){ logoImg.style.margin='12px 0 0 0'; logoImg.style.setProperty('margin-inline-start','0'); logoImg.style.setProperty('margin-inline-end','16px'); }
616
+ else if(s.logoPosition==='right'){ logoImg.style.margin='12px 0 0 0'; logoImg.style.setProperty('margin-inline-start','auto'); logoImg.style.setProperty('margin-inline-end','0'); }
559
617
  else if(s.logoPosition==='center'){ logoImg.style.margin='12px auto 0 auto'; }
560
618
  // Encima de la barra de progreso, como hermano del header (espejo de ui/logo.ts en
561
619
  // web). Fuera de #dd-main, as\xED el logo no se va con el scroll del formulario.
@@ -581,12 +639,12 @@ ${customCss}
581
639
  setLoading(false);
582
640
  if(error.toLowerCase().indexOf('no response')!==-1){
583
641
  // La p\xE1gina no ha cambiado: el estado de navegaci\xF3n se queda como estaba.
584
- errorHint.textContent='Please answer the required question to continue.';
642
+ errorHint.textContent=DD_LABELS_ACTIVE.errorRequired;
585
643
  errorHint.style.display='block';
586
644
  updateNavButtons();
587
645
  return;
588
646
  }
589
- errorHint.textContent='An error occurred while submitting. Please try again or close the popup.';
647
+ errorHint.textContent=DD_LABELS_ACTIVE.errorSubmit;
590
648
  errorHint.style.display='block';
591
649
  return;
592
650
  }
@@ -658,7 +716,8 @@ var ReactNativePopupRenderer = class {
658
716
  chrome: renderChrome,
659
717
  title: options?.title,
660
718
  showProgressBar: options?.showProgressBar,
661
- surveyCss: options?.surveyCss
719
+ surveyCss: options?.surveyCss,
720
+ language: options?.language
662
721
  });
663
722
  if (this.options.onShow) {
664
723
  this.options.onShow({ surveyId, productId, html });
@@ -754,7 +813,7 @@ var BrowserPopupRenderer = class {
754
813
  if (!this.container) return;
755
814
  this.visible = true;
756
815
  const container = this.container;
757
- void import("./renderPopup-SELXQ3UE.mjs").then(({ renderPopup }) => {
816
+ void import("./renderPopup-EWGKG53W.mjs").then(({ renderPopup }) => {
758
817
  renderPopup(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService, analyticsFeedbackSessionId, options);
759
818
  });
760
819
  }
@@ -1506,6 +1565,75 @@ var CrashReporter = class {
1506
1565
  }
1507
1566
  };
1508
1567
 
1568
+ // src/core/popup-state-store.ts
1569
+ var POPUP_STATE_STORAGE_KEY = "deepdots.popups.state";
1570
+ var MAX_TRACKED_POPUP_ENTRIES = 200;
1571
+ function emptyState() {
1572
+ return { lastShown: {}, progress: {} };
1573
+ }
1574
+ function isValidTimestamp(value) {
1575
+ return typeof value === "number" && Number.isFinite(value);
1576
+ }
1577
+ function capEntries(entries, timestampOf) {
1578
+ if (entries.length <= MAX_TRACKED_POPUP_ENTRIES) return entries;
1579
+ return [...entries].sort((a, b) => timestampOf(b[1]) - timestampOf(a[1])).slice(0, MAX_TRACKED_POPUP_ENTRIES);
1580
+ }
1581
+ function readPopupState(storage) {
1582
+ let raw = null;
1583
+ try {
1584
+ raw = storage.getItem(POPUP_STATE_STORAGE_KEY);
1585
+ } catch {
1586
+ return emptyState();
1587
+ }
1588
+ if (!raw) return emptyState();
1589
+ let parsed;
1590
+ try {
1591
+ parsed = JSON.parse(raw);
1592
+ } catch {
1593
+ return emptyState();
1594
+ }
1595
+ if (typeof parsed !== "object" || parsed === null) return emptyState();
1596
+ const source = parsed;
1597
+ const state = emptyState();
1598
+ if (typeof source.lastShown === "object" && source.lastShown !== null) {
1599
+ const valid = Object.entries(source.lastShown).filter((entry) => isValidTimestamp(entry[1]));
1600
+ capEntries(valid, (timestamp) => timestamp).forEach(([popupId, timestamp]) => {
1601
+ state.lastShown[popupId] = timestamp;
1602
+ });
1603
+ }
1604
+ if (typeof source.progress === "object" && source.progress !== null) {
1605
+ const valid = Object.entries(source.progress).filter((entry) => {
1606
+ const value = entry[1];
1607
+ if (typeof value !== "object" || value === null) return false;
1608
+ const candidate = value;
1609
+ return POPUP_TRIGGER_CONDITION_STATUSES.includes(candidate.status) && isValidTimestamp(candidate.timestamp);
1610
+ });
1611
+ capEntries(valid, (entry) => entry.timestamp).forEach(([surveyId, entry]) => {
1612
+ state.progress[surveyId] = { status: entry.status, timestamp: entry.timestamp };
1613
+ });
1614
+ }
1615
+ return state;
1616
+ }
1617
+ function writePopupState(storage, state) {
1618
+ const capped = emptyState();
1619
+ capEntries(Object.entries(state.lastShown), (timestamp) => timestamp).forEach(([popupId, timestamp]) => {
1620
+ capped.lastShown[popupId] = timestamp;
1621
+ });
1622
+ capEntries(Object.entries(state.progress), (entry) => entry.timestamp).forEach(([surveyId, entry]) => {
1623
+ capped.progress[surveyId] = entry;
1624
+ });
1625
+ try {
1626
+ storage.setItem(POPUP_STATE_STORAGE_KEY, JSON.stringify(capped));
1627
+ } catch {
1628
+ }
1629
+ }
1630
+ function clearPopupState(storage) {
1631
+ try {
1632
+ storage.removeItem(POPUP_STATE_STORAGE_KEY);
1633
+ } catch {
1634
+ }
1635
+ }
1636
+
1509
1637
  // src/analytics/messaging.ts
1510
1638
  var MESSAGE_CHANNELS = ["push", "in_app"];
1511
1639
  function buildMessageParams(stage, o) {
@@ -1593,6 +1721,10 @@ var DeepdotsPopups = class {
1593
1721
  this.popupDefinitions = [];
1594
1722
  this.popupsLoaded = false;
1595
1723
  this.pendingAutoLaunch = false;
1724
+ // Estado que alimenta las reglas de redisplay. Copia de trabajo en memoria; la fuente
1725
+ // duradera es el storage (`popup-state-store`), rehidratada en `init()` — sin eso, una
1726
+ // recarga reiniciaba el cooldown y el popup volvía a salir. `answeredSurveys` se deriva
1727
+ // de las entradas COMPLETED de `surveyProgress` (los dos los escribe `markSurveyAnswered`).
1596
1728
  this.answeredSurveys = /* @__PURE__ */ new Set();
1597
1729
  this.surveyProgress = /* @__PURE__ */ new Map();
1598
1730
  this.lastShown = /* @__PURE__ */ new Map();
@@ -1668,6 +1800,7 @@ var DeepdotsPopups = class {
1668
1800
  };
1669
1801
  const storage = config.storage ?? createDefaultStorage();
1670
1802
  this.storage = storage;
1803
+ this.loadPopupState(storage);
1671
1804
  this.tracking = new TrackingManager({
1672
1805
  storage,
1673
1806
  clientUserId: this.config.userId,
@@ -1935,6 +2068,10 @@ var DeepdotsPopups = class {
1935
2068
  }) : null;
1936
2069
  this.analytics?.resetUserScope();
1937
2070
  this.messageGuard.reset();
2071
+ this.lastShown.clear();
2072
+ this.surveyProgress.clear();
2073
+ this.answeredSurveys.clear();
2074
+ clearPopupState(storage);
1938
2075
  this.log("tracking \xB7 user_change \xB7 user_id:", this.tracking.getUserId());
1939
2076
  this.openSession();
1940
2077
  }
@@ -2106,6 +2243,7 @@ var DeepdotsPopups = class {
2106
2243
  this.log("Showing popup (definition)", def);
2107
2244
  this.surveyToPopupId.set(def.surveyId, def.id);
2108
2245
  this.lastShown.set(def.id, Date.now());
2246
+ this.persistPopupState();
2109
2247
  this.renderPopup(def.surveyId, def.productId, def.actions, def.style, def.title);
2110
2248
  this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
2111
2249
  }
@@ -2464,13 +2602,13 @@ var DeepdotsPopups = class {
2464
2602
  miniService,
2465
2603
  analyticsFeedbackSessionId,
2466
2604
  this.renderChrome,
2467
- { title, showProgressBar: this.showProgressBar, surveyCss: this.surveyCss }
2605
+ { title, showProgressBar: this.showProgressBar, surveyCss: this.surveyCss, language: this.language }
2468
2606
  );
2469
2607
  return;
2470
2608
  }
2471
2609
  if (!this.popupContainer) return;
2472
2610
  const container = this.popupContainer;
2473
- void import("./renderPopup-SELXQ3UE.mjs").then(({ renderPopup }) => {
2611
+ void import("./renderPopup-EWGKG53W.mjs").then(({ renderPopup }) => {
2474
2612
  renderPopup(
2475
2613
  container,
2476
2614
  surveyId,
@@ -2484,7 +2622,7 @@ var DeepdotsPopups = class {
2484
2622
  sessionId,
2485
2623
  miniService,
2486
2624
  analyticsFeedbackSessionId,
2487
- { title, showProgressBar: this.showProgressBar, surveyCss: this.surveyCss }
2625
+ { title, showProgressBar: this.showProgressBar, surveyCss: this.surveyCss, language: this.language }
2488
2626
  );
2489
2627
  });
2490
2628
  }
@@ -2585,6 +2723,29 @@ var DeepdotsPopups = class {
2585
2723
  return;
2586
2724
  }
2587
2725
  this.surveyProgress.set(surveyId, { status, timestamp: Date.now() });
2726
+ this.persistPopupState();
2727
+ }
2728
+ /**
2729
+ * Rehidrata el estado de redisplay desde el storage. `answeredSurveys` se reconstruye
2730
+ * desde las entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
2731
+ */
2732
+ loadPopupState(storage) {
2733
+ const state = readPopupState(storage);
2734
+ Object.entries(state.lastShown).forEach(([popupId, timestamp]) => {
2735
+ this.lastShown.set(popupId, timestamp);
2736
+ });
2737
+ Object.entries(state.progress).forEach(([surveyId, entry]) => {
2738
+ this.surveyProgress.set(surveyId, { status: entry.status, timestamp: entry.timestamp });
2739
+ if (entry.status === "COMPLETED") this.answeredSurveys.add(surveyId);
2740
+ });
2741
+ }
2742
+ /** Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria. */
2743
+ persistPopupState() {
2744
+ if (!this.storage) return;
2745
+ writePopupState(this.storage, {
2746
+ lastShown: Object.fromEntries(this.lastShown),
2747
+ progress: Object.fromEntries(this.surveyProgress)
2748
+ });
2588
2749
  }
2589
2750
  isPopupDefinition(value) {
2590
2751
  if (typeof value !== "object" || value === null) return false;