@magicfeedback/popup-sdk 1.8.1 → 1.8.3
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/README.md +32 -1
- package/dist/index.d.mts +71 -12
- package/dist/index.d.ts +71 -12
- package/dist/index.js +172 -33
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +172 -33
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +52 -5
- package/dist/react-native.d.ts +52 -5
- package/dist/react-native.js +172 -33
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +172 -33
- package/dist/react-native.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -966,10 +966,13 @@ function createDefaultRenderer() {
|
|
|
966
966
|
|
|
967
967
|
// src/analytics/analytics-manager.ts
|
|
968
968
|
function createDryRunSink(log = console.log) {
|
|
969
|
-
return (payload) =>
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
969
|
+
return (payload, meta) => {
|
|
970
|
+
if (meta?.resetSession) return;
|
|
971
|
+
log(
|
|
972
|
+
"[DeepdotsAnalytics] (dry-run \xB7 NOT sent \xB7 no init.analytics) POST /sdk/feedback \u2192",
|
|
973
|
+
JSON.stringify(payload, null, 2)
|
|
974
|
+
);
|
|
975
|
+
};
|
|
973
976
|
}
|
|
974
977
|
var dryRunSink = createDryRunSink();
|
|
975
978
|
var AnalyticsManager = class {
|
|
@@ -1036,6 +1039,19 @@ var AnalyticsManager = class {
|
|
|
1036
1039
|
this.exitMiniService(name);
|
|
1037
1040
|
}
|
|
1038
1041
|
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Olvida la sesión en curso SIN emitir nada: vacía el buffer y los mini-services activos
|
|
1044
|
+
* (sin `mini_service_exit`). Para una sesión que el backend ya cerró por inactividad: lo
|
|
1045
|
+
* que quedara pendiente pertenecía a ella y no debe llegar con la sesión nueva.
|
|
1046
|
+
*/
|
|
1047
|
+
discardSession(identity) {
|
|
1048
|
+
this.events = [];
|
|
1049
|
+
this.activeMiniServices.clear();
|
|
1050
|
+
try {
|
|
1051
|
+
void this.sink({ ...this.buildPayload(identity), events: [] }, { resetSession: true });
|
|
1052
|
+
} catch {
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1039
1055
|
/**
|
|
1040
1056
|
* Olvida lo que pertenecía al usuario anterior (user attributes + métricas) al cambiar de
|
|
1041
1057
|
* usuario. NO toca el buffer de eventos: el cierre de sesión ya lo vació con la identidad
|
|
@@ -1206,14 +1222,7 @@ function createFeedbackSink(options) {
|
|
|
1206
1222
|
const post = async (body, final, sessionEnd = false) => {
|
|
1207
1223
|
const json = JSON.stringify(body);
|
|
1208
1224
|
const small = json.length <= KEEPALIVE_MAX_BYTES;
|
|
1209
|
-
if (final
|
|
1210
|
-
const payload = typeof Blob !== "undefined" ? new Blob([json], { type: "application/json" }) : json;
|
|
1211
|
-
if (options.sendBeaconImpl(url, payload)) {
|
|
1212
|
-
options.log?.("[DeepdotsAnalytics] final flush via sendBeacon");
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1215
|
-
options.log?.("[DeepdotsAnalytics] sendBeacon rejected the batch; falling back to fetch");
|
|
1216
|
-
}
|
|
1225
|
+
if (final) options.log?.("[DeepdotsAnalytics] final flush via fetch keepalive");
|
|
1217
1226
|
const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
1218
1227
|
if (!f) {
|
|
1219
1228
|
options.log?.("[DeepdotsAnalytics] no fetch available; payload not sent", body);
|
|
@@ -1223,6 +1232,9 @@ function createFeedbackSink(options) {
|
|
|
1223
1232
|
method: "POST",
|
|
1224
1233
|
headers: { "Content-Type": "application/json" },
|
|
1225
1234
|
body: json,
|
|
1235
|
+
// La API autentica por `publicKey` en el body y responde `Allow-Origin: *`: una
|
|
1236
|
+
// petición con credenciales fallaría el CORS. Explícito para no depender del default.
|
|
1237
|
+
credentials: "omit",
|
|
1226
1238
|
// keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
|
|
1227
1239
|
...small ? { keepalive: true } : {}
|
|
1228
1240
|
});
|
|
@@ -1249,6 +1261,12 @@ function createFeedbackSink(options) {
|
|
|
1249
1261
|
}
|
|
1250
1262
|
};
|
|
1251
1263
|
return async (envelope, meta) => {
|
|
1264
|
+
if (meta?.resetSession) {
|
|
1265
|
+
feedbackSessionId = void 0;
|
|
1266
|
+
firstPostInFlight = null;
|
|
1267
|
+
options.onSessionReset?.();
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1252
1270
|
const final = meta?.final === true;
|
|
1253
1271
|
const sessionEnd = meta?.sessionEnd === true;
|
|
1254
1272
|
if (!feedbackSessionId && firstPostInFlight && !final) {
|
|
@@ -1315,6 +1333,23 @@ var NavigationObserver = class {
|
|
|
1315
1333
|
this.closeCurrent();
|
|
1316
1334
|
this.currentScreen = null;
|
|
1317
1335
|
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Olvida la pantalla actual SIN emitir su visita. Para una sesión que el backend ya dio
|
|
1338
|
+
* por cerrada: su visita no tiene dónde ir, y mandarla con la sesión nueva la contaría
|
|
1339
|
+
* donde no ocurrió. Después, `restart()` vuelve a empezar en la pantalla actual.
|
|
1340
|
+
*/
|
|
1341
|
+
discard() {
|
|
1342
|
+
this.currentScreen = null;
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Retoma el seguimiento en la pantalla actual tras un `stop()`, cuando se abre una sesión
|
|
1346
|
+
* nueva sin navegar (cambio de usuario, vuelta de la bfcache). No-op si nunca se instaló
|
|
1347
|
+
* (navegación manual por `setScreen`, RN) o si ya hay pantalla en curso.
|
|
1348
|
+
*/
|
|
1349
|
+
restart() {
|
|
1350
|
+
if (!this.installed || this.currentScreen != null || typeof window === "undefined") return;
|
|
1351
|
+
this.begin(window.location.href);
|
|
1352
|
+
}
|
|
1318
1353
|
closeCurrent() {
|
|
1319
1354
|
if (this.currentScreen == null) return;
|
|
1320
1355
|
const exit = this.now();
|
|
@@ -1835,6 +1870,10 @@ var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
|
|
|
1835
1870
|
var DAY_IN_MS = 24 * 60 * 60 * 1e3;
|
|
1836
1871
|
var ANALYTICS_MAX_BATCH_SIZE = 20;
|
|
1837
1872
|
var ANALYTICS_FLUSH_INTERVAL_MS = 3e4;
|
|
1873
|
+
var ANALYTICS_HEARTBEAT_MS = 5 * 6e4;
|
|
1874
|
+
var ANALYTICS_SESSION_TIMEOUT_MS = 30 * 6e4;
|
|
1875
|
+
var ANALYTICS_SERVER_IDLE_MS = 60 * 6e4;
|
|
1876
|
+
var ANALYTICS_SESSION_DISCARD_MS = ANALYTICS_SERVER_IDLE_MS - 10 * 6e4;
|
|
1838
1877
|
var RESERVED_EVENT_PREFIX = "deepdots_";
|
|
1839
1878
|
var CUSTOM_EVENT_PREFIX = "deepdots_event_";
|
|
1840
1879
|
var DeepdotsPopups = class {
|
|
@@ -1898,11 +1937,21 @@ var DeepdotsPopups = class {
|
|
|
1898
1937
|
* nueva al volver a foreground o al conceder el consentimiento más tarde.
|
|
1899
1938
|
*/
|
|
1900
1939
|
this.sessionOpen = false;
|
|
1940
|
+
/** Último `user_engagement` emitido (ms epoch): marca el latido. */
|
|
1941
|
+
this.lastEngagementAt = 0;
|
|
1942
|
+
/** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
|
|
1943
|
+
this.hiddenAt = null;
|
|
1901
1944
|
/**
|
|
1902
1945
|
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
1903
1946
|
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
1904
1947
|
*/
|
|
1905
1948
|
this.language = void 0;
|
|
1949
|
+
/** `geolocation` del init: si es false no se usa ni la caché ni el lookup. */
|
|
1950
|
+
this.geoEnabled = true;
|
|
1951
|
+
/** Solo se consulta a terceros si el país/ciudad se va a enviar (analytics configurado). */
|
|
1952
|
+
this.geoWanted = false;
|
|
1953
|
+
/** Evita más de un lookup por instancia (el consentimiento puede ir y venir). */
|
|
1954
|
+
this.geoLookupStarted = false;
|
|
1906
1955
|
}
|
|
1907
1956
|
/** Initialize the SDK with configuration */
|
|
1908
1957
|
init(config) {
|
|
@@ -1954,8 +2003,6 @@ var DeepdotsPopups = class {
|
|
|
1954
2003
|
baseUrl: this.baseUrl,
|
|
1955
2004
|
keys: config.analytics,
|
|
1956
2005
|
log: (...a) => this.log(...a),
|
|
1957
|
-
// sendBeacon: único transporte que sobrevive al cierre de la página.
|
|
1958
|
-
sendBeaconImpl: typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" ? (url, body) => navigator.sendBeacon(url, body) : void 0,
|
|
1959
2006
|
onSessionId: (id) => {
|
|
1960
2007
|
this.analyticsFeedbackSessionId = id;
|
|
1961
2008
|
},
|
|
@@ -1990,20 +2037,13 @@ var DeepdotsPopups = class {
|
|
|
1990
2037
|
if (this.tracking?.isTrackingEnabled()) {
|
|
1991
2038
|
this.crashReporter.install();
|
|
1992
2039
|
}
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
if (geo) {
|
|
1997
|
-
this.analytics?.updateDevice(geo);
|
|
1998
|
-
writeCachedGeo(storage, geo, Date.now());
|
|
1999
|
-
}
|
|
2000
|
-
}).catch(() => {
|
|
2001
|
-
});
|
|
2040
|
+
this.geoEnabled = config.geolocation ?? true;
|
|
2041
|
+
this.geoWanted = this.geoEnabled && !!config.analytics;
|
|
2042
|
+
this.resolveGeo();
|
|
2002
2043
|
this.navObserver = new NavigationObserver();
|
|
2003
2044
|
this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
|
|
2004
2045
|
this.navObserver.install();
|
|
2005
2046
|
this.engagement = new EngagementTracker();
|
|
2006
|
-
this.engagement.resume();
|
|
2007
2047
|
this.setupAnalyticsFlush();
|
|
2008
2048
|
this.openSession();
|
|
2009
2049
|
const pendingCrashes = this.crashReporter.drainPendingCrashes();
|
|
@@ -2041,6 +2081,7 @@ var DeepdotsPopups = class {
|
|
|
2041
2081
|
if (!enabled) this.closeSession("tracking_disabled");
|
|
2042
2082
|
this.tracking?.setTrackingEnabled(enabled);
|
|
2043
2083
|
if (enabled) this.openSession();
|
|
2084
|
+
if (enabled) this.resolveGeo();
|
|
2044
2085
|
this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
|
|
2045
2086
|
}
|
|
2046
2087
|
// ───────── Analytics (canal separado del feedback, vinculado por user_id) ─────────
|
|
@@ -2222,6 +2263,7 @@ var DeepdotsPopups = class {
|
|
|
2222
2263
|
this.navStarted = false;
|
|
2223
2264
|
this.analytics?.exitAllMiniServices();
|
|
2224
2265
|
this.flushEngagement();
|
|
2266
|
+
this.engagement?.pause();
|
|
2225
2267
|
this.track("deepdots_session_end", { reason });
|
|
2226
2268
|
this.flushAnalytics({ final: reason === "page_hide", sessionEnd: true });
|
|
2227
2269
|
this.analyticsFeedbackSessionId = void 0;
|
|
@@ -2236,7 +2278,14 @@ var DeepdotsPopups = class {
|
|
|
2236
2278
|
if (this.sessionOpen) return;
|
|
2237
2279
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
2238
2280
|
this.sessionOpen = true;
|
|
2281
|
+
this.lastEngagementAt = Date.now();
|
|
2239
2282
|
this.track("deepdots_session_start", {});
|
|
2283
|
+
this.navObserver?.restart();
|
|
2284
|
+
if (this.isDocumentVisible()) this.engagement?.resume();
|
|
2285
|
+
}
|
|
2286
|
+
/** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */
|
|
2287
|
+
isDocumentVisible() {
|
|
2288
|
+
return typeof document === "undefined" || document.visibilityState !== "hidden";
|
|
2240
2289
|
}
|
|
2241
2290
|
/** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
|
|
2242
2291
|
previewAnalytics() {
|
|
@@ -2249,8 +2298,8 @@ var DeepdotsPopups = class {
|
|
|
2249
2298
|
}
|
|
2250
2299
|
/**
|
|
2251
2300
|
* Envía el lote acumulado de analytics y vacía el buffer.
|
|
2252
|
-
* `final: true` (cierre de página/app)
|
|
2253
|
-
*
|
|
2301
|
+
* `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque
|
|
2302
|
+
* el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.
|
|
2254
2303
|
*/
|
|
2255
2304
|
flushAnalytics(options) {
|
|
2256
2305
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
@@ -2260,7 +2309,76 @@ var DeepdotsPopups = class {
|
|
|
2260
2309
|
flushEngagement() {
|
|
2261
2310
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
2262
2311
|
const ms = this.engagement?.consume() ?? 0;
|
|
2263
|
-
if (ms > 0)
|
|
2312
|
+
if (ms > 0) {
|
|
2313
|
+
this.track("deepdots_user_engagement", { engagement_time_msec: ms });
|
|
2314
|
+
this.lastEngagementAt = Date.now();
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
/** Tick del flush periódico: latido de engagement si toca, y envío de lo acumulado. */
|
|
2318
|
+
onAnalyticsFlushTick() {
|
|
2319
|
+
if (this.sessionOpen && this.isDocumentVisible() && Date.now() - this.lastEngagementAt >= ANALYTICS_HEARTBEAT_MS) {
|
|
2320
|
+
this.flushEngagement();
|
|
2321
|
+
}
|
|
2322
|
+
this.flushAnalytics();
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* La pestaña vuelve a verse. Tras más de ANALYTICS_SESSION_TIMEOUT_MS oculta, la sesión
|
|
2326
|
+
* anterior termina y empieza otra: seguir con el mismo sessionId mezclaba dos visitas y,
|
|
2327
|
+
* si el backend ya la había cerrado por inactividad, la partía en dos feedbacks.
|
|
2328
|
+
* - Oculta menos de ANALYTICS_SESSION_DISCARD_MS: el backend aún la tiene abierta → se
|
|
2329
|
+
* cierra con normalidad (page_view de la pantalla, session_end `idle_timeout`, completed).
|
|
2330
|
+
* - Más: ya la cerró el backend → se descarta sin enviar nada.
|
|
2331
|
+
*/
|
|
2332
|
+
onDocumentVisible() {
|
|
2333
|
+
const hiddenFor = this.hiddenAt === null ? 0 : Date.now() - this.hiddenAt;
|
|
2334
|
+
this.hiddenAt = null;
|
|
2335
|
+
if (!this.sessionOpen) return;
|
|
2336
|
+
if (hiddenFor < ANALYTICS_SESSION_TIMEOUT_MS) {
|
|
2337
|
+
this.engagement?.resume();
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
if (hiddenFor < ANALYTICS_SESSION_DISCARD_MS) {
|
|
2341
|
+
this.closeSession("idle_timeout");
|
|
2342
|
+
} else {
|
|
2343
|
+
this.discardSession();
|
|
2344
|
+
}
|
|
2345
|
+
this.openSession();
|
|
2346
|
+
}
|
|
2347
|
+
/** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
|
|
2348
|
+
discardSession() {
|
|
2349
|
+
this.sessionOpen = false;
|
|
2350
|
+
this.navObserver?.discard();
|
|
2351
|
+
this.analytics?.discardSession(this.analyticsIdentity());
|
|
2352
|
+
this.engagement?.consume();
|
|
2353
|
+
this.engagement?.pause();
|
|
2354
|
+
this.analyticsFeedbackSessionId = void 0;
|
|
2355
|
+
this.tracking?.setSessionId(null);
|
|
2356
|
+
this.log("tracking \xB7 session discarded after", ANALYTICS_SESSION_DISCARD_MS / 6e4, "min hidden");
|
|
2357
|
+
}
|
|
2358
|
+
/**
|
|
2359
|
+
* Geolocalización por IP (país/ciudad para analytics). La caché persistente es local y se
|
|
2360
|
+
* aplica siempre que `geolocation` no esté desactivado. El lookup a terceros (ipapi.co →
|
|
2361
|
+
* ipwho.is → ipinfo.io, fallback con timeout) solo se hace si el dato se va a ENVIAR:
|
|
2362
|
+
* analytics configurado + tracking activo + caché ausente o caducada. Una vez por instancia.
|
|
2363
|
+
*/
|
|
2364
|
+
resolveGeo() {
|
|
2365
|
+
const storage = this.storage;
|
|
2366
|
+
if (!this.geoEnabled || !storage || !this.analytics) return;
|
|
2367
|
+
const cachedGeo = readCachedGeo(storage, Date.now());
|
|
2368
|
+
if (cachedGeo) {
|
|
2369
|
+
this.analytics.updateDevice(cachedGeo);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
if (!this.geoWanted || this.geoLookupStarted) return;
|
|
2373
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
2374
|
+
this.geoLookupStarted = true;
|
|
2375
|
+
fetchGeo().then((geo) => {
|
|
2376
|
+
if (geo) {
|
|
2377
|
+
this.analytics?.updateDevice(geo);
|
|
2378
|
+
writeCachedGeo(storage, geo, Date.now());
|
|
2379
|
+
}
|
|
2380
|
+
}).catch(() => {
|
|
2381
|
+
});
|
|
2264
2382
|
}
|
|
2265
2383
|
analyticsIdentity() {
|
|
2266
2384
|
return {
|
|
@@ -2270,22 +2388,32 @@ var DeepdotsPopups = class {
|
|
|
2270
2388
|
}
|
|
2271
2389
|
/** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
|
|
2272
2390
|
setupAnalyticsFlush() {
|
|
2391
|
+
this.startAnalyticsFlushTimer();
|
|
2273
2392
|
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
2274
|
-
this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
|
|
2275
2393
|
window.addEventListener("pagehide", () => {
|
|
2276
2394
|
clearInterval(this.analyticsFlushTimer);
|
|
2277
2395
|
this.closeSession("page_hide");
|
|
2278
2396
|
});
|
|
2397
|
+
window.addEventListener("pageshow", (event) => {
|
|
2398
|
+
if (!event.persisted) return;
|
|
2399
|
+
this.startAnalyticsFlushTimer();
|
|
2400
|
+
this.openSession();
|
|
2401
|
+
});
|
|
2279
2402
|
document.addEventListener("visibilitychange", () => {
|
|
2280
2403
|
if (document.visibilityState === "hidden") {
|
|
2281
2404
|
this.flushEngagement();
|
|
2282
2405
|
this.engagement?.pause();
|
|
2283
2406
|
this.flushAnalytics();
|
|
2407
|
+
this.hiddenAt = Date.now();
|
|
2284
2408
|
} else if (document.visibilityState === "visible") {
|
|
2285
|
-
this.
|
|
2409
|
+
this.onDocumentVisible();
|
|
2286
2410
|
}
|
|
2287
2411
|
});
|
|
2288
2412
|
}
|
|
2413
|
+
startAnalyticsFlushTimer() {
|
|
2414
|
+
clearInterval(this.analyticsFlushTimer);
|
|
2415
|
+
this.analyticsFlushTimer = setInterval(() => this.onAnalyticsFlushTick(), ANALYTICS_FLUSH_INTERVAL_MS);
|
|
2416
|
+
}
|
|
2289
2417
|
/** Enable auto-launch functionality with configured triggers */
|
|
2290
2418
|
autoLaunch() {
|
|
2291
2419
|
if (!this.initialized) {
|
|
@@ -2379,6 +2507,7 @@ var DeepdotsPopups = class {
|
|
|
2379
2507
|
this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
|
|
2380
2508
|
}
|
|
2381
2509
|
shouldShow(def, pathUrl, skipPathCheck = false) {
|
|
2510
|
+
if (this.storage) this.loadPopupState(this.storage);
|
|
2382
2511
|
if (this.isPathExcluded(def, pathUrl)) {
|
|
2383
2512
|
return false;
|
|
2384
2513
|
}
|
|
@@ -2905,22 +3034,32 @@ var DeepdotsPopups = class {
|
|
|
2905
3034
|
this.persistPopupState();
|
|
2906
3035
|
}
|
|
2907
3036
|
/**
|
|
2908
|
-
*
|
|
2909
|
-
*
|
|
3037
|
+
* Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana
|
|
3038
|
+
* la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias
|
|
3039
|
+
* pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión
|
|
3040
|
+
* de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las
|
|
3041
|
+
* entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
|
|
2910
3042
|
*/
|
|
2911
3043
|
loadPopupState(storage) {
|
|
2912
3044
|
const state = readPopupState(storage);
|
|
2913
3045
|
Object.entries(state.lastShown).forEach(([popupId, timestamp]) => {
|
|
2914
|
-
this.lastShown.set(popupId, timestamp);
|
|
3046
|
+
if (timestamp > (this.lastShown.get(popupId) ?? -Infinity)) this.lastShown.set(popupId, timestamp);
|
|
2915
3047
|
});
|
|
2916
3048
|
Object.entries(state.progress).forEach(([surveyId, entry]) => {
|
|
3049
|
+
const current = this.surveyProgress.get(surveyId);
|
|
3050
|
+
if (current && current.timestamp >= entry.timestamp) return;
|
|
2917
3051
|
this.surveyProgress.set(surveyId, { status: entry.status, timestamp: entry.timestamp });
|
|
2918
3052
|
if (entry.status === "COMPLETED") this.answeredSurveys.add(surveyId);
|
|
2919
3053
|
});
|
|
2920
3054
|
}
|
|
2921
|
-
/**
|
|
3055
|
+
/**
|
|
3056
|
+
* Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.
|
|
3057
|
+
* Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña
|
|
3058
|
+
* hubiese guardado desde nuestro `init()`.
|
|
3059
|
+
*/
|
|
2922
3060
|
persistPopupState() {
|
|
2923
3061
|
if (!this.storage) return;
|
|
3062
|
+
this.loadPopupState(this.storage);
|
|
2924
3063
|
writePopupState(this.storage, {
|
|
2925
3064
|
lastShown: Object.fromEntries(this.lastShown),
|
|
2926
3065
|
progress: Object.fromEntries(this.surveyProgress)
|