@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.js
CHANGED
|
@@ -2894,6 +2894,9 @@ function ensureFontFace(family, url) {
|
|
|
2894
2894
|
}
|
|
2895
2895
|
if (el.textContent !== css) el.textContent = css;
|
|
2896
2896
|
}
|
|
2897
|
+
function hasFinePointer() {
|
|
2898
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(pointer: fine)").matches;
|
|
2899
|
+
}
|
|
2897
2900
|
async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId, options) {
|
|
2898
2901
|
let surveyCompletedEmitted = false;
|
|
2899
2902
|
let stylesInjected = false;
|
|
@@ -3324,6 +3327,13 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
3324
3327
|
if (pos.padding) container.style.padding = pos.padding;
|
|
3325
3328
|
let revealed = false;
|
|
3326
3329
|
let revealTimer = null;
|
|
3330
|
+
let surveyLoaded = false;
|
|
3331
|
+
let openFocusDone = false;
|
|
3332
|
+
function autofocusOnOpen() {
|
|
3333
|
+
if (openFocusDone || !revealed || !surveyLoaded) return;
|
|
3334
|
+
openFocusDone = true;
|
|
3335
|
+
if (hasFinePointer()) formInstance?.focusFirstQuestion?.();
|
|
3336
|
+
}
|
|
3327
3337
|
function reveal() {
|
|
3328
3338
|
if (revealed) return;
|
|
3329
3339
|
revealed = true;
|
|
@@ -3334,6 +3344,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
3334
3344
|
if (!container.contains(popup)) return;
|
|
3335
3345
|
container.style.visibility = "visible";
|
|
3336
3346
|
container.style.pointerEvents = "";
|
|
3347
|
+
autofocusOnOpen();
|
|
3337
3348
|
}
|
|
3338
3349
|
function revealWhenPainted2() {
|
|
3339
3350
|
if (revealed) return;
|
|
@@ -3386,7 +3397,11 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
3386
3397
|
addSuccessScreen: false,
|
|
3387
3398
|
// Surveys multi-idioma: native pide a la API este idioma (el del init o el del
|
|
3388
3399
|
// dispositivo) y, si el survey no lo tiene, la API sirve el idioma por defecto.
|
|
3389
|
-
lang: options?.language
|
|
3400
|
+
lang: options?.language,
|
|
3401
|
+
// Foco en la primera pregunta de escribir tras Start, Siguiente o Atrás. Native lo
|
|
3402
|
+
// aplica después de afterSubmitEvent/onBackEvent, cuando `setLoading(false)` ya ha
|
|
3403
|
+
// quitado el `inert`. La apertura va aparte (ver autofocusOnOpen).
|
|
3404
|
+
autofocus: "navigation"
|
|
3390
3405
|
};
|
|
3391
3406
|
generateOptions.onLoadedEvent = ({ formData, lang }) => {
|
|
3392
3407
|
const shownLang = typeof lang === "string" && lang.trim().length > 0 ? lang : void 0;
|
|
@@ -3441,12 +3456,14 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
3441
3456
|
updateButtons("in_progress_first");
|
|
3442
3457
|
}
|
|
3443
3458
|
updateProgress({ progress: formInstance.progress, total: formInstance.total });
|
|
3444
|
-
emit("popup_clicked", surveyId, { action: "loaded" });
|
|
3445
3459
|
setLoading(false);
|
|
3460
|
+
if (!surveyLoaded) {
|
|
3461
|
+
surveyLoaded = true;
|
|
3462
|
+
autofocusOnOpen();
|
|
3463
|
+
}
|
|
3446
3464
|
};
|
|
3447
3465
|
generateOptions.beforeSubmitEvent = () => {
|
|
3448
3466
|
setLoading(true);
|
|
3449
|
-
emit("popup_clicked", surveyId, { action: "before_submit" });
|
|
3450
3467
|
};
|
|
3451
3468
|
generateOptions.afterSubmitEvent = ({ error, completed, total, progress, followup }) => {
|
|
3452
3469
|
const errText = error ? typeof error === "string" ? error : error.message ?? String(error) : "";
|
|
@@ -3455,13 +3472,12 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
3455
3472
|
if (errText.toLowerCase().includes("no response")) {
|
|
3456
3473
|
errorHint.textContent = labels.errorRequired;
|
|
3457
3474
|
errorHint.style.display = "block";
|
|
3458
|
-
emit("popup_clicked", surveyId, { action: "validation_error_required" });
|
|
3459
3475
|
updateNavButtons();
|
|
3460
3476
|
return;
|
|
3461
3477
|
}
|
|
3462
3478
|
errorHint.textContent = labels.errorSubmit;
|
|
3463
3479
|
errorHint.style.display = "block";
|
|
3464
|
-
|
|
3480
|
+
sdkWarn("[Deepdots] survey submit error:", errText);
|
|
3465
3481
|
return;
|
|
3466
3482
|
}
|
|
3467
3483
|
errorHint.style.display = "none";
|
|
@@ -3480,7 +3496,6 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
3480
3496
|
updateProgress({ progress, total, followup });
|
|
3481
3497
|
};
|
|
3482
3498
|
generateOptions.onBackEvent = ({ progress, error, followup }) => {
|
|
3483
|
-
emit("popup_clicked", surveyId, { action: "back" });
|
|
3484
3499
|
if (!error) pageDepth = Math.max(0, pageDepth - 1);
|
|
3485
3500
|
updateNavButtons();
|
|
3486
3501
|
updateProgress({ progress, total: formInstance.total, followup });
|
|
@@ -4104,11 +4119,25 @@ ${customCss}
|
|
|
4104
4119
|
${PRIMARY_COLOR_JS}
|
|
4105
4120
|
${BUSY_JS}
|
|
4106
4121
|
${REVEAL_JS}
|
|
4122
|
+
// Foco en la primera pregunta si es de escribir. Al navegar lo pone @magicfeedback/native
|
|
4123
|
+
// (autofocus:'navigation'); la apertura la decide el popup, que es quien sabe cu\xE1ndo se revela.
|
|
4124
|
+
// Necesita el popup revelado y el survey cargado, que llegan en cualquier orden, y solo con
|
|
4125
|
+
// rat\xF3n o trackpad: en t\xE1ctil sacar\xEDa el teclado sin que el usuario haya tocado nada.
|
|
4126
|
+
var ddRevealed=false, ddSurveyLoaded=false, ddOpenFocusDone=false;
|
|
4127
|
+
function ddAutofocusOnOpen(){
|
|
4128
|
+
if(ddOpenFocusDone || !ddRevealed || !ddSurveyLoaded){ return; }
|
|
4129
|
+
ddOpenFocusDone=true;
|
|
4130
|
+
var fine=typeof window.matchMedia==='function' && window.matchMedia('(pointer: fine)').matches;
|
|
4131
|
+
var f=window.DeepdotsForm;
|
|
4132
|
+
if(fine && f && typeof f.focusFirstQuestion==='function'){ f.focusFirstQuestion(); }
|
|
4133
|
+
}
|
|
4107
4134
|
// El popup se ense\xF1a cuando el survey est\xE1 pintado; \`ready\` deja al host abrir su propio
|
|
4108
4135
|
// Modal en ese momento en vez de montar un WebView en blanco.
|
|
4109
4136
|
var ddReveal=ddCreateReveal(popup, function(){
|
|
4110
4137
|
document.body.classList.add('dd-ready');
|
|
4111
4138
|
emitJSON('ready');
|
|
4139
|
+
ddRevealed=true;
|
|
4140
|
+
ddAutofocusOnOpen();
|
|
4112
4141
|
}, ${REVEAL_TIMEOUT_MS});
|
|
4113
4142
|
|
|
4114
4143
|
function setLoading(isLoading){
|
|
@@ -4149,6 +4178,9 @@ ${REVEAL_JS}
|
|
|
4149
4178
|
// Surveys multi-idioma: native pide a la API este idioma (el del init o el del
|
|
4150
4179
|
// dispositivo); si el survey no lo tiene, la API sirve el idioma por defecto.
|
|
4151
4180
|
lang:${initLangJson}||undefined,
|
|
4181
|
+
// Foco en la primera pregunta de escribir tras Start, Siguiente o Atr\xE1s. Las versiones
|
|
4182
|
+
// de native sin esta opci\xF3n la ignoran.
|
|
4183
|
+
autofocus:'navigation',
|
|
4152
4184
|
onLoadedEvent:function(args){
|
|
4153
4185
|
// Idioma en el que native muestra el survey: manda sobre el del init para que el
|
|
4154
4186
|
// chrome no se quede en ingl\xE9s delante de un survey traducido. Native < 2.3 no lo
|
|
@@ -4208,6 +4240,8 @@ ${REVEAL_JS}
|
|
|
4208
4240
|
updateProgress({ progress:form.progress, total:form.total, completed:false, followup:false });
|
|
4209
4241
|
emitJSON('loaded');
|
|
4210
4242
|
setLoading(false);
|
|
4243
|
+
// La primera carga es la apertura; las siguientes (tras Start) las enfoca native.
|
|
4244
|
+
if(!ddSurveyLoaded){ ddSurveyLoaded=true; ddAutofocusOnOpen(); }
|
|
4211
4245
|
},
|
|
4212
4246
|
beforeSubmitEvent:function(){
|
|
4213
4247
|
setLoading(true);
|
|
@@ -4360,6 +4394,7 @@ var ReactNativePopupRenderer = class {
|
|
|
4360
4394
|
this.emitReady(surveyId);
|
|
4361
4395
|
break;
|
|
4362
4396
|
case "loaded":
|
|
4397
|
+
break;
|
|
4363
4398
|
case "before_submit":
|
|
4364
4399
|
case "after_submit":
|
|
4365
4400
|
case "back":
|
|
@@ -4738,7 +4773,7 @@ async function safeBodyText(res) {
|
|
|
4738
4773
|
}
|
|
4739
4774
|
}
|
|
4740
4775
|
function createFeedbackSink(options) {
|
|
4741
|
-
let feedbackSessionId;
|
|
4776
|
+
let feedbackSessionId = options.initialSessionId || void 0;
|
|
4742
4777
|
let firstPostInFlight = null;
|
|
4743
4778
|
let generation = 0;
|
|
4744
4779
|
const url = `${options.baseUrl}/sdk/feedback`;
|
|
@@ -4831,6 +4866,50 @@ function createFeedbackSink(options) {
|
|
|
4831
4866
|
};
|
|
4832
4867
|
}
|
|
4833
4868
|
|
|
4869
|
+
// src/analytics/suspended-session.ts
|
|
4870
|
+
var SUSPENDED_SESSION_KEY = "deepdots.analytics.suspended";
|
|
4871
|
+
function getSessionStorage() {
|
|
4872
|
+
try {
|
|
4873
|
+
return typeof window !== "undefined" && window.sessionStorage ? window.sessionStorage : null;
|
|
4874
|
+
} catch {
|
|
4875
|
+
return null;
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
function writeSuspendedSession(storage, session) {
|
|
4879
|
+
try {
|
|
4880
|
+
storage.setItem(SUSPENDED_SESSION_KEY, JSON.stringify(session));
|
|
4881
|
+
return true;
|
|
4882
|
+
} catch {
|
|
4883
|
+
return false;
|
|
4884
|
+
}
|
|
4885
|
+
}
|
|
4886
|
+
function clearSuspendedSession(storage) {
|
|
4887
|
+
try {
|
|
4888
|
+
storage.removeItem(SUSPENDED_SESSION_KEY);
|
|
4889
|
+
} catch {
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4892
|
+
function takeSuspendedSession(storage, match, now, windowMs) {
|
|
4893
|
+
let raw = null;
|
|
4894
|
+
try {
|
|
4895
|
+
raw = storage.getItem(SUSPENDED_SESSION_KEY);
|
|
4896
|
+
} catch {
|
|
4897
|
+
return null;
|
|
4898
|
+
}
|
|
4899
|
+
if (!raw) return null;
|
|
4900
|
+
clearSuspendedSession(storage);
|
|
4901
|
+
try {
|
|
4902
|
+
const s = JSON.parse(raw);
|
|
4903
|
+
if (typeof s.sessionId !== "string" || !s.sessionId) return null;
|
|
4904
|
+
if (typeof s.at !== "number" || now - s.at < 0 || now - s.at > windowMs) return null;
|
|
4905
|
+
if (s.integration !== match.integration) return null;
|
|
4906
|
+
if ((s.userId ?? null) !== match.userId) return null;
|
|
4907
|
+
return { sessionId: s.sessionId, integration: s.integration, userId: s.userId ?? null, at: s.at };
|
|
4908
|
+
} catch {
|
|
4909
|
+
return null;
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
|
|
4834
4913
|
// src/tracking/navigation-observer.ts
|
|
4835
4914
|
var UUID_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-/i;
|
|
4836
4915
|
function screenPath(href) {
|
|
@@ -5429,8 +5508,10 @@ var ANALYTICS_HEARTBEAT_MS = 5 * 6e4;
|
|
|
5429
5508
|
var ANALYTICS_SESSION_TIMEOUT_MS = 30 * 6e4;
|
|
5430
5509
|
var ANALYTICS_SERVER_IDLE_MS = 60 * 6e4;
|
|
5431
5510
|
var ANALYTICS_SESSION_DISCARD_MS = ANALYTICS_SERVER_IDLE_MS - 10 * 6e4;
|
|
5511
|
+
var ANALYTICS_RESUME_WINDOW_MS = 6e4;
|
|
5432
5512
|
var RESERVED_EVENT_PREFIX = "deepdots_";
|
|
5433
5513
|
var CUSTOM_EVENT_PREFIX = "deepdots_event_";
|
|
5514
|
+
var MAX_PENDING_TRIGGERS = 20;
|
|
5434
5515
|
var DeepdotsPopups = class {
|
|
5435
5516
|
constructor() {
|
|
5436
5517
|
this.config = null;
|
|
@@ -5444,6 +5525,14 @@ var DeepdotsPopups = class {
|
|
|
5444
5525
|
// deprecated: mantenido para compatibilidad interna
|
|
5445
5526
|
this.popupDefinitions = [];
|
|
5446
5527
|
this.popupsLoaded = false;
|
|
5528
|
+
/**
|
|
5529
|
+
* `true` mientras el `GET /sdk/{apiKey}/popups` de `init()` está en vuelo. En esa ventana los
|
|
5530
|
+
* disparos del host (`triggerEvent`, `triggerSurvey`) no tienen contra qué evaluarse, así que
|
|
5531
|
+
* se guardan en `pendingTriggers` y se reproducen al cargar. Click, scroll, time y exit no
|
|
5532
|
+
* hacen falta: son listeners que `autoLaunch` instala ya con las definiciones cargadas.
|
|
5533
|
+
*/
|
|
5534
|
+
this.awaitingPopups = false;
|
|
5535
|
+
this.pendingTriggers = [];
|
|
5447
5536
|
this.pendingAutoLaunch = false;
|
|
5448
5537
|
// Estado que alimenta las reglas de redisplay. Copia de trabajo en memoria; la fuente
|
|
5449
5538
|
// duradera es el storage (`popup-state-store`), rehidratada en `init()` — sin eso, una
|
|
@@ -5498,6 +5587,13 @@ var DeepdotsPopups = class {
|
|
|
5498
5587
|
this.lastEngagementAt = 0;
|
|
5499
5588
|
/** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
|
|
5500
5589
|
this.hiddenAt = null;
|
|
5590
|
+
/**
|
|
5591
|
+
* Sesión suspendida por un `pagehide` (ms epoch y `deepdots_user_id`); null si no hay.
|
|
5592
|
+
* `openSession` la reanuda si sigue a tiempo (ver ANALYTICS_RESUME_WINDOW_MS).
|
|
5593
|
+
*/
|
|
5594
|
+
this.suspended = null;
|
|
5595
|
+
/** Integración de analytics del init: la sesión suspendida solo la hereda la misma. */
|
|
5596
|
+
this.analyticsIntegration = void 0;
|
|
5501
5597
|
/**
|
|
5502
5598
|
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
5503
5599
|
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
@@ -5557,10 +5653,17 @@ var DeepdotsPopups = class {
|
|
|
5557
5653
|
}
|
|
5558
5654
|
}
|
|
5559
5655
|
this.log("tracking \xB7 user_id:", this.tracking.getUserId(), "\xB7 new_user:", this.tracking.isNewUser(), "\xB7 enabled:", this.tracking.isTrackingEnabled());
|
|
5656
|
+
this.analyticsIntegration = config.analytics?.integration;
|
|
5657
|
+
const resumable = this.readResumableSession();
|
|
5658
|
+
if (resumable) {
|
|
5659
|
+
this.analyticsFeedbackSessionId = resumable.sessionId;
|
|
5660
|
+
this.suspended = { at: resumable.at, userId: resumable.userId };
|
|
5661
|
+
}
|
|
5560
5662
|
const analyticsSink = config.analytics ? createFeedbackSink({
|
|
5561
5663
|
baseUrl: this.baseUrl,
|
|
5562
5664
|
keys: config.analytics,
|
|
5563
5665
|
log: (...a) => this.log(...a),
|
|
5666
|
+
initialSessionId: resumable?.sessionId,
|
|
5564
5667
|
onSessionId: (id) => {
|
|
5565
5668
|
this.analyticsFeedbackSessionId = id;
|
|
5566
5669
|
this.notifyFeedbackSession(id, "open");
|
|
@@ -5609,7 +5712,10 @@ var DeepdotsPopups = class {
|
|
|
5609
5712
|
this.navObserver.install();
|
|
5610
5713
|
this.engagement = new EngagementTracker();
|
|
5611
5714
|
this.setupAnalyticsFlush();
|
|
5612
|
-
this.openSession();
|
|
5715
|
+
this.openSession("cold_start");
|
|
5716
|
+
if (resumable && this.analyticsFeedbackSessionId === resumable.sessionId) {
|
|
5717
|
+
this.notifyFeedbackSession(resumable.sessionId, "open");
|
|
5718
|
+
}
|
|
5613
5719
|
const pendingCrashes = this.crashReporter.drainPendingCrashes();
|
|
5614
5720
|
if (this.tracking?.isTrackingEnabled()) {
|
|
5615
5721
|
for (const rec of pendingCrashes) {
|
|
@@ -5620,8 +5726,10 @@ var DeepdotsPopups = class {
|
|
|
5620
5726
|
this.log("SDK initialized", this.config);
|
|
5621
5727
|
if (this.renderer.init) this.renderer.init();
|
|
5622
5728
|
this.setupPopupContainer();
|
|
5729
|
+
this.awaitingPopups = Boolean(this.config.apiKey && this.baseUrl);
|
|
5623
5730
|
this.fetchPopupsFromServer().then((defs) => {
|
|
5624
|
-
|
|
5731
|
+
if (!defs) this.dropPendingTriggers();
|
|
5732
|
+
this.popupDefinitions = this.validatePopupDefinitions(defs ?? []);
|
|
5625
5733
|
this.popupsLoaded = true;
|
|
5626
5734
|
this.log("Popups loaded from API", this.popupDefinitions);
|
|
5627
5735
|
if (this.popupDefinitions.length) this.renderer.preload?.();
|
|
@@ -5630,6 +5738,7 @@ var DeepdotsPopups = class {
|
|
|
5630
5738
|
if (this.pendingAutoLaunch) {
|
|
5631
5739
|
this.startTriggers();
|
|
5632
5740
|
}
|
|
5741
|
+
this.replayPendingTriggers();
|
|
5633
5742
|
});
|
|
5634
5743
|
}
|
|
5635
5744
|
/** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
|
|
@@ -5661,7 +5770,7 @@ var DeepdotsPopups = class {
|
|
|
5661
5770
|
setTrackingEnabled(enabled) {
|
|
5662
5771
|
if (!enabled) this.closeSession("tracking_disabled");
|
|
5663
5772
|
this.tracking?.setTrackingEnabled(enabled);
|
|
5664
|
-
if (enabled) this.openSession();
|
|
5773
|
+
if (enabled) this.openSession("tracking_enabled");
|
|
5665
5774
|
if (enabled) this.resolveGeo();
|
|
5666
5775
|
this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
|
|
5667
5776
|
}
|
|
@@ -5778,7 +5887,7 @@ var DeepdotsPopups = class {
|
|
|
5778
5887
|
/** App a foreground (RN: AppState 'active'): reanuda el engagement time y abre sesión nueva. */
|
|
5779
5888
|
onForeground() {
|
|
5780
5889
|
this.engagement?.resume();
|
|
5781
|
-
this.openSession();
|
|
5890
|
+
this.openSession("foreground");
|
|
5782
5891
|
}
|
|
5783
5892
|
/**
|
|
5784
5893
|
* App a background (RN: AppState 'background'): FIN DE SESIÓN. Cierra pantalla actual
|
|
@@ -5826,7 +5935,7 @@ var DeepdotsPopups = class {
|
|
|
5826
5935
|
this.answeredSurveys.clear();
|
|
5827
5936
|
clearPopupState(storage);
|
|
5828
5937
|
this.log("tracking \xB7 user_change \xB7 user_id:", this.tracking.getUserId());
|
|
5829
|
-
this.openSession();
|
|
5938
|
+
this.openSession("user_change");
|
|
5830
5939
|
}
|
|
5831
5940
|
/**
|
|
5832
5941
|
* Cierra la sesión actual: vuelca todo lo que quedaba abierto y envía el último lote
|
|
@@ -5852,15 +5961,28 @@ var DeepdotsPopups = class {
|
|
|
5852
5961
|
this.log("tracking \xB7 session_end:", reason);
|
|
5853
5962
|
}
|
|
5854
5963
|
/**
|
|
5855
|
-
* Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente
|
|
5856
|
-
*
|
|
5964
|
+
* Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente.
|
|
5965
|
+
* El `reason` dice por qué empieza (ver `SessionStartReason`):
|
|
5966
|
+
* - `cold_start`: init() (carga de la página / arranque de la app RN).
|
|
5967
|
+
* - `foreground`: onForeground() tras un cierre (RN).
|
|
5968
|
+
* - `tracking_enabled`: setTrackingEnabled(true).
|
|
5969
|
+
* - `user_change`: setUserId() o init() con otro userId.
|
|
5970
|
+
* - `idle_timeout`: la pestaña vuelve a verse tras más de 30 min oculta.
|
|
5971
|
+
* - `bfcache_restore`: vuelta desde la bfcache fuera de la ventana de reanudación.
|
|
5972
|
+
* Si hay una sesión suspendida a tiempo emite `deepdots_session_resume` en su lugar, sin reason.
|
|
5857
5973
|
*/
|
|
5858
|
-
openSession() {
|
|
5974
|
+
openSession(reason) {
|
|
5859
5975
|
if (this.sessionOpen) return;
|
|
5860
5976
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
5977
|
+
const suspendedMs = this.takeSuspendedSession();
|
|
5861
5978
|
this.sessionOpen = true;
|
|
5862
5979
|
this.lastEngagementAt = Date.now();
|
|
5863
|
-
|
|
5980
|
+
if (suspendedMs === null) {
|
|
5981
|
+
this.track("deepdots_session_start", { reason });
|
|
5982
|
+
} else {
|
|
5983
|
+
this.track("deepdots_session_resume", { suspended_ms: suspendedMs });
|
|
5984
|
+
this.log("tracking \xB7 session resumed after", suspendedMs, "ms:", this.analyticsFeedbackSessionId);
|
|
5985
|
+
}
|
|
5864
5986
|
this.navObserver?.restart();
|
|
5865
5987
|
if (this.isDocumentVisible()) this.engagement?.resume();
|
|
5866
5988
|
}
|
|
@@ -5923,7 +6045,76 @@ var DeepdotsPopups = class {
|
|
|
5923
6045
|
} else {
|
|
5924
6046
|
this.discardSession();
|
|
5925
6047
|
}
|
|
5926
|
-
this.openSession();
|
|
6048
|
+
this.openSession("idle_timeout");
|
|
6049
|
+
}
|
|
6050
|
+
/**
|
|
6051
|
+
* `pagehide` en web: SUSPENDE la sesión en vez de cerrarla, porque el navegador lo lanza
|
|
6052
|
+
* igual al recargar que al cerrar la pestaña. Vuelca lo pendiente (page_view de la
|
|
6053
|
+
* pantalla, mini-services, engagement) con un `deepdots_session_suspend` y SIN
|
|
6054
|
+
* `completed`, y deja el registro en `sessionStorage` (ver `suspended-session.ts`):
|
|
6055
|
+
* - si la página vuelve a cargar en esta pestaña, `init()` sigue en el mismo registro;
|
|
6056
|
+
* - si la pestaña se cerró, el backend lo cierra a los pocos minutos sin otro lote.
|
|
6057
|
+
*
|
|
6058
|
+
* Devuelve false cuando no se puede suspender — aún no se conoce el registro (el primer
|
|
6059
|
+
* POST sigue en vuelo) o no hay `sessionStorage` — y entonces el caller cierra como antes.
|
|
6060
|
+
*/
|
|
6061
|
+
suspendSession() {
|
|
6062
|
+
if (!this.sessionOpen || !this.tracking?.isTrackingEnabled()) return false;
|
|
6063
|
+
const sessionId = this.analyticsFeedbackSessionId;
|
|
6064
|
+
const integration = this.analyticsIntegration;
|
|
6065
|
+
const storage = getSessionStorage();
|
|
6066
|
+
if (!sessionId || !integration || !storage) return false;
|
|
6067
|
+
const at = Date.now();
|
|
6068
|
+
const userId = this.tracking.getUserId();
|
|
6069
|
+
if (!writeSuspendedSession(storage, { sessionId, integration, userId, at })) return false;
|
|
6070
|
+
this.sessionOpen = false;
|
|
6071
|
+
this.suspended = { at, userId };
|
|
6072
|
+
this.navObserver?.stop();
|
|
6073
|
+
this.navStarted = false;
|
|
6074
|
+
this.analytics?.exitAllMiniServices();
|
|
6075
|
+
this.flushEngagement();
|
|
6076
|
+
this.engagement?.pause();
|
|
6077
|
+
this.track("deepdots_session_suspend", { reason: "page_hide" });
|
|
6078
|
+
this.flushAnalytics({ final: true });
|
|
6079
|
+
this.log("tracking \xB7 session suspended:", sessionId);
|
|
6080
|
+
return true;
|
|
6081
|
+
}
|
|
6082
|
+
/**
|
|
6083
|
+
* Recoge la sesión que dejó suspendida la carga anterior de esta pestaña (ver
|
|
6084
|
+
* `suspendSession`). Solo con analytics configurado y el tracking activo: sin ellos no hay
|
|
6085
|
+
* registro en el que seguir.
|
|
6086
|
+
*/
|
|
6087
|
+
readResumableSession() {
|
|
6088
|
+
const integration = this.analyticsIntegration;
|
|
6089
|
+
const storage = getSessionStorage();
|
|
6090
|
+
if (!integration || !storage || !this.tracking?.isTrackingEnabled()) return null;
|
|
6091
|
+
return takeSuspendedSession(
|
|
6092
|
+
storage,
|
|
6093
|
+
{ integration, userId: this.tracking.getUserId() },
|
|
6094
|
+
Date.now(),
|
|
6095
|
+
ANALYTICS_RESUME_WINDOW_MS
|
|
6096
|
+
);
|
|
6097
|
+
}
|
|
6098
|
+
/**
|
|
6099
|
+
* Consume la sesión suspendida al abrir: los ms que estuvo suspendida si se puede seguir en
|
|
6100
|
+
* ella (mismo usuario, dentro de ANALYTICS_RESUME_WINDOW_MS); null si no había o ya no toca.
|
|
6101
|
+
* En ese caso se olvida su registro sin enviar nada: lo cierra el backend, y la sesión que
|
|
6102
|
+
* se abre ahora va a uno nuevo.
|
|
6103
|
+
*/
|
|
6104
|
+
takeSuspendedSession() {
|
|
6105
|
+
const suspended = this.suspended;
|
|
6106
|
+
if (!suspended) return null;
|
|
6107
|
+
this.suspended = null;
|
|
6108
|
+
const storage = getSessionStorage();
|
|
6109
|
+
if (storage) clearSuspendedSession(storage);
|
|
6110
|
+
const suspendedMs = Math.max(0, Date.now() - suspended.at);
|
|
6111
|
+
if (this.analyticsFeedbackSessionId && suspendedMs <= ANALYTICS_RESUME_WINDOW_MS && suspended.userId === (this.tracking?.getUserId() ?? null)) {
|
|
6112
|
+
return suspendedMs;
|
|
6113
|
+
}
|
|
6114
|
+
this.analytics?.discardSession(this.analyticsIdentity());
|
|
6115
|
+
this.analyticsFeedbackSessionId = void 0;
|
|
6116
|
+
this.tracking?.setSessionId(null);
|
|
6117
|
+
return null;
|
|
5927
6118
|
}
|
|
5928
6119
|
/** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
|
|
5929
6120
|
discardSession() {
|
|
@@ -5973,12 +6164,12 @@ var DeepdotsPopups = class {
|
|
|
5973
6164
|
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
5974
6165
|
window.addEventListener("pagehide", () => {
|
|
5975
6166
|
clearInterval(this.analyticsFlushTimer);
|
|
5976
|
-
this.closeSession("page_hide");
|
|
6167
|
+
if (!this.suspendSession()) this.closeSession("page_hide");
|
|
5977
6168
|
});
|
|
5978
6169
|
window.addEventListener("pageshow", (event) => {
|
|
5979
6170
|
if (!event.persisted) return;
|
|
5980
6171
|
this.startAnalyticsFlushTimer();
|
|
5981
|
-
this.openSession();
|
|
6172
|
+
this.openSession("bfcache_restore");
|
|
5982
6173
|
});
|
|
5983
6174
|
document.addEventListener("visibilitychange", () => {
|
|
5984
6175
|
if (document.visibilityState === "hidden") {
|
|
@@ -6041,6 +6232,7 @@ var DeepdotsPopups = class {
|
|
|
6041
6232
|
}
|
|
6042
6233
|
/** Lógica para evaluar condiciones antes de mostrar una encuesta */
|
|
6043
6234
|
triggerSurvey(surveyId, popupId) {
|
|
6235
|
+
if (this.bufferIfAwaitingPopups({ kind: "survey", surveyId, popupId })) return;
|
|
6044
6236
|
const def = this.findPopupDefinition(surveyId, popupId);
|
|
6045
6237
|
if (!def) {
|
|
6046
6238
|
this.debug("No popup definition for trigger", { surveyId, popupId });
|
|
@@ -6062,6 +6254,10 @@ var DeepdotsPopups = class {
|
|
|
6062
6254
|
this.debug("Ignoring empty event trigger name");
|
|
6063
6255
|
return;
|
|
6064
6256
|
}
|
|
6257
|
+
if (this.bufferIfAwaitingPopups({ kind: "event", name: normalized })) return;
|
|
6258
|
+
this.evaluateEvent(normalized);
|
|
6259
|
+
}
|
|
6260
|
+
evaluateEvent(normalized) {
|
|
6065
6261
|
const candidates = this.popupDefinitions.filter((def) => {
|
|
6066
6262
|
return def.triggers.some((trigger) => {
|
|
6067
6263
|
const triggerValue = String(trigger.value ?? "").trim();
|
|
@@ -6079,6 +6275,40 @@ var DeepdotsPopups = class {
|
|
|
6079
6275
|
}
|
|
6080
6276
|
this.showDefinition(matched);
|
|
6081
6277
|
}
|
|
6278
|
+
/**
|
|
6279
|
+
* Guarda `trigger` si las definiciones aún no han llegado. Devuelve `true` si lo guardó (el
|
|
6280
|
+
* llamador no debe evaluarlo ahora) y `false` si no hay carga en vuelo (se evalúa ya, como
|
|
6281
|
+
* siempre: tras cargar, tras un fallo o sin apiKey).
|
|
6282
|
+
*/
|
|
6283
|
+
bufferIfAwaitingPopups(trigger) {
|
|
6284
|
+
if (!this.awaitingPopups) return false;
|
|
6285
|
+
if (this.pendingTriggers.length >= MAX_PENDING_TRIGGERS) {
|
|
6286
|
+
const dropped = this.pendingTriggers.shift();
|
|
6287
|
+
this.debug("Pending trigger buffer full; dropped oldest", dropped);
|
|
6288
|
+
}
|
|
6289
|
+
this.pendingTriggers.push(trigger);
|
|
6290
|
+
this.debug("Popups not loaded yet; trigger buffered", trigger);
|
|
6291
|
+
return true;
|
|
6292
|
+
}
|
|
6293
|
+
/** La carga falló: sin definiciones no hay nada que reproducir, se descarta todo. */
|
|
6294
|
+
dropPendingTriggers() {
|
|
6295
|
+
const dropped = this.pendingTriggers.length;
|
|
6296
|
+
this.awaitingPopups = false;
|
|
6297
|
+
this.pendingTriggers = [];
|
|
6298
|
+
if (dropped > 0) this.debug("Popups failed to load; dropped pending triggers", dropped);
|
|
6299
|
+
}
|
|
6300
|
+
/** Reproduce en orden por la evaluación normal (segmentos, cooldowns y condiciones de AHORA). */
|
|
6301
|
+
replayPendingTriggers() {
|
|
6302
|
+
this.awaitingPopups = false;
|
|
6303
|
+
const replay = this.pendingTriggers;
|
|
6304
|
+
this.pendingTriggers = [];
|
|
6305
|
+
if (!replay.length) return;
|
|
6306
|
+
this.debug("Replaying triggers received before popups loaded", replay.length);
|
|
6307
|
+
replay.forEach((trigger) => {
|
|
6308
|
+
if (trigger.kind === "event") this.evaluateEvent(trigger.name);
|
|
6309
|
+
else this.triggerSurvey(trigger.surveyId, trigger.popupId);
|
|
6310
|
+
});
|
|
6311
|
+
}
|
|
6082
6312
|
showDefinition(def) {
|
|
6083
6313
|
this.log("Showing popup (definition)", def);
|
|
6084
6314
|
this.surveyToPopupId.set(def.surveyId, def.id);
|
|
@@ -6349,7 +6579,7 @@ var DeepdotsPopups = class {
|
|
|
6349
6579
|
} catch {
|
|
6350
6580
|
}
|
|
6351
6581
|
}
|
|
6352
|
-
/** Fetch al servidor para obtener popups */
|
|
6582
|
+
/** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
|
|
6353
6583
|
async fetchPopupsFromServer() {
|
|
6354
6584
|
const apiKey = this.config?.apiKey;
|
|
6355
6585
|
const baseUrl = this.baseUrl;
|
|
@@ -6365,19 +6595,19 @@ var DeepdotsPopups = class {
|
|
|
6365
6595
|
const response = await fetch(endpoint);
|
|
6366
6596
|
if (!response.ok) {
|
|
6367
6597
|
this.log("Failed to fetch popups", response.status, response.statusText);
|
|
6368
|
-
return
|
|
6598
|
+
return null;
|
|
6369
6599
|
}
|
|
6370
6600
|
const raw = await response.text();
|
|
6371
6601
|
const parsed = JSON.parse(raw);
|
|
6372
6602
|
if (!Array.isArray(parsed)) {
|
|
6373
6603
|
this.log("Unexpected popups payload", parsed);
|
|
6374
|
-
return
|
|
6604
|
+
return null;
|
|
6375
6605
|
}
|
|
6376
6606
|
this.debug("Fetched popups payload", parsed);
|
|
6377
6607
|
return parsed;
|
|
6378
6608
|
} catch (error) {
|
|
6379
6609
|
this.log("Error fetching popups", error);
|
|
6380
|
-
return
|
|
6610
|
+
return null;
|
|
6381
6611
|
}
|
|
6382
6612
|
}
|
|
6383
6613
|
/** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
|
|
@@ -6801,6 +7031,7 @@ function DeepdotsProvider({ config, children }) {
|
|
|
6801
7031
|
style: { flex: 1 },
|
|
6802
7032
|
originWhitelist: ["*"],
|
|
6803
7033
|
javaScriptEnabled: true,
|
|
7034
|
+
keyboardDisplayRequiresUserAction: false,
|
|
6804
7035
|
source: { html: survey.html, baseUrl: "https://sdk.deepdots.com/" },
|
|
6805
7036
|
onMessage: (e) => rendererRef.current?.handleMessage(e.nativeEvent.data)
|
|
6806
7037
|
}
|