@magicfeedback/popup-sdk 1.10.0 → 1.12.0
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 +23 -1
- package/dist/index.d.mts +84 -4
- package/dist/index.d.ts +84 -4
- package/dist/index.js +253 -23
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +235 -20
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +61 -3
- package/dist/react-native.d.ts +61 -3
- package/dist/react-native.js +254 -23
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +236 -20
- package/dist/react-native.mjs.map +1 -1
- package/dist/{renderPopup-WLOFOCDR.mjs → renderPopup-JF6IB6HL.mjs} +22 -7
- package/dist/renderPopup-JF6IB6HL.mjs.map +1 -0
- package/dist/{renderPopup-424HR5LF.mjs → renderPopup-KSZJ5AN4.mjs} +22 -7
- package/dist/renderPopup-KSZJ5AN4.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/renderPopup-424HR5LF.mjs.map +0 -1
- package/dist/renderPopup-WLOFOCDR.mjs.map +0 -1
package/dist/react-native.mjs
CHANGED
|
@@ -594,11 +594,25 @@ ${customCss}
|
|
|
594
594
|
${PRIMARY_COLOR_JS}
|
|
595
595
|
${BUSY_JS}
|
|
596
596
|
${REVEAL_JS}
|
|
597
|
+
// Foco en la primera pregunta si es de escribir. Al navegar lo pone @magicfeedback/native
|
|
598
|
+
// (autofocus:'navigation'); la apertura la decide el popup, que es quien sabe cu\xE1ndo se revela.
|
|
599
|
+
// Necesita el popup revelado y el survey cargado, que llegan en cualquier orden, y solo con
|
|
600
|
+
// rat\xF3n o trackpad: en t\xE1ctil sacar\xEDa el teclado sin que el usuario haya tocado nada.
|
|
601
|
+
var ddRevealed=false, ddSurveyLoaded=false, ddOpenFocusDone=false;
|
|
602
|
+
function ddAutofocusOnOpen(){
|
|
603
|
+
if(ddOpenFocusDone || !ddRevealed || !ddSurveyLoaded){ return; }
|
|
604
|
+
ddOpenFocusDone=true;
|
|
605
|
+
var fine=typeof window.matchMedia==='function' && window.matchMedia('(pointer: fine)').matches;
|
|
606
|
+
var f=window.DeepdotsForm;
|
|
607
|
+
if(fine && f && typeof f.focusFirstQuestion==='function'){ f.focusFirstQuestion(); }
|
|
608
|
+
}
|
|
597
609
|
// El popup se ense\xF1a cuando el survey est\xE1 pintado; \`ready\` deja al host abrir su propio
|
|
598
610
|
// Modal en ese momento en vez de montar un WebView en blanco.
|
|
599
611
|
var ddReveal=ddCreateReveal(popup, function(){
|
|
600
612
|
document.body.classList.add('dd-ready');
|
|
601
613
|
emitJSON('ready');
|
|
614
|
+
ddRevealed=true;
|
|
615
|
+
ddAutofocusOnOpen();
|
|
602
616
|
}, ${REVEAL_TIMEOUT_MS});
|
|
603
617
|
|
|
604
618
|
function setLoading(isLoading){
|
|
@@ -639,6 +653,9 @@ ${REVEAL_JS}
|
|
|
639
653
|
// Surveys multi-idioma: native pide a la API este idioma (el del init o el del
|
|
640
654
|
// dispositivo); si el survey no lo tiene, la API sirve el idioma por defecto.
|
|
641
655
|
lang:${initLangJson}||undefined,
|
|
656
|
+
// Foco en la primera pregunta de escribir tras Start, Siguiente o Atr\xE1s. Las versiones
|
|
657
|
+
// de native sin esta opci\xF3n la ignoran.
|
|
658
|
+
autofocus:'navigation',
|
|
642
659
|
onLoadedEvent:function(args){
|
|
643
660
|
// Idioma en el que native muestra el survey: manda sobre el del init para que el
|
|
644
661
|
// chrome no se quede en ingl\xE9s delante de un survey traducido. Native < 2.3 no lo
|
|
@@ -698,6 +715,8 @@ ${REVEAL_JS}
|
|
|
698
715
|
updateProgress({ progress:form.progress, total:form.total, completed:false, followup:false });
|
|
699
716
|
emitJSON('loaded');
|
|
700
717
|
setLoading(false);
|
|
718
|
+
// La primera carga es la apertura; las siguientes (tras Start) las enfoca native.
|
|
719
|
+
if(!ddSurveyLoaded){ ddSurveyLoaded=true; ddAutofocusOnOpen(); }
|
|
701
720
|
},
|
|
702
721
|
beforeSubmitEvent:function(){
|
|
703
722
|
setLoading(true);
|
|
@@ -848,6 +867,7 @@ var ReactNativePopupRenderer = class {
|
|
|
848
867
|
this.emitReady(surveyId);
|
|
849
868
|
break;
|
|
850
869
|
case "loaded":
|
|
870
|
+
break;
|
|
851
871
|
case "before_submit":
|
|
852
872
|
case "after_submit":
|
|
853
873
|
case "back":
|
|
@@ -875,7 +895,7 @@ function createReactNativeRenderer() {
|
|
|
875
895
|
}
|
|
876
896
|
|
|
877
897
|
// src/ui/preload.ts
|
|
878
|
-
var defaultLoader = () => import("./renderPopup-
|
|
898
|
+
var defaultLoader = () => import("./renderPopup-JF6IB6HL.mjs");
|
|
879
899
|
var pending = null;
|
|
880
900
|
function preloadRenderPopup(loader = defaultLoader) {
|
|
881
901
|
if (pending) return pending;
|
|
@@ -948,7 +968,7 @@ var BrowserPopupRenderer = class {
|
|
|
948
968
|
if (!this.container) return;
|
|
949
969
|
this.visible = true;
|
|
950
970
|
const container = this.container;
|
|
951
|
-
void import("./renderPopup-
|
|
971
|
+
void import("./renderPopup-JF6IB6HL.mjs").then(({ renderPopup }) => {
|
|
952
972
|
renderPopup(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService, analyticsFeedbackSessionId, options);
|
|
953
973
|
});
|
|
954
974
|
}
|
|
@@ -1223,7 +1243,7 @@ async function safeBodyText(res) {
|
|
|
1223
1243
|
}
|
|
1224
1244
|
}
|
|
1225
1245
|
function createFeedbackSink(options) {
|
|
1226
|
-
let feedbackSessionId;
|
|
1246
|
+
let feedbackSessionId = options.initialSessionId || void 0;
|
|
1227
1247
|
let firstPostInFlight = null;
|
|
1228
1248
|
let generation = 0;
|
|
1229
1249
|
const url = `${options.baseUrl}/sdk/feedback`;
|
|
@@ -1316,6 +1336,50 @@ function createFeedbackSink(options) {
|
|
|
1316
1336
|
};
|
|
1317
1337
|
}
|
|
1318
1338
|
|
|
1339
|
+
// src/analytics/suspended-session.ts
|
|
1340
|
+
var SUSPENDED_SESSION_KEY = "deepdots.analytics.suspended";
|
|
1341
|
+
function getSessionStorage() {
|
|
1342
|
+
try {
|
|
1343
|
+
return typeof window !== "undefined" && window.sessionStorage ? window.sessionStorage : null;
|
|
1344
|
+
} catch {
|
|
1345
|
+
return null;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
function writeSuspendedSession(storage, session) {
|
|
1349
|
+
try {
|
|
1350
|
+
storage.setItem(SUSPENDED_SESSION_KEY, JSON.stringify(session));
|
|
1351
|
+
return true;
|
|
1352
|
+
} catch {
|
|
1353
|
+
return false;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
function clearSuspendedSession(storage) {
|
|
1357
|
+
try {
|
|
1358
|
+
storage.removeItem(SUSPENDED_SESSION_KEY);
|
|
1359
|
+
} catch {
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
function takeSuspendedSession(storage, match, now, windowMs) {
|
|
1363
|
+
let raw = null;
|
|
1364
|
+
try {
|
|
1365
|
+
raw = storage.getItem(SUSPENDED_SESSION_KEY);
|
|
1366
|
+
} catch {
|
|
1367
|
+
return null;
|
|
1368
|
+
}
|
|
1369
|
+
if (!raw) return null;
|
|
1370
|
+
clearSuspendedSession(storage);
|
|
1371
|
+
try {
|
|
1372
|
+
const s = JSON.parse(raw);
|
|
1373
|
+
if (typeof s.sessionId !== "string" || !s.sessionId) return null;
|
|
1374
|
+
if (typeof s.at !== "number" || now - s.at < 0 || now - s.at > windowMs) return null;
|
|
1375
|
+
if (s.integration !== match.integration) return null;
|
|
1376
|
+
if ((s.userId ?? null) !== match.userId) return null;
|
|
1377
|
+
return { sessionId: s.sessionId, integration: s.integration, userId: s.userId ?? null, at: s.at };
|
|
1378
|
+
} catch {
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1319
1383
|
// src/tracking/navigation-observer.ts
|
|
1320
1384
|
var UUID_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-/i;
|
|
1321
1385
|
function screenPath(href) {
|
|
@@ -1910,8 +1974,10 @@ var ANALYTICS_HEARTBEAT_MS = 5 * 6e4;
|
|
|
1910
1974
|
var ANALYTICS_SESSION_TIMEOUT_MS = 30 * 6e4;
|
|
1911
1975
|
var ANALYTICS_SERVER_IDLE_MS = 60 * 6e4;
|
|
1912
1976
|
var ANALYTICS_SESSION_DISCARD_MS = ANALYTICS_SERVER_IDLE_MS - 10 * 6e4;
|
|
1977
|
+
var ANALYTICS_RESUME_WINDOW_MS = 6e4;
|
|
1913
1978
|
var RESERVED_EVENT_PREFIX = "deepdots_";
|
|
1914
1979
|
var CUSTOM_EVENT_PREFIX = "deepdots_event_";
|
|
1980
|
+
var MAX_PENDING_TRIGGERS = 20;
|
|
1915
1981
|
var DeepdotsPopups = class {
|
|
1916
1982
|
constructor() {
|
|
1917
1983
|
this.config = null;
|
|
@@ -1925,6 +1991,14 @@ var DeepdotsPopups = class {
|
|
|
1925
1991
|
// deprecated: mantenido para compatibilidad interna
|
|
1926
1992
|
this.popupDefinitions = [];
|
|
1927
1993
|
this.popupsLoaded = false;
|
|
1994
|
+
/**
|
|
1995
|
+
* `true` mientras el `GET /sdk/{apiKey}/popups` de `init()` está en vuelo. En esa ventana los
|
|
1996
|
+
* disparos del host (`triggerEvent`, `triggerSurvey`) no tienen contra qué evaluarse, así que
|
|
1997
|
+
* se guardan en `pendingTriggers` y se reproducen al cargar. Click, scroll, time y exit no
|
|
1998
|
+
* hacen falta: son listeners que `autoLaunch` instala ya con las definiciones cargadas.
|
|
1999
|
+
*/
|
|
2000
|
+
this.awaitingPopups = false;
|
|
2001
|
+
this.pendingTriggers = [];
|
|
1928
2002
|
this.pendingAutoLaunch = false;
|
|
1929
2003
|
// Estado que alimenta las reglas de redisplay. Copia de trabajo en memoria; la fuente
|
|
1930
2004
|
// duradera es el storage (`popup-state-store`), rehidratada en `init()` — sin eso, una
|
|
@@ -1979,6 +2053,13 @@ var DeepdotsPopups = class {
|
|
|
1979
2053
|
this.lastEngagementAt = 0;
|
|
1980
2054
|
/** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
|
|
1981
2055
|
this.hiddenAt = null;
|
|
2056
|
+
/**
|
|
2057
|
+
* Sesión suspendida por un `pagehide` (ms epoch y `deepdots_user_id`); null si no hay.
|
|
2058
|
+
* `openSession` la reanuda si sigue a tiempo (ver ANALYTICS_RESUME_WINDOW_MS).
|
|
2059
|
+
*/
|
|
2060
|
+
this.suspended = null;
|
|
2061
|
+
/** Integración de analytics del init: la sesión suspendida solo la hereda la misma. */
|
|
2062
|
+
this.analyticsIntegration = void 0;
|
|
1982
2063
|
/**
|
|
1983
2064
|
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
1984
2065
|
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
@@ -2038,10 +2119,17 @@ var DeepdotsPopups = class {
|
|
|
2038
2119
|
}
|
|
2039
2120
|
}
|
|
2040
2121
|
this.log("tracking \xB7 user_id:", this.tracking.getUserId(), "\xB7 new_user:", this.tracking.isNewUser(), "\xB7 enabled:", this.tracking.isTrackingEnabled());
|
|
2122
|
+
this.analyticsIntegration = config.analytics?.integration;
|
|
2123
|
+
const resumable = this.readResumableSession();
|
|
2124
|
+
if (resumable) {
|
|
2125
|
+
this.analyticsFeedbackSessionId = resumable.sessionId;
|
|
2126
|
+
this.suspended = { at: resumable.at, userId: resumable.userId };
|
|
2127
|
+
}
|
|
2041
2128
|
const analyticsSink = config.analytics ? createFeedbackSink({
|
|
2042
2129
|
baseUrl: this.baseUrl,
|
|
2043
2130
|
keys: config.analytics,
|
|
2044
2131
|
log: (...a) => this.log(...a),
|
|
2132
|
+
initialSessionId: resumable?.sessionId,
|
|
2045
2133
|
onSessionId: (id) => {
|
|
2046
2134
|
this.analyticsFeedbackSessionId = id;
|
|
2047
2135
|
this.notifyFeedbackSession(id, "open");
|
|
@@ -2090,7 +2178,10 @@ var DeepdotsPopups = class {
|
|
|
2090
2178
|
this.navObserver.install();
|
|
2091
2179
|
this.engagement = new EngagementTracker();
|
|
2092
2180
|
this.setupAnalyticsFlush();
|
|
2093
|
-
this.openSession();
|
|
2181
|
+
this.openSession("cold_start");
|
|
2182
|
+
if (resumable && this.analyticsFeedbackSessionId === resumable.sessionId) {
|
|
2183
|
+
this.notifyFeedbackSession(resumable.sessionId, "open");
|
|
2184
|
+
}
|
|
2094
2185
|
const pendingCrashes = this.crashReporter.drainPendingCrashes();
|
|
2095
2186
|
if (this.tracking?.isTrackingEnabled()) {
|
|
2096
2187
|
for (const rec of pendingCrashes) {
|
|
@@ -2101,8 +2192,10 @@ var DeepdotsPopups = class {
|
|
|
2101
2192
|
this.log("SDK initialized", this.config);
|
|
2102
2193
|
if (this.renderer.init) this.renderer.init();
|
|
2103
2194
|
this.setupPopupContainer();
|
|
2195
|
+
this.awaitingPopups = Boolean(this.config.apiKey && this.baseUrl);
|
|
2104
2196
|
this.fetchPopupsFromServer().then((defs) => {
|
|
2105
|
-
|
|
2197
|
+
if (!defs) this.dropPendingTriggers();
|
|
2198
|
+
this.popupDefinitions = this.validatePopupDefinitions(defs ?? []);
|
|
2106
2199
|
this.popupsLoaded = true;
|
|
2107
2200
|
this.log("Popups loaded from API", this.popupDefinitions);
|
|
2108
2201
|
if (this.popupDefinitions.length) this.renderer.preload?.();
|
|
@@ -2111,6 +2204,7 @@ var DeepdotsPopups = class {
|
|
|
2111
2204
|
if (this.pendingAutoLaunch) {
|
|
2112
2205
|
this.startTriggers();
|
|
2113
2206
|
}
|
|
2207
|
+
this.replayPendingTriggers();
|
|
2114
2208
|
});
|
|
2115
2209
|
}
|
|
2116
2210
|
/** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
|
|
@@ -2142,7 +2236,7 @@ var DeepdotsPopups = class {
|
|
|
2142
2236
|
setTrackingEnabled(enabled) {
|
|
2143
2237
|
if (!enabled) this.closeSession("tracking_disabled");
|
|
2144
2238
|
this.tracking?.setTrackingEnabled(enabled);
|
|
2145
|
-
if (enabled) this.openSession();
|
|
2239
|
+
if (enabled) this.openSession("tracking_enabled");
|
|
2146
2240
|
if (enabled) this.resolveGeo();
|
|
2147
2241
|
this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
|
|
2148
2242
|
}
|
|
@@ -2259,7 +2353,7 @@ var DeepdotsPopups = class {
|
|
|
2259
2353
|
/** App a foreground (RN: AppState 'active'): reanuda el engagement time y abre sesión nueva. */
|
|
2260
2354
|
onForeground() {
|
|
2261
2355
|
this.engagement?.resume();
|
|
2262
|
-
this.openSession();
|
|
2356
|
+
this.openSession("foreground");
|
|
2263
2357
|
}
|
|
2264
2358
|
/**
|
|
2265
2359
|
* App a background (RN: AppState 'background'): FIN DE SESIÓN. Cierra pantalla actual
|
|
@@ -2307,7 +2401,7 @@ var DeepdotsPopups = class {
|
|
|
2307
2401
|
this.answeredSurveys.clear();
|
|
2308
2402
|
clearPopupState(storage);
|
|
2309
2403
|
this.log("tracking \xB7 user_change \xB7 user_id:", this.tracking.getUserId());
|
|
2310
|
-
this.openSession();
|
|
2404
|
+
this.openSession("user_change");
|
|
2311
2405
|
}
|
|
2312
2406
|
/**
|
|
2313
2407
|
* Cierra la sesión actual: vuelca todo lo que quedaba abierto y envía el último lote
|
|
@@ -2333,15 +2427,28 @@ var DeepdotsPopups = class {
|
|
|
2333
2427
|
this.log("tracking \xB7 session_end:", reason);
|
|
2334
2428
|
}
|
|
2335
2429
|
/**
|
|
2336
|
-
* Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente
|
|
2337
|
-
*
|
|
2430
|
+
* Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente.
|
|
2431
|
+
* El `reason` dice por qué empieza (ver `SessionStartReason`):
|
|
2432
|
+
* - `cold_start`: init() (carga de la página / arranque de la app RN).
|
|
2433
|
+
* - `foreground`: onForeground() tras un cierre (RN).
|
|
2434
|
+
* - `tracking_enabled`: setTrackingEnabled(true).
|
|
2435
|
+
* - `user_change`: setUserId() o init() con otro userId.
|
|
2436
|
+
* - `idle_timeout`: la pestaña vuelve a verse tras más de 30 min oculta.
|
|
2437
|
+
* - `bfcache_restore`: vuelta desde la bfcache fuera de la ventana de reanudación.
|
|
2438
|
+
* Si hay una sesión suspendida a tiempo emite `deepdots_session_resume` en su lugar, sin reason.
|
|
2338
2439
|
*/
|
|
2339
|
-
openSession() {
|
|
2440
|
+
openSession(reason) {
|
|
2340
2441
|
if (this.sessionOpen) return;
|
|
2341
2442
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
2443
|
+
const suspendedMs = this.takeSuspendedSession();
|
|
2342
2444
|
this.sessionOpen = true;
|
|
2343
2445
|
this.lastEngagementAt = Date.now();
|
|
2344
|
-
|
|
2446
|
+
if (suspendedMs === null) {
|
|
2447
|
+
this.track("deepdots_session_start", { reason });
|
|
2448
|
+
} else {
|
|
2449
|
+
this.track("deepdots_session_resume", { suspended_ms: suspendedMs });
|
|
2450
|
+
this.log("tracking \xB7 session resumed after", suspendedMs, "ms:", this.analyticsFeedbackSessionId);
|
|
2451
|
+
}
|
|
2345
2452
|
this.navObserver?.restart();
|
|
2346
2453
|
if (this.isDocumentVisible()) this.engagement?.resume();
|
|
2347
2454
|
}
|
|
@@ -2404,7 +2511,76 @@ var DeepdotsPopups = class {
|
|
|
2404
2511
|
} else {
|
|
2405
2512
|
this.discardSession();
|
|
2406
2513
|
}
|
|
2407
|
-
this.openSession();
|
|
2514
|
+
this.openSession("idle_timeout");
|
|
2515
|
+
}
|
|
2516
|
+
/**
|
|
2517
|
+
* `pagehide` en web: SUSPENDE la sesión en vez de cerrarla, porque el navegador lo lanza
|
|
2518
|
+
* igual al recargar que al cerrar la pestaña. Vuelca lo pendiente (page_view de la
|
|
2519
|
+
* pantalla, mini-services, engagement) con un `deepdots_session_suspend` y SIN
|
|
2520
|
+
* `completed`, y deja el registro en `sessionStorage` (ver `suspended-session.ts`):
|
|
2521
|
+
* - si la página vuelve a cargar en esta pestaña, `init()` sigue en el mismo registro;
|
|
2522
|
+
* - si la pestaña se cerró, el backend lo cierra a los pocos minutos sin otro lote.
|
|
2523
|
+
*
|
|
2524
|
+
* Devuelve false cuando no se puede suspender — aún no se conoce el registro (el primer
|
|
2525
|
+
* POST sigue en vuelo) o no hay `sessionStorage` — y entonces el caller cierra como antes.
|
|
2526
|
+
*/
|
|
2527
|
+
suspendSession() {
|
|
2528
|
+
if (!this.sessionOpen || !this.tracking?.isTrackingEnabled()) return false;
|
|
2529
|
+
const sessionId = this.analyticsFeedbackSessionId;
|
|
2530
|
+
const integration = this.analyticsIntegration;
|
|
2531
|
+
const storage = getSessionStorage();
|
|
2532
|
+
if (!sessionId || !integration || !storage) return false;
|
|
2533
|
+
const at = Date.now();
|
|
2534
|
+
const userId = this.tracking.getUserId();
|
|
2535
|
+
if (!writeSuspendedSession(storage, { sessionId, integration, userId, at })) return false;
|
|
2536
|
+
this.sessionOpen = false;
|
|
2537
|
+
this.suspended = { at, userId };
|
|
2538
|
+
this.navObserver?.stop();
|
|
2539
|
+
this.navStarted = false;
|
|
2540
|
+
this.analytics?.exitAllMiniServices();
|
|
2541
|
+
this.flushEngagement();
|
|
2542
|
+
this.engagement?.pause();
|
|
2543
|
+
this.track("deepdots_session_suspend", { reason: "page_hide" });
|
|
2544
|
+
this.flushAnalytics({ final: true });
|
|
2545
|
+
this.log("tracking \xB7 session suspended:", sessionId);
|
|
2546
|
+
return true;
|
|
2547
|
+
}
|
|
2548
|
+
/**
|
|
2549
|
+
* Recoge la sesión que dejó suspendida la carga anterior de esta pestaña (ver
|
|
2550
|
+
* `suspendSession`). Solo con analytics configurado y el tracking activo: sin ellos no hay
|
|
2551
|
+
* registro en el que seguir.
|
|
2552
|
+
*/
|
|
2553
|
+
readResumableSession() {
|
|
2554
|
+
const integration = this.analyticsIntegration;
|
|
2555
|
+
const storage = getSessionStorage();
|
|
2556
|
+
if (!integration || !storage || !this.tracking?.isTrackingEnabled()) return null;
|
|
2557
|
+
return takeSuspendedSession(
|
|
2558
|
+
storage,
|
|
2559
|
+
{ integration, userId: this.tracking.getUserId() },
|
|
2560
|
+
Date.now(),
|
|
2561
|
+
ANALYTICS_RESUME_WINDOW_MS
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
/**
|
|
2565
|
+
* Consume la sesión suspendida al abrir: los ms que estuvo suspendida si se puede seguir en
|
|
2566
|
+
* ella (mismo usuario, dentro de ANALYTICS_RESUME_WINDOW_MS); null si no había o ya no toca.
|
|
2567
|
+
* En ese caso se olvida su registro sin enviar nada: lo cierra el backend, y la sesión que
|
|
2568
|
+
* se abre ahora va a uno nuevo.
|
|
2569
|
+
*/
|
|
2570
|
+
takeSuspendedSession() {
|
|
2571
|
+
const suspended = this.suspended;
|
|
2572
|
+
if (!suspended) return null;
|
|
2573
|
+
this.suspended = null;
|
|
2574
|
+
const storage = getSessionStorage();
|
|
2575
|
+
if (storage) clearSuspendedSession(storage);
|
|
2576
|
+
const suspendedMs = Math.max(0, Date.now() - suspended.at);
|
|
2577
|
+
if (this.analyticsFeedbackSessionId && suspendedMs <= ANALYTICS_RESUME_WINDOW_MS && suspended.userId === (this.tracking?.getUserId() ?? null)) {
|
|
2578
|
+
return suspendedMs;
|
|
2579
|
+
}
|
|
2580
|
+
this.analytics?.discardSession(this.analyticsIdentity());
|
|
2581
|
+
this.analyticsFeedbackSessionId = void 0;
|
|
2582
|
+
this.tracking?.setSessionId(null);
|
|
2583
|
+
return null;
|
|
2408
2584
|
}
|
|
2409
2585
|
/** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
|
|
2410
2586
|
discardSession() {
|
|
@@ -2454,12 +2630,12 @@ var DeepdotsPopups = class {
|
|
|
2454
2630
|
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
2455
2631
|
window.addEventListener("pagehide", () => {
|
|
2456
2632
|
clearInterval(this.analyticsFlushTimer);
|
|
2457
|
-
this.closeSession("page_hide");
|
|
2633
|
+
if (!this.suspendSession()) this.closeSession("page_hide");
|
|
2458
2634
|
});
|
|
2459
2635
|
window.addEventListener("pageshow", (event) => {
|
|
2460
2636
|
if (!event.persisted) return;
|
|
2461
2637
|
this.startAnalyticsFlushTimer();
|
|
2462
|
-
this.openSession();
|
|
2638
|
+
this.openSession("bfcache_restore");
|
|
2463
2639
|
});
|
|
2464
2640
|
document.addEventListener("visibilitychange", () => {
|
|
2465
2641
|
if (document.visibilityState === "hidden") {
|
|
@@ -2522,6 +2698,7 @@ var DeepdotsPopups = class {
|
|
|
2522
2698
|
}
|
|
2523
2699
|
/** Lógica para evaluar condiciones antes de mostrar una encuesta */
|
|
2524
2700
|
triggerSurvey(surveyId, popupId) {
|
|
2701
|
+
if (this.bufferIfAwaitingPopups({ kind: "survey", surveyId, popupId })) return;
|
|
2525
2702
|
const def = this.findPopupDefinition(surveyId, popupId);
|
|
2526
2703
|
if (!def) {
|
|
2527
2704
|
this.debug("No popup definition for trigger", { surveyId, popupId });
|
|
@@ -2543,6 +2720,10 @@ var DeepdotsPopups = class {
|
|
|
2543
2720
|
this.debug("Ignoring empty event trigger name");
|
|
2544
2721
|
return;
|
|
2545
2722
|
}
|
|
2723
|
+
if (this.bufferIfAwaitingPopups({ kind: "event", name: normalized })) return;
|
|
2724
|
+
this.evaluateEvent(normalized);
|
|
2725
|
+
}
|
|
2726
|
+
evaluateEvent(normalized) {
|
|
2546
2727
|
const candidates = this.popupDefinitions.filter((def) => {
|
|
2547
2728
|
return def.triggers.some((trigger) => {
|
|
2548
2729
|
const triggerValue = String(trigger.value ?? "").trim();
|
|
@@ -2560,6 +2741,40 @@ var DeepdotsPopups = class {
|
|
|
2560
2741
|
}
|
|
2561
2742
|
this.showDefinition(matched);
|
|
2562
2743
|
}
|
|
2744
|
+
/**
|
|
2745
|
+
* Guarda `trigger` si las definiciones aún no han llegado. Devuelve `true` si lo guardó (el
|
|
2746
|
+
* llamador no debe evaluarlo ahora) y `false` si no hay carga en vuelo (se evalúa ya, como
|
|
2747
|
+
* siempre: tras cargar, tras un fallo o sin apiKey).
|
|
2748
|
+
*/
|
|
2749
|
+
bufferIfAwaitingPopups(trigger) {
|
|
2750
|
+
if (!this.awaitingPopups) return false;
|
|
2751
|
+
if (this.pendingTriggers.length >= MAX_PENDING_TRIGGERS) {
|
|
2752
|
+
const dropped = this.pendingTriggers.shift();
|
|
2753
|
+
this.debug("Pending trigger buffer full; dropped oldest", dropped);
|
|
2754
|
+
}
|
|
2755
|
+
this.pendingTriggers.push(trigger);
|
|
2756
|
+
this.debug("Popups not loaded yet; trigger buffered", trigger);
|
|
2757
|
+
return true;
|
|
2758
|
+
}
|
|
2759
|
+
/** La carga falló: sin definiciones no hay nada que reproducir, se descarta todo. */
|
|
2760
|
+
dropPendingTriggers() {
|
|
2761
|
+
const dropped = this.pendingTriggers.length;
|
|
2762
|
+
this.awaitingPopups = false;
|
|
2763
|
+
this.pendingTriggers = [];
|
|
2764
|
+
if (dropped > 0) this.debug("Popups failed to load; dropped pending triggers", dropped);
|
|
2765
|
+
}
|
|
2766
|
+
/** Reproduce en orden por la evaluación normal (segmentos, cooldowns y condiciones de AHORA). */
|
|
2767
|
+
replayPendingTriggers() {
|
|
2768
|
+
this.awaitingPopups = false;
|
|
2769
|
+
const replay = this.pendingTriggers;
|
|
2770
|
+
this.pendingTriggers = [];
|
|
2771
|
+
if (!replay.length) return;
|
|
2772
|
+
this.debug("Replaying triggers received before popups loaded", replay.length);
|
|
2773
|
+
replay.forEach((trigger) => {
|
|
2774
|
+
if (trigger.kind === "event") this.evaluateEvent(trigger.name);
|
|
2775
|
+
else this.triggerSurvey(trigger.surveyId, trigger.popupId);
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2563
2778
|
showDefinition(def) {
|
|
2564
2779
|
this.log("Showing popup (definition)", def);
|
|
2565
2780
|
this.surveyToPopupId.set(def.surveyId, def.id);
|
|
@@ -2830,7 +3045,7 @@ var DeepdotsPopups = class {
|
|
|
2830
3045
|
} catch {
|
|
2831
3046
|
}
|
|
2832
3047
|
}
|
|
2833
|
-
/** Fetch al servidor para obtener popups */
|
|
3048
|
+
/** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
|
|
2834
3049
|
async fetchPopupsFromServer() {
|
|
2835
3050
|
const apiKey = this.config?.apiKey;
|
|
2836
3051
|
const baseUrl = this.baseUrl;
|
|
@@ -2846,19 +3061,19 @@ var DeepdotsPopups = class {
|
|
|
2846
3061
|
const response = await fetch(endpoint);
|
|
2847
3062
|
if (!response.ok) {
|
|
2848
3063
|
this.log("Failed to fetch popups", response.status, response.statusText);
|
|
2849
|
-
return
|
|
3064
|
+
return null;
|
|
2850
3065
|
}
|
|
2851
3066
|
const raw = await response.text();
|
|
2852
3067
|
const parsed = JSON.parse(raw);
|
|
2853
3068
|
if (!Array.isArray(parsed)) {
|
|
2854
3069
|
this.log("Unexpected popups payload", parsed);
|
|
2855
|
-
return
|
|
3070
|
+
return null;
|
|
2856
3071
|
}
|
|
2857
3072
|
this.debug("Fetched popups payload", parsed);
|
|
2858
3073
|
return parsed;
|
|
2859
3074
|
} catch (error) {
|
|
2860
3075
|
this.log("Error fetching popups", error);
|
|
2861
|
-
return
|
|
3076
|
+
return null;
|
|
2862
3077
|
}
|
|
2863
3078
|
}
|
|
2864
3079
|
/** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
|
|
@@ -2978,7 +3193,7 @@ var DeepdotsPopups = class {
|
|
|
2978
3193
|
}
|
|
2979
3194
|
if (!this.popupContainer) return;
|
|
2980
3195
|
const container = this.popupContainer;
|
|
2981
|
-
void import("./renderPopup-
|
|
3196
|
+
void import("./renderPopup-JF6IB6HL.mjs").then(({ renderPopup }) => {
|
|
2982
3197
|
renderPopup(
|
|
2983
3198
|
container,
|
|
2984
3199
|
surveyId,
|
|
@@ -3282,6 +3497,7 @@ function DeepdotsProvider({ config, children }) {
|
|
|
3282
3497
|
style: { flex: 1 },
|
|
3283
3498
|
originWhitelist: ["*"],
|
|
3284
3499
|
javaScriptEnabled: true,
|
|
3500
|
+
keyboardDisplayRequiresUserAction: false,
|
|
3285
3501
|
source: { html: survey.html, baseUrl: "https://sdk.deepdots.com/" },
|
|
3286
3502
|
onMessage: (e) => rendererRef.current?.handleMessage(e.nativeEvent.data)
|
|
3287
3503
|
}
|