@magicfeedback/popup-sdk 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -19
- package/dist/chunk-5VVCJBND.mjs +140 -0
- package/dist/chunk-5VVCJBND.mjs.map +1 -0
- package/dist/chunk-M6PEFLLQ.mjs +119 -0
- package/dist/chunk-M6PEFLLQ.mjs.map +1 -0
- package/dist/index.d.mts +400 -40
- package/dist/index.d.ts +400 -40
- package/dist/index.js +903 -106
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +758 -3411
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +470 -0
- package/dist/react-native.d.ts +470 -0
- package/dist/react-native.js +3202 -0
- package/dist/react-native.js.map +1 -0
- package/dist/react-native.mjs +1675 -0
- package/dist/react-native.mjs.map +1 -0
- package/dist/renderPopup-7GGKR5BY.mjs +1376 -0
- package/dist/renderPopup-7GGKR5BY.mjs.map +1 -0
- package/dist/renderPopup-Y4WG3575.mjs +3303 -0
- package/dist/renderPopup-Y4WG3575.mjs.map +1 -0
- package/package.json +36 -2
|
@@ -0,0 +1,1675 @@
|
|
|
1
|
+
import {
|
|
2
|
+
STORAGE_KEYS,
|
|
3
|
+
TrackingManager,
|
|
4
|
+
__require,
|
|
5
|
+
buildSurveyIdentity,
|
|
6
|
+
createDefaultStorage
|
|
7
|
+
} from "./chunk-M6PEFLLQ.mjs";
|
|
8
|
+
|
|
9
|
+
// src/react-native/index.tsx
|
|
10
|
+
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
|
11
|
+
import { AppState, Modal, Platform, View } from "react-native";
|
|
12
|
+
import { WebView } from "react-native-webview";
|
|
13
|
+
|
|
14
|
+
// src/types/index.ts
|
|
15
|
+
var POPUP_TRIGGER_CONDITION_STATUSES = [
|
|
16
|
+
"SHOWED",
|
|
17
|
+
"PARTIAL",
|
|
18
|
+
"COMPLETED"
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// src/triggers/index.ts
|
|
22
|
+
function setupTrigger(popups, trigger) {
|
|
23
|
+
switch (trigger.type) {
|
|
24
|
+
case "time":
|
|
25
|
+
setupTimeTrigger(popups, trigger);
|
|
26
|
+
break;
|
|
27
|
+
case "scroll":
|
|
28
|
+
setupScrollTrigger(popups, trigger);
|
|
29
|
+
break;
|
|
30
|
+
case "exit":
|
|
31
|
+
setupExitTrigger(popups, trigger);
|
|
32
|
+
break;
|
|
33
|
+
case "event":
|
|
34
|
+
setupEventTrigger(popups, trigger);
|
|
35
|
+
break;
|
|
36
|
+
case "click":
|
|
37
|
+
setupClickTrigger(popups, trigger);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function setupTimeTrigger(popups, trigger) {
|
|
42
|
+
const delay = trigger.value || 5e3;
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
popups.triggerSurvey(trigger.surveyId, trigger.popupId);
|
|
45
|
+
}, delay);
|
|
46
|
+
popups.debug(`Time trigger set for ${delay}ms`);
|
|
47
|
+
}
|
|
48
|
+
function setupScrollTrigger(popups, trigger) {
|
|
49
|
+
const threshold = trigger.value || 50;
|
|
50
|
+
const handler = () => {
|
|
51
|
+
const scrollPercentage = window.scrollY / (document.documentElement.scrollHeight - window.innerHeight) * 100;
|
|
52
|
+
if (scrollPercentage >= threshold) {
|
|
53
|
+
popups.triggerSurvey(trigger.surveyId, trigger.popupId);
|
|
54
|
+
window.removeEventListener("scroll", handler);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
window.addEventListener("scroll", handler);
|
|
58
|
+
popups.debug(`Scroll trigger set for ${threshold}%`);
|
|
59
|
+
}
|
|
60
|
+
function setupExitTrigger(popups, trigger) {
|
|
61
|
+
if (typeof window === "undefined") return;
|
|
62
|
+
const delaySeconds = parseExitDelaySeconds(trigger.value);
|
|
63
|
+
const historyRef = window.history;
|
|
64
|
+
const originalPushState = historyRef.pushState.bind(historyRef);
|
|
65
|
+
const originalReplaceState = historyRef.replaceState.bind(historyRef);
|
|
66
|
+
let lastUrl = normalizeUrl(window.location.href);
|
|
67
|
+
let lastQueuedKey = "";
|
|
68
|
+
let lastQueuedAt = 0;
|
|
69
|
+
const queueLeave = (fromRaw, toRaw) => {
|
|
70
|
+
const from = normalizeUrl(fromRaw);
|
|
71
|
+
const to = normalizeUrl(toRaw);
|
|
72
|
+
if (!from || !to || from === to) return;
|
|
73
|
+
const key = `${from}=>${to}`;
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
if (key === lastQueuedKey && now - lastQueuedAt < 150) return;
|
|
76
|
+
lastQueuedKey = key;
|
|
77
|
+
lastQueuedAt = now;
|
|
78
|
+
popups.queueExitPopup(trigger.surveyId, delaySeconds, from, trigger.popupId);
|
|
79
|
+
};
|
|
80
|
+
const handleNavigation = (to) => {
|
|
81
|
+
const from = lastUrl;
|
|
82
|
+
queueLeave(from, to);
|
|
83
|
+
lastUrl = normalizeUrl(to);
|
|
84
|
+
};
|
|
85
|
+
const handleDocumentClick = (event) => {
|
|
86
|
+
if (event.defaultPrevented) return;
|
|
87
|
+
if (event.button !== 0) return;
|
|
88
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
89
|
+
const anchor = event.target?.closest?.("a");
|
|
90
|
+
if (!(anchor instanceof HTMLAnchorElement)) return;
|
|
91
|
+
const href = anchor.getAttribute("href");
|
|
92
|
+
if (!href || href.startsWith("javascript:") || href.startsWith("mailto:") || href.startsWith("tel:")) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const target = anchor.getAttribute("target");
|
|
96
|
+
if (target && target !== "_self") return;
|
|
97
|
+
let destination;
|
|
98
|
+
try {
|
|
99
|
+
destination = new URL(href, window.location.href);
|
|
100
|
+
} catch {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (destination.origin !== window.location.origin) return;
|
|
104
|
+
queueLeave(window.location.href, destination.href);
|
|
105
|
+
};
|
|
106
|
+
const handleHashChange = (event) => {
|
|
107
|
+
const to = event.newURL || window.location.href;
|
|
108
|
+
const from = event.oldURL || lastUrl;
|
|
109
|
+
queueLeave(from, to);
|
|
110
|
+
lastUrl = normalizeUrl(to);
|
|
111
|
+
};
|
|
112
|
+
const handlePopState = () => {
|
|
113
|
+
handleNavigation(window.location.href);
|
|
114
|
+
};
|
|
115
|
+
historyRef.pushState = function(data, unused, url) {
|
|
116
|
+
const from = window.location.href;
|
|
117
|
+
const result = originalPushState(data, unused, url);
|
|
118
|
+
queueLeave(from, window.location.href);
|
|
119
|
+
lastUrl = normalizeUrl(window.location.href);
|
|
120
|
+
return result;
|
|
121
|
+
};
|
|
122
|
+
historyRef.replaceState = function(data, unused, url) {
|
|
123
|
+
const from = window.location.href;
|
|
124
|
+
const result = originalReplaceState(data, unused, url);
|
|
125
|
+
queueLeave(from, window.location.href);
|
|
126
|
+
lastUrl = normalizeUrl(window.location.href);
|
|
127
|
+
return result;
|
|
128
|
+
};
|
|
129
|
+
document.addEventListener("click", handleDocumentClick, true);
|
|
130
|
+
window.addEventListener("hashchange", handleHashChange);
|
|
131
|
+
window.addEventListener("popstate", handlePopState);
|
|
132
|
+
popups.debug("Exit trigger set (deferred to next route)", {
|
|
133
|
+
surveyId: trigger.surveyId,
|
|
134
|
+
delaySeconds
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function setupEventTrigger(popups, trigger) {
|
|
138
|
+
popups.debug("Event trigger registered", {
|
|
139
|
+
popupId: trigger.popupId,
|
|
140
|
+
surveyId: trigger.surveyId,
|
|
141
|
+
eventName: String(trigger.value || "")
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function parseExitDelaySeconds(value) {
|
|
145
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
146
|
+
return Math.max(0, value);
|
|
147
|
+
}
|
|
148
|
+
if (typeof value === "string" && value.trim()) {
|
|
149
|
+
const parsed = Number(value);
|
|
150
|
+
if (Number.isFinite(parsed)) return Math.max(0, parsed);
|
|
151
|
+
}
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
function normalizeUrl(value) {
|
|
155
|
+
if (!value) return "";
|
|
156
|
+
const withoutIndex = value.replace(/\/index\.html(?=($|[?#]))/i, "");
|
|
157
|
+
return withoutIndex.length > 1 && withoutIndex.endsWith("/") ? withoutIndex.slice(0, -1) : withoutIndex;
|
|
158
|
+
}
|
|
159
|
+
function setupClickTrigger(popups, trigger) {
|
|
160
|
+
const targetId = typeof trigger.value === "string" ? trigger.value : "";
|
|
161
|
+
if (!targetId) {
|
|
162
|
+
popups.debug("Click trigger missing element id", trigger);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const attach = () => {
|
|
166
|
+
const el = document.getElementById(targetId);
|
|
167
|
+
if (!el) {
|
|
168
|
+
popups.debug("Click trigger target not found", targetId);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const handler = (e) => {
|
|
172
|
+
if (e.defaultPrevented) return;
|
|
173
|
+
popups.triggerSurvey(trigger.surveyId, trigger.popupId);
|
|
174
|
+
el.removeEventListener("click", handler);
|
|
175
|
+
};
|
|
176
|
+
el.addEventListener("click", handler);
|
|
177
|
+
popups.debug(`Click trigger set for #${targetId}`);
|
|
178
|
+
return true;
|
|
179
|
+
};
|
|
180
|
+
if (typeof document === "undefined") return;
|
|
181
|
+
if (attach()) return;
|
|
182
|
+
window.addEventListener("DOMContentLoaded", () => attach(), { once: true });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/config/env.ts
|
|
186
|
+
var ENVIRONMENT_MAP = {
|
|
187
|
+
development: {
|
|
188
|
+
apiBaseUrl: "https://api-dev.deepdots.com"
|
|
189
|
+
},
|
|
190
|
+
production: {
|
|
191
|
+
apiBaseUrl: "https://api.deepdots.com"
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
function resolveEnvironment(nodeEnv) {
|
|
195
|
+
return ENVIRONMENT_MAP[nodeEnv || "production"];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/ui/surveyHtml.ts
|
|
199
|
+
function buildSurveyHtml(opts) {
|
|
200
|
+
const env = opts.env === "development" ? "dev" : "prod";
|
|
201
|
+
const version = opts.version ?? "2.2.4";
|
|
202
|
+
const cdn = `https://cdn.jsdelivr.net/npm/@magicfeedback/native@${version}/dist/magicfeedback-sdk.browser.js`;
|
|
203
|
+
const sid = JSON.stringify(opts.surveyId);
|
|
204
|
+
const pid = JSON.stringify(opts.productId);
|
|
205
|
+
const profileJson = JSON.stringify(opts.profile ?? []);
|
|
206
|
+
const metaJson = JSON.stringify(opts.metadata ?? []);
|
|
207
|
+
return `<!DOCTYPE html><html><head>
|
|
208
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
209
|
+
<meta name="color-scheme" content="light"/>
|
|
210
|
+
<style>html,body{margin:0;padding:0;background:transparent;font-family:-apple-system,system-ui,sans-serif}#mf{width:100%;box-sizing:border-box}#mf *{max-width:100%;box-sizing:border-box}</style>
|
|
211
|
+
</head><body>
|
|
212
|
+
<div id="mf"></div>
|
|
213
|
+
<script>
|
|
214
|
+
(function(){
|
|
215
|
+
function emit(s){ try{ if(window.ReactNativeWebView){ window.ReactNativeWebView.postMessage(s); } }catch(e){} }
|
|
216
|
+
function emitJSON(n,p){ try{ emit(JSON.stringify({name:n,payload:p||{}})); }catch(e){} }
|
|
217
|
+
window.DeepdotsClose = function(){ emitJSON('popup_close'); };
|
|
218
|
+
var initialized=false;
|
|
219
|
+
function initMF(){
|
|
220
|
+
if(initialized || !window.magicfeedback) return initialized;
|
|
221
|
+
initialized=true;
|
|
222
|
+
try{
|
|
223
|
+
window.magicfeedback.init({ debug:false, env:'${env}' });
|
|
224
|
+
var form = window.magicfeedback.form(${sid}, ${pid}, ${profileJson}, ${metaJson});
|
|
225
|
+
window.DeepdotsForm = form;
|
|
226
|
+
form.generate('mf', {
|
|
227
|
+
addButton:false,
|
|
228
|
+
getMetaData:true,
|
|
229
|
+
onLoadedEvent:function(){ emitJSON('loaded'); },
|
|
230
|
+
beforeSubmitEvent:function(){ emitJSON('before_submit'); },
|
|
231
|
+
afterSubmitEvent:function(p){
|
|
232
|
+
try{ if(p && p.completed){ emitJSON('survey_completed', p); } else { emitJSON('after_submit', p); } }
|
|
233
|
+
catch(e){ emitJSON('after_submit'); }
|
|
234
|
+
},
|
|
235
|
+
onBackEvent:function(p){ emitJSON('back', p); }
|
|
236
|
+
}).catch(function(e){ emit('error:init'); });
|
|
237
|
+
}catch(e){ emit('error:exception'); }
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
var s=document.createElement('script'); s.src=${JSON.stringify(cdn)}; s.async=true;
|
|
241
|
+
s.onload=function(){ initMF(); };
|
|
242
|
+
s.onerror=function(){ emit('error:load'); };
|
|
243
|
+
document.head.appendChild(s);
|
|
244
|
+
var tries=0, t=setInterval(function(){ if(initMF()||++tries>40){ clearInterval(t); } },150);
|
|
245
|
+
})();
|
|
246
|
+
</script>
|
|
247
|
+
</body></html>`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/platform/react-native-renderer.ts
|
|
251
|
+
var ReactNativePopupRenderer = class {
|
|
252
|
+
constructor(options = {}) {
|
|
253
|
+
this.options = options;
|
|
254
|
+
this.emitFn = null;
|
|
255
|
+
this.onCloseFn = null;
|
|
256
|
+
this.currentSurveyId = null;
|
|
257
|
+
this.partialEmitted = false;
|
|
258
|
+
}
|
|
259
|
+
init() {
|
|
260
|
+
}
|
|
261
|
+
show(surveyId, productId, _actions, emit, onClose, env = "production", userId, _style, sessionId, miniService) {
|
|
262
|
+
this.emitFn = emit;
|
|
263
|
+
this.onCloseFn = onClose;
|
|
264
|
+
this.currentSurveyId = surveyId;
|
|
265
|
+
this.partialEmitted = false;
|
|
266
|
+
const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null);
|
|
267
|
+
const html = buildSurveyHtml({ surveyId, productId, env, profile, metadata });
|
|
268
|
+
if (this.options.onShow) {
|
|
269
|
+
this.options.onShow({ surveyId, productId, html });
|
|
270
|
+
} else {
|
|
271
|
+
console.warn(
|
|
272
|
+
"[Deepdots] ReactNativePopupRenderer sin onShow: pasa { onShow } a new ReactNativePopupRenderer({...}) para montar el WebView del survey."
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
hide() {
|
|
277
|
+
this.options.onHide?.();
|
|
278
|
+
this.currentSurveyId = null;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
|
|
282
|
+
* eventos del SDK: primera interacción → `popup_clicked` (PARTIAL), completado →
|
|
283
|
+
* `survey_completed` (COMPLETED) y cierra el popup.
|
|
284
|
+
*/
|
|
285
|
+
handleMessage(raw) {
|
|
286
|
+
const surveyId = this.currentSurveyId;
|
|
287
|
+
if (!surveyId || !this.emitFn) return;
|
|
288
|
+
let name;
|
|
289
|
+
try {
|
|
290
|
+
name = JSON.parse(raw).name;
|
|
291
|
+
} catch {
|
|
292
|
+
name = raw;
|
|
293
|
+
}
|
|
294
|
+
switch (name) {
|
|
295
|
+
case "loaded":
|
|
296
|
+
case "before_submit":
|
|
297
|
+
case "after_submit":
|
|
298
|
+
case "back":
|
|
299
|
+
if (!this.partialEmitted) {
|
|
300
|
+
this.partialEmitted = true;
|
|
301
|
+
this.emitFn("popup_clicked", surveyId, { action: "partial" });
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
case "survey_completed":
|
|
305
|
+
this.emitFn("survey_completed", surveyId);
|
|
306
|
+
this.onCloseFn?.();
|
|
307
|
+
break;
|
|
308
|
+
case "popup_close":
|
|
309
|
+
this.onCloseFn?.();
|
|
310
|
+
break;
|
|
311
|
+
default:
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
function isReactNativeEnv() {
|
|
317
|
+
return typeof navigator !== "undefined" && navigator.product === "ReactNative";
|
|
318
|
+
}
|
|
319
|
+
function createReactNativeRenderer() {
|
|
320
|
+
return new ReactNativePopupRenderer();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/platform/renderer.ts
|
|
324
|
+
var NoopPopupRenderer = class {
|
|
325
|
+
show() {
|
|
326
|
+
}
|
|
327
|
+
hide() {
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
var BrowserPopupRenderer = class {
|
|
331
|
+
constructor() {
|
|
332
|
+
this.container = null;
|
|
333
|
+
this.visible = false;
|
|
334
|
+
}
|
|
335
|
+
init() {
|
|
336
|
+
if (typeof document === "undefined") return;
|
|
337
|
+
if (!this.container) {
|
|
338
|
+
this.container = document.createElement("div");
|
|
339
|
+
this.container.id = "deepdots-popup-container";
|
|
340
|
+
this.container.style.cssText = `
|
|
341
|
+
position: fixed;
|
|
342
|
+
top: 0;
|
|
343
|
+
left: 0;
|
|
344
|
+
width: 100%;
|
|
345
|
+
height: 100%;
|
|
346
|
+
display: none;
|
|
347
|
+
z-index: 999999;
|
|
348
|
+
background: rgba(0, 0, 0, 0.5);
|
|
349
|
+
justify-content: center;
|
|
350
|
+
align-items: center;
|
|
351
|
+
`;
|
|
352
|
+
}
|
|
353
|
+
if (!document.body.contains(this.container)) {
|
|
354
|
+
document.body.appendChild(this.container);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService) {
|
|
358
|
+
if (this.visible) return;
|
|
359
|
+
if (!this.container || !document.body.contains(this.container)) this.init();
|
|
360
|
+
if (!this.container) return;
|
|
361
|
+
this.visible = true;
|
|
362
|
+
const container = this.container;
|
|
363
|
+
void import("./renderPopup-7GGKR5BY.mjs").then(({ renderPopup }) => {
|
|
364
|
+
renderPopup(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService);
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
hide() {
|
|
368
|
+
this.visible = false;
|
|
369
|
+
if (this.container) {
|
|
370
|
+
this.container.style.display = "none";
|
|
371
|
+
this.container.innerHTML = "";
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
function createDefaultRenderer() {
|
|
376
|
+
if (typeof document === "undefined") {
|
|
377
|
+
if (isReactNativeEnv()) return createReactNativeRenderer();
|
|
378
|
+
return new NoopPopupRenderer();
|
|
379
|
+
}
|
|
380
|
+
if (isReactNativeEnv()) return createReactNativeRenderer();
|
|
381
|
+
return new BrowserPopupRenderer();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/analytics/analytics-manager.ts
|
|
385
|
+
var dryRunSink = (payload) => {
|
|
386
|
+
console.log(
|
|
387
|
+
"[DeepdotsAnalytics] (dry-run \xB7 NO enviado \xB7 sin init.analytics) POST /sdk/feedback \u2192",
|
|
388
|
+
JSON.stringify(payload, null, 2)
|
|
389
|
+
);
|
|
390
|
+
};
|
|
391
|
+
var AnalyticsManager = class {
|
|
392
|
+
constructor(options = {}) {
|
|
393
|
+
this.events = [];
|
|
394
|
+
this.attributes = {};
|
|
395
|
+
this.miniService = null;
|
|
396
|
+
this.miniServiceEnteredAt = 0;
|
|
397
|
+
this.sink = options.sink ?? dryRunSink;
|
|
398
|
+
this.now = options.now ?? (() => Date.now());
|
|
399
|
+
this.publicKey = options.publicKey;
|
|
400
|
+
this.language = options.language;
|
|
401
|
+
this.device = options.device;
|
|
402
|
+
this.platform = options.platform ?? "web";
|
|
403
|
+
}
|
|
404
|
+
/** Mezcla user attributes (se coercionan a string). Mutable en runtime. */
|
|
405
|
+
setUserAttributes(attrs) {
|
|
406
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
407
|
+
if (!k) continue;
|
|
408
|
+
this.attributes[k] = String(v);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/** Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`. */
|
|
412
|
+
enterMiniService(name, entryPointType) {
|
|
413
|
+
this.miniService = name;
|
|
414
|
+
this.miniServiceEnteredAt = this.now();
|
|
415
|
+
this.track("mini_service_enter", { entry_point_type: entryPointType ?? null });
|
|
416
|
+
}
|
|
417
|
+
/** Cierra el mini-service activo emitiendo `mini_service_exit` con su duración (#27). No-op si no hay ninguno. */
|
|
418
|
+
exitMiniService() {
|
|
419
|
+
const name = this.miniService;
|
|
420
|
+
if (name == null) return;
|
|
421
|
+
const durationSeconds = Math.max(0, Math.round((this.now() - this.miniServiceEnteredAt) / 1e3));
|
|
422
|
+
this.miniService = null;
|
|
423
|
+
this.track("mini_service_exit", { mini_service: name, duration_seconds: durationSeconds });
|
|
424
|
+
}
|
|
425
|
+
/** Registra un evento de analítica (modelo GA: nombre + params). */
|
|
426
|
+
track(name, params) {
|
|
427
|
+
const merged = {
|
|
428
|
+
...this.miniService ? { mini_service: this.miniService } : {},
|
|
429
|
+
...params ?? {}
|
|
430
|
+
};
|
|
431
|
+
this.events.push({
|
|
432
|
+
name,
|
|
433
|
+
timestamp: this.now(),
|
|
434
|
+
params: Object.keys(merged).length ? merged : void 0
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
/** Mini-service activo (para inyectarlo en la metadata del survey, #33). */
|
|
438
|
+
getMiniService() {
|
|
439
|
+
return this.miniService;
|
|
440
|
+
}
|
|
441
|
+
/** Nº de eventos pendientes de flush. */
|
|
442
|
+
pending() {
|
|
443
|
+
return this.events.length;
|
|
444
|
+
}
|
|
445
|
+
/** Construye el envelope que se enviaría al endpoint de analytics. */
|
|
446
|
+
buildPayload(identity) {
|
|
447
|
+
return {
|
|
448
|
+
publicKey: this.publicKey,
|
|
449
|
+
userId: identity.userId,
|
|
450
|
+
sessionId: identity.sessionId,
|
|
451
|
+
context: {
|
|
452
|
+
platform: this.platform,
|
|
453
|
+
language: this.language,
|
|
454
|
+
device: this.device,
|
|
455
|
+
attributes: { ...this.attributes }
|
|
456
|
+
},
|
|
457
|
+
events: [...this.events]
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
/** Envía (vía sink) el lote acumulado y vacía el buffer. No-op si no hay eventos. */
|
|
461
|
+
flush(identity) {
|
|
462
|
+
if (this.events.length === 0) return null;
|
|
463
|
+
const payload = this.buildPayload(identity);
|
|
464
|
+
this.sink(payload);
|
|
465
|
+
this.events = [];
|
|
466
|
+
return payload;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// src/analytics/feedback-payload.ts
|
|
471
|
+
function pushKV(list, key, value) {
|
|
472
|
+
if (value === void 0 || value === null || value === "") return;
|
|
473
|
+
list.push({ key, value: String(value) });
|
|
474
|
+
}
|
|
475
|
+
function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
|
|
476
|
+
const { context } = envelope;
|
|
477
|
+
const metrics = envelope.events.map((e) => ({
|
|
478
|
+
key: e.name,
|
|
479
|
+
value: JSON.stringify({ timestamp: e.timestamp, ...e.params ?? {} })
|
|
480
|
+
}));
|
|
481
|
+
const profile = [];
|
|
482
|
+
pushKV(profile, "external-user-id", envelope.userId);
|
|
483
|
+
const metadata = [];
|
|
484
|
+
pushKV(metadata, "user_id", envelope.userId);
|
|
485
|
+
pushKV(metadata, "session_id", envelope.sessionId);
|
|
486
|
+
pushKV(metadata, "platform", context.platform);
|
|
487
|
+
pushKV(metadata, "language", context.language);
|
|
488
|
+
if (context.device) {
|
|
489
|
+
pushKV(metadata, "device_type", context.device.device_type);
|
|
490
|
+
pushKV(metadata, "os_version", context.device.os_version);
|
|
491
|
+
pushKV(metadata, "device_model", context.device.device_model);
|
|
492
|
+
pushKV(metadata, "app_version", context.device.app_version);
|
|
493
|
+
pushKV(metadata, "user_agent", context.device.user_agent);
|
|
494
|
+
}
|
|
495
|
+
for (const [k, v] of Object.entries(context.attributes ?? {})) {
|
|
496
|
+
pushKV(metadata, k, v);
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
feedback: {
|
|
500
|
+
text: "",
|
|
501
|
+
answers: [],
|
|
502
|
+
metadata,
|
|
503
|
+
metrics,
|
|
504
|
+
profile,
|
|
505
|
+
finished: false
|
|
506
|
+
},
|
|
507
|
+
publicKey: keys.publicKey,
|
|
508
|
+
integration: keys.integration,
|
|
509
|
+
completed: false,
|
|
510
|
+
// finished: false,
|
|
511
|
+
...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function createFeedbackSink(options) {
|
|
515
|
+
let feedbackSessionId;
|
|
516
|
+
return (envelope) => {
|
|
517
|
+
const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId);
|
|
518
|
+
const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
519
|
+
if (!f) {
|
|
520
|
+
options.log?.("[DeepdotsAnalytics] no fetch disponible; payload no enviado", body);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
options.log?.("[DeepdotsAnalytics] POST /sdk/feedback \u2192", body);
|
|
524
|
+
f(`${options.baseUrl}/sdk/feedback`, {
|
|
525
|
+
method: "POST",
|
|
526
|
+
headers: { "Content-Type": "application/json" },
|
|
527
|
+
body: JSON.stringify(body)
|
|
528
|
+
}).then(async (res) => {
|
|
529
|
+
if (res.ok) {
|
|
530
|
+
try {
|
|
531
|
+
const data = await res.json();
|
|
532
|
+
if (data?.sessionId && data.sessionId !== feedbackSessionId) {
|
|
533
|
+
feedbackSessionId = data.sessionId;
|
|
534
|
+
options.log?.("[DeepdotsAnalytics] feedbackSessionId cacheado:", feedbackSessionId);
|
|
535
|
+
}
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}).catch((err) => options.log?.("[DeepdotsAnalytics] error enviando feedback", err));
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/tracking/navigation-observer.ts
|
|
544
|
+
var UUID_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-/i;
|
|
545
|
+
function normalizeScreen(href) {
|
|
546
|
+
let pathAndHash;
|
|
547
|
+
try {
|
|
548
|
+
const u = new URL(href, "http://deepdots.local");
|
|
549
|
+
pathAndHash = u.pathname + (u.hash || "");
|
|
550
|
+
} catch {
|
|
551
|
+
pathAndHash = href;
|
|
552
|
+
}
|
|
553
|
+
pathAndHash = pathAndHash.replace(/\?[^#]*/g, "");
|
|
554
|
+
const collapsed = pathAndHash.split("/").map((seg) => /^[0-9]+$/.test(seg) || UUID_PREFIX.test(seg) ? ":id" : seg).join("/");
|
|
555
|
+
return collapsed || "/";
|
|
556
|
+
}
|
|
557
|
+
var NavigationObserver = class {
|
|
558
|
+
constructor(options = {}) {
|
|
559
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
560
|
+
this.currentScreen = null;
|
|
561
|
+
this.entryAt = 0;
|
|
562
|
+
this.installed = false;
|
|
563
|
+
this.domHandlers = [];
|
|
564
|
+
this.now = options.now ?? (() => Date.now());
|
|
565
|
+
}
|
|
566
|
+
onVisit(listener) {
|
|
567
|
+
this.listeners.add(listener);
|
|
568
|
+
}
|
|
569
|
+
/** Fija la pantalla inicial (sin emitir visita; su duración se cierra al salir). */
|
|
570
|
+
begin(href) {
|
|
571
|
+
this.currentScreen = normalizeScreen(href);
|
|
572
|
+
this.entryAt = this.now();
|
|
573
|
+
}
|
|
574
|
+
/** Navega a una pantalla nueva: cierra la anterior (emite visita) y abre la nueva. */
|
|
575
|
+
visit(href) {
|
|
576
|
+
const next = normalizeScreen(href);
|
|
577
|
+
if (next === this.currentScreen) return;
|
|
578
|
+
this.closeCurrent();
|
|
579
|
+
this.currentScreen = next;
|
|
580
|
+
this.entryAt = this.now();
|
|
581
|
+
}
|
|
582
|
+
/** Cierra la pantalla actual emitiendo su visita (p. ej. al cerrar la app/pestaña). */
|
|
583
|
+
stop() {
|
|
584
|
+
this.closeCurrent();
|
|
585
|
+
this.currentScreen = null;
|
|
586
|
+
}
|
|
587
|
+
closeCurrent() {
|
|
588
|
+
if (this.currentScreen == null) return;
|
|
589
|
+
const exit = this.now();
|
|
590
|
+
const visit = {
|
|
591
|
+
screen: this.currentScreen,
|
|
592
|
+
entry: this.entryAt,
|
|
593
|
+
exit,
|
|
594
|
+
durationSeconds: Math.max(0, Math.round((exit - this.entryAt) / 1e3))
|
|
595
|
+
};
|
|
596
|
+
this.listeners.forEach((l) => {
|
|
597
|
+
try {
|
|
598
|
+
l(visit);
|
|
599
|
+
} catch {
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
/** Instala los hooks de History en el navegador y registra la pantalla actual. */
|
|
604
|
+
install() {
|
|
605
|
+
if (this.installed || typeof window === "undefined" || typeof history === "undefined") return;
|
|
606
|
+
this.installed = true;
|
|
607
|
+
const handle = () => this.visit(window.location.href);
|
|
608
|
+
const originalPush = history.pushState.bind(history);
|
|
609
|
+
const originalReplace = history.replaceState.bind(history);
|
|
610
|
+
history.pushState = (data, unused, url) => {
|
|
611
|
+
const r = originalPush(data, unused, url);
|
|
612
|
+
handle();
|
|
613
|
+
return r;
|
|
614
|
+
};
|
|
615
|
+
history.replaceState = (data, unused, url) => {
|
|
616
|
+
const r = originalReplace(data, unused, url);
|
|
617
|
+
handle();
|
|
618
|
+
return r;
|
|
619
|
+
};
|
|
620
|
+
window.addEventListener("popstate", handle);
|
|
621
|
+
window.addEventListener("hashchange", handle);
|
|
622
|
+
this.domHandlers.push(() => {
|
|
623
|
+
history.pushState = originalPush;
|
|
624
|
+
history.replaceState = originalReplace;
|
|
625
|
+
window.removeEventListener("popstate", handle);
|
|
626
|
+
window.removeEventListener("hashchange", handle);
|
|
627
|
+
});
|
|
628
|
+
this.begin(window.location.href);
|
|
629
|
+
}
|
|
630
|
+
uninstall() {
|
|
631
|
+
this.domHandlers.forEach((off) => off());
|
|
632
|
+
this.domHandlers = [];
|
|
633
|
+
this.installed = false;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
// src/analytics/device-info.ts
|
|
638
|
+
function parseDeviceType(ua) {
|
|
639
|
+
const s = (ua || "").toLowerCase();
|
|
640
|
+
if (/ipad|tablet|playbook|silk/.test(s) || /android/.test(s) && !/mobile/.test(s)) {
|
|
641
|
+
return "tablet";
|
|
642
|
+
}
|
|
643
|
+
if (/mobi|iphone|ipod|windows phone/.test(s) || /android/.test(s) && /mobile/.test(s)) {
|
|
644
|
+
return "mobile";
|
|
645
|
+
}
|
|
646
|
+
return "desktop";
|
|
647
|
+
}
|
|
648
|
+
function collectDeviceInfo(appVersion) {
|
|
649
|
+
const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
|
|
650
|
+
return {
|
|
651
|
+
device_type: parseDeviceType(ua),
|
|
652
|
+
user_agent: ua || void 0,
|
|
653
|
+
app_version: appVersion
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/analytics/engagement-tracker.ts
|
|
658
|
+
var EngagementTracker = class {
|
|
659
|
+
constructor(now = () => Date.now()) {
|
|
660
|
+
this.activeMs = 0;
|
|
661
|
+
this.lastResumeAt = null;
|
|
662
|
+
this.now = now;
|
|
663
|
+
}
|
|
664
|
+
/** Reanuda el conteo (pestaña visible / app en foreground). Idempotente. */
|
|
665
|
+
resume() {
|
|
666
|
+
if (this.lastResumeAt === null) this.lastResumeAt = this.now();
|
|
667
|
+
}
|
|
668
|
+
/** Pausa el conteo (pestaña oculta / app en background), acumulando el tramo activo. */
|
|
669
|
+
pause() {
|
|
670
|
+
if (this.lastResumeAt !== null) {
|
|
671
|
+
this.activeMs += this.now() - this.lastResumeAt;
|
|
672
|
+
this.lastResumeAt = null;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
/** Devuelve los ms activos acumulados desde la última lectura y reinicia (sin parar el timer). */
|
|
676
|
+
consume() {
|
|
677
|
+
if (this.lastResumeAt !== null) {
|
|
678
|
+
const t = this.now();
|
|
679
|
+
this.activeMs += t - this.lastResumeAt;
|
|
680
|
+
this.lastResumeAt = t;
|
|
681
|
+
}
|
|
682
|
+
const ms = this.activeMs;
|
|
683
|
+
this.activeMs = 0;
|
|
684
|
+
return ms;
|
|
685
|
+
}
|
|
686
|
+
isActive() {
|
|
687
|
+
return this.lastResumeAt !== null;
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
// src/contact/contact-manager.ts
|
|
692
|
+
function stableSerialize(attrs) {
|
|
693
|
+
const sorted = {};
|
|
694
|
+
for (const key of Object.keys(attrs).sort()) {
|
|
695
|
+
sorted[key] = attrs[key];
|
|
696
|
+
}
|
|
697
|
+
return JSON.stringify(sorted);
|
|
698
|
+
}
|
|
699
|
+
var ContactManager = class {
|
|
700
|
+
constructor(options) {
|
|
701
|
+
this.storage = options.storage;
|
|
702
|
+
this.publicKey = options.publicKey;
|
|
703
|
+
this.userId = options.userId;
|
|
704
|
+
this.post = options.post;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Envía los atributos si cambiaron respecto a lo último enviado.
|
|
708
|
+
* @returns `true` si se envió, `false` si no hubo cambios.
|
|
709
|
+
*/
|
|
710
|
+
async setAttributes(attributes) {
|
|
711
|
+
const serialized = stableSerialize(attributes);
|
|
712
|
+
const last = this.storage.getItem(STORAGE_KEYS.contactAttributes);
|
|
713
|
+
if (last === serialized) return false;
|
|
714
|
+
await this.post({
|
|
715
|
+
publicKey: this.publicKey,
|
|
716
|
+
userId: this.userId,
|
|
717
|
+
userAttributes: attributes
|
|
718
|
+
});
|
|
719
|
+
this.storage.setItem(STORAGE_KEYS.contactAttributes, serialized);
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
// src/core/deepdots-popups.ts
|
|
725
|
+
var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
|
|
726
|
+
var DAY_IN_MS = 24 * 60 * 60 * 1e3;
|
|
727
|
+
var DeepdotsPopups = class {
|
|
728
|
+
constructor() {
|
|
729
|
+
this.config = null;
|
|
730
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
731
|
+
this.triggers = [];
|
|
732
|
+
this.initialized = false;
|
|
733
|
+
this.renderer = createDefaultRenderer();
|
|
734
|
+
this.popupContainer = null;
|
|
735
|
+
// deprecated: mantenido para compatibilidad interna
|
|
736
|
+
this.popupDefinitions = [];
|
|
737
|
+
this.popupsLoaded = false;
|
|
738
|
+
this.pendingAutoLaunch = false;
|
|
739
|
+
this.answeredSurveys = /* @__PURE__ */ new Set();
|
|
740
|
+
this.surveyProgress = /* @__PURE__ */ new Map();
|
|
741
|
+
this.lastShown = /* @__PURE__ */ new Map();
|
|
742
|
+
// popupId -> timestamp mostrado
|
|
743
|
+
this.surveyToPopupId = /* @__PURE__ */ new Map();
|
|
744
|
+
// surveyId -> popupId
|
|
745
|
+
this.deferredExitTimers = [];
|
|
746
|
+
this.baseUrl = "";
|
|
747
|
+
this.env = "production";
|
|
748
|
+
/** Identidad + sesión (Fase 1 tracking). Null hasta init(). */
|
|
749
|
+
this.tracking = null;
|
|
750
|
+
/** Capa de analytics (canal separado del feedback). Null hasta init(). */
|
|
751
|
+
this.analytics = null;
|
|
752
|
+
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
753
|
+
this.navObserver = null;
|
|
754
|
+
/** Tiempo activo (engagement time, #8). */
|
|
755
|
+
this.engagement = null;
|
|
756
|
+
/** Atributos internos del usuario identificado → POST /sdk/popups/contact. Null si no hay userId. */
|
|
757
|
+
this.contact = null;
|
|
758
|
+
/** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
|
|
759
|
+
this.navStarted = false;
|
|
760
|
+
}
|
|
761
|
+
/** Initialize the SDK with configuration */
|
|
762
|
+
init(config) {
|
|
763
|
+
if (this.initialized) {
|
|
764
|
+
this.log("SDK already initialized");
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const env = resolveEnvironment(config.nodeEnv);
|
|
768
|
+
this.baseUrl = env.apiBaseUrl;
|
|
769
|
+
this.env = config.nodeEnv || "production";
|
|
770
|
+
this.config = {
|
|
771
|
+
apiKey: config.apiKey || void 0,
|
|
772
|
+
debug: config.debug || false,
|
|
773
|
+
userId: config.userId || void 0
|
|
774
|
+
};
|
|
775
|
+
const storage = config.storage ?? createDefaultStorage();
|
|
776
|
+
this.tracking = new TrackingManager({
|
|
777
|
+
storage,
|
|
778
|
+
clientUserId: this.config.userId,
|
|
779
|
+
enabled: config.trackingEnabled ?? true
|
|
780
|
+
});
|
|
781
|
+
if (this.config.userId) {
|
|
782
|
+
const userId = this.config.userId;
|
|
783
|
+
const publicKey = this.config.apiKey ?? "";
|
|
784
|
+
this.contact = new ContactManager({
|
|
785
|
+
storage,
|
|
786
|
+
publicKey,
|
|
787
|
+
userId,
|
|
788
|
+
post: (body) => this.postContact(body)
|
|
789
|
+
});
|
|
790
|
+
if (config.contactAttributes) {
|
|
791
|
+
void this.setContactAttributes(config.contactAttributes);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
this.log("tracking \xB7 user_id:", this.tracking.getUserId(), "\xB7 new_user:", this.tracking.isNewUser(), "\xB7 enabled:", this.tracking.isTrackingEnabled());
|
|
795
|
+
const analyticsSink = config.analytics ? createFeedbackSink({
|
|
796
|
+
baseUrl: this.baseUrl,
|
|
797
|
+
keys: config.analytics,
|
|
798
|
+
log: (...a) => this.log(...a)
|
|
799
|
+
}) : void 0;
|
|
800
|
+
this.analytics = new AnalyticsManager({
|
|
801
|
+
sink: analyticsSink,
|
|
802
|
+
publicKey: config.analytics?.publicKey ?? this.config.apiKey,
|
|
803
|
+
language: typeof navigator !== "undefined" ? navigator.language : void 0,
|
|
804
|
+
platform: config.platform ?? "web",
|
|
805
|
+
device: config.device ?? collectDeviceInfo(config.appVersion)
|
|
806
|
+
});
|
|
807
|
+
this.navObserver = new NavigationObserver();
|
|
808
|
+
this.navObserver.onVisit((v) => this.track("page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
|
|
809
|
+
this.navObserver.install();
|
|
810
|
+
this.engagement = new EngagementTracker();
|
|
811
|
+
this.engagement.resume();
|
|
812
|
+
this.setupAnalyticsFlush();
|
|
813
|
+
this.initialized = true;
|
|
814
|
+
this.log("SDK initialized", this.config);
|
|
815
|
+
if (this.renderer.init) this.renderer.init();
|
|
816
|
+
this.setupPopupContainer();
|
|
817
|
+
this.fetchPopupsFromServer().then((defs) => {
|
|
818
|
+
this.popupDefinitions = this.validatePopupDefinitions(defs);
|
|
819
|
+
this.popupsLoaded = true;
|
|
820
|
+
this.log("Popups loaded from API", this.popupDefinitions);
|
|
821
|
+
this.configureTriggersFromDefinitions();
|
|
822
|
+
this.processDeferredExitQueue();
|
|
823
|
+
if (this.pendingAutoLaunch) {
|
|
824
|
+
this.startTriggers();
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
/** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
|
|
829
|
+
getUserId() {
|
|
830
|
+
return this.tracking?.getUserId() ?? null;
|
|
831
|
+
}
|
|
832
|
+
/** Session id de navegación actual. Null si tracking off. */
|
|
833
|
+
getSessionId() {
|
|
834
|
+
return this.tracking?.getSessionId() ?? null;
|
|
835
|
+
}
|
|
836
|
+
/** Activa/desactiva el tracking (identidad + sesión + analytics). Kill-switch del contrato §7bis. */
|
|
837
|
+
setTrackingEnabled(enabled) {
|
|
838
|
+
this.tracking?.setTrackingEnabled(enabled);
|
|
839
|
+
this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
|
|
840
|
+
}
|
|
841
|
+
// ───────── Analytics (canal separado del feedback, vinculado por user_id) ─────────
|
|
842
|
+
/** Registra un evento de analítica (modelo GA: nombre + params). No-op si tracking off. */
|
|
843
|
+
track(name, params) {
|
|
844
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
845
|
+
this.analytics?.track(name, params);
|
|
846
|
+
this.log("analytics \xB7 track:", name, params ?? {});
|
|
847
|
+
}
|
|
848
|
+
/** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
|
|
849
|
+
setUserAttributes(attributes) {
|
|
850
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
851
|
+
this.analytics?.setUserAttributes(attributes);
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
|
|
855
|
+
* Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
|
|
856
|
+
* init. Solo envía si los atributos cambiaron (diff en storage). No-op si tracking off o sin userId.
|
|
857
|
+
* @returns `true` si se envió al backend, `false` si no hubo cambios o está deshabilitado.
|
|
858
|
+
*/
|
|
859
|
+
async setContactAttributes(attributes) {
|
|
860
|
+
if (!this.tracking?.isTrackingEnabled()) return false;
|
|
861
|
+
if (!this.contact) return false;
|
|
862
|
+
return this.contact.setAttributes(attributes);
|
|
863
|
+
}
|
|
864
|
+
/** Marca el inicio de un mini-service; etiqueta los eventos siguientes. No-op si tracking off. */
|
|
865
|
+
enterMiniService(name, entryPointType) {
|
|
866
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
867
|
+
this.analytics?.enterMiniService(name, entryPointType);
|
|
868
|
+
this.log("analytics \xB7 enterMiniService:", name, entryPointType ?? "");
|
|
869
|
+
}
|
|
870
|
+
/** Cierra el mini-service activo (emite `mini_service_exit` con duración, #27). No-op si tracking off. */
|
|
871
|
+
exitMiniService() {
|
|
872
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
873
|
+
this.analytics?.exitMiniService();
|
|
874
|
+
this.log("analytics \xB7 exitMiniService");
|
|
875
|
+
}
|
|
876
|
+
/** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
|
|
877
|
+
trackSearch(query, resultsCount, params) {
|
|
878
|
+
this.track("search", { query, results_count: resultsCount, has_results: resultsCount > 0, ...params ?? {} });
|
|
879
|
+
}
|
|
880
|
+
/** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
|
|
881
|
+
trackFindabilityFriction(frictionTopic, params) {
|
|
882
|
+
this.track("findability_friction", { friction_topic: frictionTopic, ...params ?? {} });
|
|
883
|
+
}
|
|
884
|
+
/** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
|
|
885
|
+
trackFunnelStep(funnel, step, taskId, params) {
|
|
886
|
+
this.track("funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
|
|
890
|
+
* El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.
|
|
891
|
+
*/
|
|
892
|
+
setScreen(name) {
|
|
893
|
+
if (!this.navObserver) return;
|
|
894
|
+
if (!this.navStarted) {
|
|
895
|
+
this.navObserver.begin(name);
|
|
896
|
+
this.navStarted = true;
|
|
897
|
+
} else {
|
|
898
|
+
this.navObserver.visit(name);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/** App a foreground (RN: AppState 'active'): reanuda el engagement time. */
|
|
902
|
+
onForeground() {
|
|
903
|
+
this.engagement?.resume();
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* App a background (RN: AppState 'background'): cierra pantalla actual (page_view),
|
|
907
|
+
* mini-service (mini_service_exit), emite engagement y hace flush del lote.
|
|
908
|
+
*/
|
|
909
|
+
onBackground() {
|
|
910
|
+
this.navObserver?.stop();
|
|
911
|
+
this.navStarted = false;
|
|
912
|
+
this.exitMiniService();
|
|
913
|
+
this.flushEngagement();
|
|
914
|
+
this.engagement?.pause();
|
|
915
|
+
this.flushAnalytics();
|
|
916
|
+
}
|
|
917
|
+
/** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
|
|
918
|
+
previewAnalytics() {
|
|
919
|
+
return this.analytics?.buildPayload(this.analyticsIdentity()) ?? {
|
|
920
|
+
userId: null,
|
|
921
|
+
sessionId: null,
|
|
922
|
+
context: { platform: "web", attributes: {} },
|
|
923
|
+
events: []
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
/** Envía (hoy dry-run → console.log) el lote acumulado de analytics y vacía el buffer. */
|
|
927
|
+
flushAnalytics() {
|
|
928
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
929
|
+
this.analytics?.flush(this.analyticsIdentity());
|
|
930
|
+
}
|
|
931
|
+
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
932
|
+
flushEngagement() {
|
|
933
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
934
|
+
const ms = this.engagement?.consume() ?? 0;
|
|
935
|
+
if (ms > 0) this.track("user_engagement", { engagement_time_msec: ms });
|
|
936
|
+
}
|
|
937
|
+
analyticsIdentity() {
|
|
938
|
+
return {
|
|
939
|
+
userId: this.tracking?.getUserId() ?? null,
|
|
940
|
+
sessionId: this.tracking?.getSessionId() ?? null
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
/** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
|
|
944
|
+
setupAnalyticsFlush() {
|
|
945
|
+
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
946
|
+
window.addEventListener("pagehide", () => {
|
|
947
|
+
this.navObserver?.stop();
|
|
948
|
+
this.exitMiniService();
|
|
949
|
+
this.flushEngagement();
|
|
950
|
+
this.flushAnalytics();
|
|
951
|
+
});
|
|
952
|
+
document.addEventListener("visibilitychange", () => {
|
|
953
|
+
if (document.visibilityState === "hidden") {
|
|
954
|
+
this.flushEngagement();
|
|
955
|
+
this.engagement?.pause();
|
|
956
|
+
this.flushAnalytics();
|
|
957
|
+
} else if (document.visibilityState === "visible") {
|
|
958
|
+
this.engagement?.resume();
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
/** Enable auto-launch functionality with configured triggers */
|
|
963
|
+
autoLaunch() {
|
|
964
|
+
if (!this.initialized) {
|
|
965
|
+
throw new Error("SDK not initialized. Call init() first.");
|
|
966
|
+
}
|
|
967
|
+
if (!this.popupsLoaded) {
|
|
968
|
+
this.pendingAutoLaunch = true;
|
|
969
|
+
this.log("Auto-launch deferred until popups are loaded");
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
this.startTriggers();
|
|
973
|
+
}
|
|
974
|
+
/** Inicia los triggers configurados */
|
|
975
|
+
startTriggers() {
|
|
976
|
+
this.log("Auto-launch enabled");
|
|
977
|
+
this.triggers.forEach((trigger) => setupTrigger(this, trigger));
|
|
978
|
+
}
|
|
979
|
+
/** Configure triggers for auto-launching popups (manual) */
|
|
980
|
+
configureTriggers(triggers) {
|
|
981
|
+
if (!this.initialized) {
|
|
982
|
+
throw new Error("SDK not initialized. Call init() first.");
|
|
983
|
+
}
|
|
984
|
+
this.triggers = triggers;
|
|
985
|
+
this.log("Triggers configured", triggers);
|
|
986
|
+
}
|
|
987
|
+
/** Deriva triggers desde las definiciones de popup */
|
|
988
|
+
configureTriggersFromDefinitions() {
|
|
989
|
+
const derived = [];
|
|
990
|
+
this.debug("Validated popup definitions", this.popupDefinitions);
|
|
991
|
+
this.popupDefinitions.forEach((def) => {
|
|
992
|
+
def.triggers.forEach((trigger) => {
|
|
993
|
+
const type = this.mapPopupTriggerType(trigger.type);
|
|
994
|
+
if (!type) {
|
|
995
|
+
this.debug("Unsupported trigger type", trigger.type);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
const value = type === "time" && typeof trigger.value === "number" ? trigger.value * 1e3 : trigger.value;
|
|
999
|
+
derived.push({ type, value, surveyId: def.surveyId, popupId: def.id });
|
|
1000
|
+
});
|
|
1001
|
+
});
|
|
1002
|
+
if (derived.length) {
|
|
1003
|
+
this.configureTriggers(derived);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/** Lógica para evaluar condiciones antes de mostrar una encuesta */
|
|
1007
|
+
triggerSurvey(surveyId, popupId) {
|
|
1008
|
+
const def = this.findPopupDefinition(surveyId, popupId);
|
|
1009
|
+
if (!def) {
|
|
1010
|
+
this.debug("No popup definition for trigger", { surveyId, popupId });
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (!this.shouldShow(def)) {
|
|
1014
|
+
this.debug("Conditions prevented showing popup", def.id);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
this.showDefinition(def);
|
|
1018
|
+
}
|
|
1019
|
+
/** Trigger popups configured with trigger.type = 'event' and matching trigger.value */
|
|
1020
|
+
triggerEvent(eventName) {
|
|
1021
|
+
if (!this.initialized) {
|
|
1022
|
+
throw new Error("SDK not initialized. Call init() first.");
|
|
1023
|
+
}
|
|
1024
|
+
const normalized = String(eventName || "").trim();
|
|
1025
|
+
if (!normalized) {
|
|
1026
|
+
this.debug("Ignoring empty event trigger name");
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const candidates = this.popupDefinitions.filter((def) => {
|
|
1030
|
+
return def.triggers.some((trigger) => {
|
|
1031
|
+
const triggerValue = String(trigger.value ?? "").trim();
|
|
1032
|
+
return trigger.type === "event" && triggerValue === normalized;
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
1035
|
+
if (!candidates.length) {
|
|
1036
|
+
this.debug("No event popup definitions found", { eventName: normalized });
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const matched = candidates.find((def) => this.shouldShow(def));
|
|
1040
|
+
if (!matched) {
|
|
1041
|
+
this.debug("Event popup blocked by conditions/segments", { eventName: normalized });
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
this.showDefinition(matched);
|
|
1045
|
+
}
|
|
1046
|
+
showDefinition(def) {
|
|
1047
|
+
this.log("Showing popup (definition)", def);
|
|
1048
|
+
this.surveyToPopupId.set(def.surveyId, def.id);
|
|
1049
|
+
this.lastShown.set(def.id, Date.now());
|
|
1050
|
+
this.renderPopup(def.surveyId, def.productId, def.actions, def.style);
|
|
1051
|
+
this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
|
|
1052
|
+
}
|
|
1053
|
+
shouldShow(def, pathUrl, skipPathCheck = false) {
|
|
1054
|
+
if (!skipPathCheck && !this.matchesSegmentsPath(def, pathUrl)) {
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
if (!this.matchesSegmentsLang(def)) {
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
const cooldowns = def.cooldown || [];
|
|
1061
|
+
if (cooldowns.length && !cooldowns.every((cooldown) => this.evaluateCooldown(def, cooldown))) {
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
const legacyConditions = def.legacyConditions || [];
|
|
1065
|
+
if (!legacyConditions.length) return true;
|
|
1066
|
+
return legacyConditions.every((condition) => this.evaluateLegacyCondition(def, condition));
|
|
1067
|
+
}
|
|
1068
|
+
matchesSegmentsLang(def) {
|
|
1069
|
+
const langs = def.segments?.lang;
|
|
1070
|
+
if (!langs || langs.length === 0) return true;
|
|
1071
|
+
const browserLang = (typeof navigator !== "undefined" ? navigator.language : "").toLowerCase();
|
|
1072
|
+
if (!browserLang) {
|
|
1073
|
+
this.debug("No navigator.language available for lang comparison", { popupId: def.id, langs });
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
const matches = langs.some((lang) => browserLang.startsWith(lang.toLowerCase()));
|
|
1077
|
+
if (!matches) {
|
|
1078
|
+
this.debug("Lang mismatch for popup", { popupId: def.id, langs, browserLang });
|
|
1079
|
+
}
|
|
1080
|
+
return matches;
|
|
1081
|
+
}
|
|
1082
|
+
matchesSegmentsPath(def, pathUrl) {
|
|
1083
|
+
const paths = def.segments?.path;
|
|
1084
|
+
if (!paths || paths.length === 0) return true;
|
|
1085
|
+
if (typeof window === "undefined" || !window.location) {
|
|
1086
|
+
this.debug("No window.location available for path comparison", { popupId: def.id, paths });
|
|
1087
|
+
return true;
|
|
1088
|
+
}
|
|
1089
|
+
const normalizedHref = this.normalizeUrl(pathUrl || window.location.href || "");
|
|
1090
|
+
const currentUrl = this.safeParseUrl(normalizedHref);
|
|
1091
|
+
const normalizedPath = this.normalizeUrl(currentUrl?.pathname || window.location.pathname || "");
|
|
1092
|
+
const matches = paths.some((rawCandidate) => {
|
|
1093
|
+
const candidate = this.normalizeUrl(rawCandidate);
|
|
1094
|
+
let match = false;
|
|
1095
|
+
if (candidate.startsWith("http://") || candidate.startsWith("https://")) {
|
|
1096
|
+
match = normalizedHref === candidate;
|
|
1097
|
+
} else if (candidate.startsWith("/")) {
|
|
1098
|
+
match = normalizedHref.includes(candidate);
|
|
1099
|
+
} else {
|
|
1100
|
+
match = normalizedPath === candidate;
|
|
1101
|
+
}
|
|
1102
|
+
this.debug("Path comparison", {
|
|
1103
|
+
popupId: def.id,
|
|
1104
|
+
candidate,
|
|
1105
|
+
currentPath: normalizedPath,
|
|
1106
|
+
currentHref: normalizedHref,
|
|
1107
|
+
match
|
|
1108
|
+
});
|
|
1109
|
+
return match;
|
|
1110
|
+
});
|
|
1111
|
+
if (!matches) {
|
|
1112
|
+
this.debug("No path match for popup", { popupId: def.id, paths, currentPath: normalizedPath });
|
|
1113
|
+
}
|
|
1114
|
+
return matches;
|
|
1115
|
+
}
|
|
1116
|
+
evaluateCooldown(def, condition) {
|
|
1117
|
+
switch (condition.answered) {
|
|
1118
|
+
case "SHOWED":
|
|
1119
|
+
return this.hasCooldownElapsed(this.lastShown.get(def.id), condition.cooldownDays);
|
|
1120
|
+
case "PARTIAL":
|
|
1121
|
+
case "COMPLETED": {
|
|
1122
|
+
const progress = this.surveyProgress.get(def.surveyId);
|
|
1123
|
+
if (!progress || progress.status !== condition.answered) {
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
return this.hasCooldownElapsed(progress.timestamp, condition.cooldownDays);
|
|
1127
|
+
}
|
|
1128
|
+
default:
|
|
1129
|
+
return true;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
evaluateLegacyCondition(def, condition) {
|
|
1133
|
+
if (!condition.answered && this.answeredSurveys.has(def.surveyId)) {
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
return this.hasCooldownElapsed(this.lastShown.get(def.id), condition.cooldownDays);
|
|
1137
|
+
}
|
|
1138
|
+
hasCooldownElapsed(timestamp, cooldownDays) {
|
|
1139
|
+
if (!timestamp || !cooldownDays) {
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
return Date.now() - timestamp >= cooldownDays * DAY_IN_MS;
|
|
1143
|
+
}
|
|
1144
|
+
/** Marcar encuesta como contestada externamente */
|
|
1145
|
+
markSurveyAnswered(surveyId) {
|
|
1146
|
+
this.answeredSurveys.add(surveyId);
|
|
1147
|
+
this.markSurveyProgress(surveyId, "COMPLETED");
|
|
1148
|
+
}
|
|
1149
|
+
/** Queue an exit popup so it can render after navigation */
|
|
1150
|
+
queueExitPopup(surveyId, delaySeconds, sourceUrl, popupId) {
|
|
1151
|
+
const def = this.findPopupDefinition(surveyId, popupId);
|
|
1152
|
+
if (!def) {
|
|
1153
|
+
this.debug("No popup definition for exit trigger", { surveyId, popupId });
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
const originUrl = sourceUrl || (typeof window !== "undefined" ? window.location.href : "");
|
|
1157
|
+
if (!originUrl) {
|
|
1158
|
+
this.debug("Exit popup skipped: missing source URL", { surveyId });
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (!this.shouldShow(def, originUrl)) {
|
|
1162
|
+
this.debug("Exit popup skipped by conditions/path", { popupId: def.id, sourceUrl: originUrl });
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const safeDelayMs = Number.isFinite(delaySeconds) ? Math.max(0, delaySeconds * 1e3) : 0;
|
|
1166
|
+
const deferred = {
|
|
1167
|
+
id: def.id,
|
|
1168
|
+
surveyId: def.surveyId,
|
|
1169
|
+
sourceUrl: this.normalizeUrl(originUrl),
|
|
1170
|
+
dueAt: Date.now() + safeDelayMs
|
|
1171
|
+
};
|
|
1172
|
+
const queue = this.getDeferredExitQueue().filter((item) => !(item.id === deferred.id && item.sourceUrl === deferred.sourceUrl));
|
|
1173
|
+
queue.push(deferred);
|
|
1174
|
+
this.setDeferredExitQueue(queue);
|
|
1175
|
+
this.scheduleDeferredExitPopup(deferred);
|
|
1176
|
+
this.debug("Exit popup queued", deferred);
|
|
1177
|
+
}
|
|
1178
|
+
processDeferredExitQueue() {
|
|
1179
|
+
const queue = this.getDeferredExitQueue();
|
|
1180
|
+
if (!queue.length) return;
|
|
1181
|
+
const pending = [];
|
|
1182
|
+
const now = Date.now();
|
|
1183
|
+
queue.forEach((item) => {
|
|
1184
|
+
const def = this.popupDefinitions.find((popup) => popup.id === item.id && popup.surveyId === item.surveyId);
|
|
1185
|
+
if (!def) {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (item.dueAt <= now) {
|
|
1189
|
+
this.tryShowDeferredExitPopup(item);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
pending.push(item);
|
|
1193
|
+
this.scheduleDeferredExitPopup(item);
|
|
1194
|
+
});
|
|
1195
|
+
this.setDeferredExitQueue(pending);
|
|
1196
|
+
}
|
|
1197
|
+
scheduleDeferredExitPopup(item) {
|
|
1198
|
+
if (typeof window === "undefined") return;
|
|
1199
|
+
const delay = Math.max(0, item.dueAt - Date.now());
|
|
1200
|
+
const timer = window.setTimeout(() => this.tryShowDeferredExitPopup(item), delay);
|
|
1201
|
+
this.deferredExitTimers.push(timer);
|
|
1202
|
+
}
|
|
1203
|
+
tryShowDeferredExitPopup(item) {
|
|
1204
|
+
const def = this.popupDefinitions.find((popup) => popup.id === item.id && popup.surveyId === item.surveyId);
|
|
1205
|
+
if (!def) {
|
|
1206
|
+
this.removeDeferredExitPopup(item);
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
const currentUrl = typeof window !== "undefined" ? this.normalizeUrl(window.location.href || "") : "";
|
|
1210
|
+
if (!currentUrl || currentUrl === this.normalizeUrl(item.sourceUrl)) {
|
|
1211
|
+
this.removeDeferredExitPopup(item);
|
|
1212
|
+
this.debug("Exit popup dropped because route did not change", item);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (!this.shouldShow(def, void 0, true)) {
|
|
1216
|
+
this.removeDeferredExitPopup(item);
|
|
1217
|
+
this.debug("Exit popup skipped at render-time conditions", item);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
this.showDefinition(def);
|
|
1221
|
+
this.removeDeferredExitPopup(item);
|
|
1222
|
+
}
|
|
1223
|
+
findPopupDefinition(surveyId, popupId) {
|
|
1224
|
+
if (popupId) {
|
|
1225
|
+
const byId = this.popupDefinitions.find((popup) => popup.id === popupId);
|
|
1226
|
+
if (byId) return byId;
|
|
1227
|
+
}
|
|
1228
|
+
return this.popupDefinitions.find((popup) => popup.surveyId === surveyId);
|
|
1229
|
+
}
|
|
1230
|
+
removeDeferredExitPopup(item) {
|
|
1231
|
+
const queue = this.getDeferredExitQueue().filter((entry) => !(entry.id === item.id && entry.sourceUrl === item.sourceUrl));
|
|
1232
|
+
this.setDeferredExitQueue(queue);
|
|
1233
|
+
}
|
|
1234
|
+
getDeferredExitQueue() {
|
|
1235
|
+
if (typeof window === "undefined") return [];
|
|
1236
|
+
try {
|
|
1237
|
+
const raw = window.sessionStorage.getItem(EXIT_QUEUE_STORAGE_KEY);
|
|
1238
|
+
if (!raw) return [];
|
|
1239
|
+
const parsed = JSON.parse(raw);
|
|
1240
|
+
if (!Array.isArray(parsed)) return [];
|
|
1241
|
+
return parsed.filter((entry) => {
|
|
1242
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
1243
|
+
const item = entry;
|
|
1244
|
+
return typeof item.id === "string" && typeof item.surveyId === "string" && typeof item.sourceUrl === "string" && typeof item.dueAt === "number" && Number.isFinite(item.dueAt);
|
|
1245
|
+
}).map((item) => ({
|
|
1246
|
+
...item,
|
|
1247
|
+
sourceUrl: this.normalizeUrl(item.sourceUrl)
|
|
1248
|
+
}));
|
|
1249
|
+
} catch {
|
|
1250
|
+
return [];
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
setDeferredExitQueue(queue) {
|
|
1254
|
+
if (typeof window === "undefined") return;
|
|
1255
|
+
try {
|
|
1256
|
+
if (!queue.length) {
|
|
1257
|
+
window.sessionStorage.removeItem(EXIT_QUEUE_STORAGE_KEY);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
window.sessionStorage.setItem(EXIT_QUEUE_STORAGE_KEY, JSON.stringify(queue));
|
|
1261
|
+
} catch {
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
/** Fetch al servidor para obtener popups */
|
|
1265
|
+
async fetchPopupsFromServer() {
|
|
1266
|
+
const apiKey = this.config?.apiKey;
|
|
1267
|
+
const baseUrl = this.baseUrl;
|
|
1268
|
+
const userId = this.config?.userId;
|
|
1269
|
+
if (!apiKey || !baseUrl) {
|
|
1270
|
+
this.log("Missing apiKey or baseUrl. Skipping popups fetch.");
|
|
1271
|
+
return [];
|
|
1272
|
+
}
|
|
1273
|
+
const filter = userId ? { where: { userId } } : null;
|
|
1274
|
+
const query = filter ? `?filter=${encodeURIComponent(JSON.stringify(filter))}` : "";
|
|
1275
|
+
const endpoint = `${baseUrl}/sdk/${encodeURIComponent(apiKey)}/popups${query}`;
|
|
1276
|
+
try {
|
|
1277
|
+
const response = await fetch(endpoint);
|
|
1278
|
+
if (!response.ok) {
|
|
1279
|
+
this.log("Failed to fetch popups", response.status, response.statusText);
|
|
1280
|
+
return [];
|
|
1281
|
+
}
|
|
1282
|
+
const raw = await response.text();
|
|
1283
|
+
const parsed = JSON.parse(raw);
|
|
1284
|
+
if (!Array.isArray(parsed)) {
|
|
1285
|
+
this.log("Unexpected popups payload", parsed);
|
|
1286
|
+
return [];
|
|
1287
|
+
}
|
|
1288
|
+
this.debug("Fetched popups payload", parsed);
|
|
1289
|
+
return parsed;
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
this.log("Error fetching popups", error);
|
|
1292
|
+
return [];
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
/** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
|
|
1296
|
+
async postContact(body) {
|
|
1297
|
+
const baseUrl = this.baseUrl;
|
|
1298
|
+
if (!baseUrl) return;
|
|
1299
|
+
const endpoint = `${baseUrl}/sdk/popups/contact`;
|
|
1300
|
+
try {
|
|
1301
|
+
const response = await fetch(endpoint, {
|
|
1302
|
+
method: "POST",
|
|
1303
|
+
headers: { "Content-Type": "application/json" },
|
|
1304
|
+
body: JSON.stringify(body)
|
|
1305
|
+
});
|
|
1306
|
+
if (!response.ok) {
|
|
1307
|
+
this.log("Failed to post contact", response.status, response.statusText);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
this.log("contact \xB7 sent attributes for user:", body.userId);
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
this.log("Error posting contact", error);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
/** Notifica eventos de popup a la API */
|
|
1316
|
+
async postPopupEvent(status, popupId, userId) {
|
|
1317
|
+
const apiKey = this.config?.apiKey;
|
|
1318
|
+
const baseUrl = this.baseUrl;
|
|
1319
|
+
if (!apiKey || !baseUrl) {
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const endpoint = `${baseUrl}/sdk/popups`;
|
|
1323
|
+
try {
|
|
1324
|
+
const response = await fetch(endpoint, {
|
|
1325
|
+
method: "POST",
|
|
1326
|
+
headers: { "Content-Type": "application/json" },
|
|
1327
|
+
body: JSON.stringify({
|
|
1328
|
+
publicKey: apiKey,
|
|
1329
|
+
status,
|
|
1330
|
+
popupId,
|
|
1331
|
+
userId: userId || void 0
|
|
1332
|
+
})
|
|
1333
|
+
});
|
|
1334
|
+
if (!response.ok) {
|
|
1335
|
+
this.log("Failed to post popup event", response.status, response.statusText);
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
try {
|
|
1339
|
+
const data = await response.json();
|
|
1340
|
+
if (data && typeof data.sessionId === "string") {
|
|
1341
|
+
this.tracking?.setSessionId(data.sessionId);
|
|
1342
|
+
this.log("tracking \xB7 session_id from backend:", data.sessionId);
|
|
1343
|
+
}
|
|
1344
|
+
} catch {
|
|
1345
|
+
}
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
this.log("Error posting popup event", error);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
/** Add an event listener */
|
|
1351
|
+
on(eventType, listener) {
|
|
1352
|
+
if (!this.listeners.has(eventType)) {
|
|
1353
|
+
this.listeners.set(eventType, /* @__PURE__ */ new Set());
|
|
1354
|
+
}
|
|
1355
|
+
this.listeners.get(eventType).add(listener);
|
|
1356
|
+
}
|
|
1357
|
+
/** Remove an event listener */
|
|
1358
|
+
off(eventType, listener) {
|
|
1359
|
+
const listeners = this.listeners.get(eventType);
|
|
1360
|
+
if (listeners) {
|
|
1361
|
+
listeners.delete(listener);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
/** Setup popup container element */
|
|
1365
|
+
setupPopupContainer() {
|
|
1366
|
+
if (typeof document === "undefined") {
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
this.popupContainer = document.createElement("div");
|
|
1370
|
+
this.popupContainer.id = "deepdots-popup-container";
|
|
1371
|
+
this.popupContainer.style.cssText = `
|
|
1372
|
+
position: fixed;
|
|
1373
|
+
top: 0;
|
|
1374
|
+
left: 0;
|
|
1375
|
+
width: 100%;
|
|
1376
|
+
height: 100%;
|
|
1377
|
+
display: none;
|
|
1378
|
+
z-index: 999999;
|
|
1379
|
+
background: rgba(0, 0, 0, 0.5);
|
|
1380
|
+
justify-content: center;
|
|
1381
|
+
align-items: center;
|
|
1382
|
+
`;
|
|
1383
|
+
document.body.appendChild(this.popupContainer);
|
|
1384
|
+
}
|
|
1385
|
+
/** Render the popup UI */
|
|
1386
|
+
renderPopup(surveyId, productId, actions, style) {
|
|
1387
|
+
const userId = this.tracking?.getUserId() ?? this.config?.userId;
|
|
1388
|
+
const sessionId = this.tracking?.getSessionId() ?? void 0;
|
|
1389
|
+
const miniService = this.analytics?.getMiniService() ?? void 0;
|
|
1390
|
+
this.log("tracking \xB7 survey identity \u2192", { userId: userId ?? null, sessionId: sessionId ?? null, miniService: miniService ?? null });
|
|
1391
|
+
if (this.renderer) {
|
|
1392
|
+
this.renderer.show(
|
|
1393
|
+
surveyId,
|
|
1394
|
+
productId,
|
|
1395
|
+
actions,
|
|
1396
|
+
(type, id, payload) => this.emitEvent(type, id, payload),
|
|
1397
|
+
() => this.hidePopup(),
|
|
1398
|
+
this.env,
|
|
1399
|
+
userId,
|
|
1400
|
+
style,
|
|
1401
|
+
sessionId,
|
|
1402
|
+
miniService
|
|
1403
|
+
);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
if (!this.popupContainer) return;
|
|
1407
|
+
const container = this.popupContainer;
|
|
1408
|
+
void import("./renderPopup-7GGKR5BY.mjs").then(({ renderPopup }) => {
|
|
1409
|
+
renderPopup(
|
|
1410
|
+
container,
|
|
1411
|
+
surveyId,
|
|
1412
|
+
productId,
|
|
1413
|
+
actions,
|
|
1414
|
+
(type, id, payload) => this.emitEvent(type, id, payload),
|
|
1415
|
+
() => this.hidePopup(),
|
|
1416
|
+
this.env,
|
|
1417
|
+
userId,
|
|
1418
|
+
style,
|
|
1419
|
+
sessionId,
|
|
1420
|
+
miniService
|
|
1421
|
+
);
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
/** Hide the popup */
|
|
1425
|
+
hidePopup() {
|
|
1426
|
+
if (this.renderer) {
|
|
1427
|
+
this.renderer.hide();
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (this.popupContainer) {
|
|
1431
|
+
this.popupContainer.style.display = "none";
|
|
1432
|
+
this.popupContainer.innerHTML = "";
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
/** Emit an event */
|
|
1436
|
+
emitEvent(type, surveyId, data) {
|
|
1437
|
+
const event = { type, surveyId, timestamp: Date.now(), data };
|
|
1438
|
+
this.log("Event emitted", event);
|
|
1439
|
+
const isPartialClick = type === "popup_clicked" && data?.action === "partial";
|
|
1440
|
+
if (isPartialClick) {
|
|
1441
|
+
this.markSurveyProgress(surveyId, "PARTIAL");
|
|
1442
|
+
}
|
|
1443
|
+
if (type === "survey_completed") {
|
|
1444
|
+
this.markSurveyAnswered(surveyId);
|
|
1445
|
+
}
|
|
1446
|
+
if (type === "popup_shown" || type === "survey_completed" || isPartialClick) {
|
|
1447
|
+
const popupIdFromData = data?.popupId;
|
|
1448
|
+
const popupId = popupIdFromData || this.surveyToPopupId.get(surveyId);
|
|
1449
|
+
if (popupId) {
|
|
1450
|
+
const userIdFromData = data?.userId;
|
|
1451
|
+
const status = type === "popup_shown" ? "SHOWED" /* SHOWED */ : type === "survey_completed" ? "COMPLETED" /* COMPLETED */ : "PARTIAL" /* PARTIAL */;
|
|
1452
|
+
void this.postPopupEvent(status, popupId, userIdFromData || this.tracking?.getUserId() || this.config?.userId);
|
|
1453
|
+
} else {
|
|
1454
|
+
this.debug("No popupId available to post event", { type, surveyId });
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
const listeners = this.listeners.get(type);
|
|
1458
|
+
if (listeners) {
|
|
1459
|
+
listeners.forEach((listener) => {
|
|
1460
|
+
try {
|
|
1461
|
+
listener(event);
|
|
1462
|
+
} catch (error) {
|
|
1463
|
+
console.error("Error in event listener:", error);
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
/** Log debug messages */
|
|
1469
|
+
log(...args) {
|
|
1470
|
+
if (this.config?.debug) {
|
|
1471
|
+
console.log("[DeepdotsPopups]", ...args);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
/** External debug method for triggers */
|
|
1475
|
+
debug(...args) {
|
|
1476
|
+
this.log(...args);
|
|
1477
|
+
}
|
|
1478
|
+
/** Set a custom renderer */
|
|
1479
|
+
setRenderer(renderer) {
|
|
1480
|
+
this.renderer = renderer;
|
|
1481
|
+
if (this.initialized && this.renderer.init) {
|
|
1482
|
+
this.renderer.init();
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
normalizeUrl(value) {
|
|
1486
|
+
if (!value) return "";
|
|
1487
|
+
const withoutIndex = value.replace(/\/index\.html(?=($|[?#]))/i, "");
|
|
1488
|
+
return withoutIndex.length > 1 && withoutIndex.endsWith("/") ? withoutIndex.slice(0, -1) : withoutIndex;
|
|
1489
|
+
}
|
|
1490
|
+
safeParseUrl(url) {
|
|
1491
|
+
if (!url) return null;
|
|
1492
|
+
try {
|
|
1493
|
+
return new URL(url, typeof window !== "undefined" ? window.location.href : void 0);
|
|
1494
|
+
} catch {
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
mapPopupTriggerType(type) {
|
|
1499
|
+
switch (type) {
|
|
1500
|
+
case "time_on_page":
|
|
1501
|
+
return "time";
|
|
1502
|
+
case "scroll":
|
|
1503
|
+
return "scroll";
|
|
1504
|
+
case "exit":
|
|
1505
|
+
return "exit";
|
|
1506
|
+
case "click":
|
|
1507
|
+
return "click";
|
|
1508
|
+
case "event":
|
|
1509
|
+
return "event";
|
|
1510
|
+
default:
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
markSurveyProgress(surveyId, status) {
|
|
1515
|
+
const current = this.surveyProgress.get(surveyId);
|
|
1516
|
+
if (current?.status === "COMPLETED" && status !== "COMPLETED") {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
this.surveyProgress.set(surveyId, { status, timestamp: Date.now() });
|
|
1520
|
+
}
|
|
1521
|
+
isPopupDefinition(value) {
|
|
1522
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1523
|
+
const popup = value;
|
|
1524
|
+
return typeof popup.id === "string" && typeof popup.title === "string" && typeof popup.message === "string" && typeof popup.surveyId === "string" && typeof popup.productId === "string" && Array.isArray(popup.triggers) && popup.triggers.length > 0 && popup.triggers.every((trigger) => this.isPopupTrigger(trigger)) && (!popup.cooldown || popup.cooldown.every((condition) => this.isPopupTriggerCondition(condition)));
|
|
1525
|
+
}
|
|
1526
|
+
isPopupTrigger(value) {
|
|
1527
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1528
|
+
const trigger = value;
|
|
1529
|
+
return this.isPopupTriggerType(trigger.type) && (typeof trigger.value === "number" || typeof trigger.value === "string");
|
|
1530
|
+
}
|
|
1531
|
+
isPopupTriggerType(value) {
|
|
1532
|
+
return value === "time_on_page" || value === "scroll" || value === "exit" || value === "click" || value === "event";
|
|
1533
|
+
}
|
|
1534
|
+
isPopupTriggerCondition(value) {
|
|
1535
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1536
|
+
const condition = value;
|
|
1537
|
+
return POPUP_TRIGGER_CONDITION_STATUSES.includes(condition.answered) && typeof condition.cooldownDays === "number" && Number.isFinite(condition.cooldownDays) && condition.cooldownDays >= 0;
|
|
1538
|
+
}
|
|
1539
|
+
isLegacyPopupTriggerCondition(value) {
|
|
1540
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1541
|
+
const condition = value;
|
|
1542
|
+
if (condition.answered !== void 0 && typeof condition.answered !== "boolean") {
|
|
1543
|
+
return false;
|
|
1544
|
+
}
|
|
1545
|
+
if (condition.cooldownDays !== void 0) {
|
|
1546
|
+
return typeof condition.cooldownDays === "number" && Number.isFinite(condition.cooldownDays) && condition.cooldownDays >= 0;
|
|
1547
|
+
}
|
|
1548
|
+
return true;
|
|
1549
|
+
}
|
|
1550
|
+
normalizeTriggers(value) {
|
|
1551
|
+
const items = Array.isArray(value) ? value : value ? [value] : [];
|
|
1552
|
+
return items.filter((trigger) => this.isPopupTrigger(trigger)).map((trigger) => ({
|
|
1553
|
+
type: trigger.type,
|
|
1554
|
+
value: trigger.value
|
|
1555
|
+
}));
|
|
1556
|
+
}
|
|
1557
|
+
normalizeCooldown(value) {
|
|
1558
|
+
if (!Array.isArray(value)) return [];
|
|
1559
|
+
return value.filter((condition) => this.isPopupTriggerCondition(condition));
|
|
1560
|
+
}
|
|
1561
|
+
normalizeLegacyConditions(value) {
|
|
1562
|
+
if (!Array.isArray(value)) return [];
|
|
1563
|
+
return value.filter((condition) => this.isLegacyPopupTriggerCondition(condition));
|
|
1564
|
+
}
|
|
1565
|
+
normalizePopupDefinition(def) {
|
|
1566
|
+
if (typeof def !== "object" || def === null) return null;
|
|
1567
|
+
const raw = def;
|
|
1568
|
+
const rawTriggers = raw.triggers ?? raw.trigger;
|
|
1569
|
+
const triggers = this.normalizeTriggers(rawTriggers);
|
|
1570
|
+
if (!triggers.length) return null;
|
|
1571
|
+
const rawTriggerList = Array.isArray(rawTriggers) ? rawTriggers : rawTriggers ? [rawTriggers] : [];
|
|
1572
|
+
const legacyConditions = [
|
|
1573
|
+
...this.normalizeLegacyConditions(raw.conditions),
|
|
1574
|
+
...rawTriggerList.flatMap((trigger) => this.normalizeLegacyConditions(trigger.condition))
|
|
1575
|
+
];
|
|
1576
|
+
const cooldown = this.normalizeCooldown(raw.cooldown);
|
|
1577
|
+
return {
|
|
1578
|
+
...raw,
|
|
1579
|
+
triggers,
|
|
1580
|
+
cooldown,
|
|
1581
|
+
legacyConditions
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
validatePopupDefinitions(defs) {
|
|
1585
|
+
return defs.map((def) => this.normalizePopupDefinition(def)).filter((def) => !!def && this.isPopupDefinition(def));
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
// src/react-native/setup.ts
|
|
1590
|
+
function mmkvStorage(mmkv) {
|
|
1591
|
+
return {
|
|
1592
|
+
getItem: (k) => mmkv.getString(k) ?? null,
|
|
1593
|
+
setItem: (k, v) => mmkv.set(k, v),
|
|
1594
|
+
removeItem: (k) => mmkv.delete(k)
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
function collectRnDevice(d) {
|
|
1598
|
+
return {
|
|
1599
|
+
device_type: d.isTablet?.() ? "tablet" : "mobile",
|
|
1600
|
+
os_version: d.getSystemVersion?.(),
|
|
1601
|
+
device_model: d.getDeviceId?.(),
|
|
1602
|
+
app_version: d.getVersion?.()
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
function setupReactNative(sdk2, config, deps = {}) {
|
|
1606
|
+
if (deps.renderer) sdk2.setRenderer(deps.renderer);
|
|
1607
|
+
const storage = deps.mmkv ? mmkvStorage(deps.mmkv) : config.storage;
|
|
1608
|
+
const device = deps.deviceInfo ? collectRnDevice(deps.deviceInfo) : config.device;
|
|
1609
|
+
const platform = deps.platform ?? config.platform;
|
|
1610
|
+
sdk2.init({ ...config, storage, device, platform });
|
|
1611
|
+
let subscription;
|
|
1612
|
+
if (deps.appState?.addEventListener) {
|
|
1613
|
+
subscription = deps.appState.addEventListener("change", (state) => {
|
|
1614
|
+
if (state === "active") sdk2.onForeground();
|
|
1615
|
+
else sdk2.onBackground();
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
return () => {
|
|
1619
|
+
if (subscription && typeof subscription.remove === "function") subscription.remove();
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// src/react-native/index.tsx
|
|
1624
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1625
|
+
var sdk = new DeepdotsPopups();
|
|
1626
|
+
var DeepdotsContext = createContext(sdk);
|
|
1627
|
+
var useDeepdots = () => useContext(DeepdotsContext);
|
|
1628
|
+
function optionalRequire(name) {
|
|
1629
|
+
try {
|
|
1630
|
+
return __require(name);
|
|
1631
|
+
} catch {
|
|
1632
|
+
return null;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
function DeepdotsProvider({ config, children }) {
|
|
1636
|
+
const [survey, setSurvey] = useState(null);
|
|
1637
|
+
const rendererRef = useRef(null);
|
|
1638
|
+
useEffect(() => {
|
|
1639
|
+
const renderer = new ReactNativePopupRenderer({
|
|
1640
|
+
onShow: (p) => setSurvey(p),
|
|
1641
|
+
onHide: () => setSurvey(null)
|
|
1642
|
+
});
|
|
1643
|
+
rendererRef.current = renderer;
|
|
1644
|
+
const MmkvMod = optionalRequire("react-native-mmkv");
|
|
1645
|
+
const mmkv = MmkvMod?.MMKV ? new MmkvMod.MMKV({ id: "deepdots-sdk" }) : null;
|
|
1646
|
+
const deviceInfo = optionalRequire("react-native-device-info")?.default ?? null;
|
|
1647
|
+
const dispose = setupReactNative(sdk, config, {
|
|
1648
|
+
mmkv,
|
|
1649
|
+
deviceInfo,
|
|
1650
|
+
appState: AppState,
|
|
1651
|
+
platform: Platform.OS === "ios" ? "ios" : "android",
|
|
1652
|
+
renderer
|
|
1653
|
+
});
|
|
1654
|
+
return dispose;
|
|
1655
|
+
}, []);
|
|
1656
|
+
return /* @__PURE__ */ jsxs(DeepdotsContext.Provider, { value: sdk, children: [
|
|
1657
|
+
children,
|
|
1658
|
+
survey ? /* @__PURE__ */ jsx(Modal, { visible: true, transparent: true, animationType: "slide", children: /* @__PURE__ */ jsx(View, { style: { flex: 1 }, children: /* @__PURE__ */ jsx(
|
|
1659
|
+
WebView,
|
|
1660
|
+
{
|
|
1661
|
+
originWhitelist: ["*"],
|
|
1662
|
+
javaScriptEnabled: true,
|
|
1663
|
+
source: { html: survey.html },
|
|
1664
|
+
onMessage: (e) => rendererRef.current?.handleMessage(e.nativeEvent.data)
|
|
1665
|
+
}
|
|
1666
|
+
) }) }) : null
|
|
1667
|
+
] });
|
|
1668
|
+
}
|
|
1669
|
+
export {
|
|
1670
|
+
DeepdotsProvider,
|
|
1671
|
+
ReactNativePopupRenderer,
|
|
1672
|
+
setupReactNative,
|
|
1673
|
+
useDeepdots
|
|
1674
|
+
};
|
|
1675
|
+
//# sourceMappingURL=react-native.mjs.map
|