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