veo-sdk 0.1.0 → 0.2.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 +17 -11
- package/dist/builder.cjs +2072 -0
- package/dist/builder.cjs.map +1 -0
- package/dist/builder.d.cts +162 -0
- package/dist/builder.d.ts +162 -0
- package/dist/builder.mjs +449 -0
- package/dist/builder.mjs.map +1 -0
- package/dist/chunk-BBAA5BRS.mjs +2728 -0
- package/dist/chunk-BBAA5BRS.mjs.map +1 -0
- package/dist/guide-preview-CO5ShtNf.d.cts +137 -0
- package/dist/guide-preview-CO5ShtNf.d.ts +137 -0
- package/dist/index.cjs +1785 -727
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -28
- package/dist/index.d.ts +43 -28
- package/dist/index.mjs +343 -2009
- package/dist/index.mjs.map +1 -1
- package/dist/veo-builder.js +289 -0
- package/dist/veo-builder.js.map +1 -0
- package/dist/veo-guides.js +259 -0
- package/dist/veo-guides.js.map +1 -0
- package/dist/veo.js +2 -169
- package/dist/veo.js.map +1 -1
- package/package.json +7 -2
package/dist/index.cjs
CHANGED
|
@@ -6,6 +6,16 @@ var dom = require('@floating-ui/dom');
|
|
|
6
6
|
var SDK_VERSION = "0.0.1";
|
|
7
7
|
var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
|
|
8
8
|
var DEFAULT_FLUSH_BATCH_SIZE = 20;
|
|
9
|
+
var DEFAULT_API_URL = "https://veo-backend-production-8c78.up.railway.app";
|
|
10
|
+
|
|
11
|
+
// src/core/api-url.ts
|
|
12
|
+
function resolveApiUrl(config) {
|
|
13
|
+
const fromConfig = config?.apiUrl;
|
|
14
|
+
if (typeof fromConfig === "string" && fromConfig.trim()) return fromConfig;
|
|
15
|
+
const fromGlobal = globalThis.__VEO_API_URL__;
|
|
16
|
+
if (typeof fromGlobal === "string" && fromGlobal.trim()) return fromGlobal;
|
|
17
|
+
return DEFAULT_API_URL;
|
|
18
|
+
}
|
|
9
19
|
|
|
10
20
|
// src/utils/safe-env.ts
|
|
11
21
|
function hasWindow() {
|
|
@@ -29,507 +39,70 @@ function hasNavigatorSendBeacon() {
|
|
|
29
39
|
return typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function";
|
|
30
40
|
}
|
|
31
41
|
|
|
32
|
-
// src/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
for (let i = 0; i < random.length; i++) {
|
|
41
|
-
random[i] = Math.floor(Math.random() * 256);
|
|
42
|
-
}
|
|
42
|
+
// src/core/builder-loader.ts
|
|
43
|
+
var BUILDER_TOKEN_PARAM = "veoBuilder";
|
|
44
|
+
function isBuilderMode() {
|
|
45
|
+
if (!hasWindow()) return false;
|
|
46
|
+
try {
|
|
47
|
+
return new URLSearchParams(window.location.search).has(BUILDER_TOKEN_PARAM);
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
43
50
|
}
|
|
44
|
-
random[0] = (random[0] ?? 0) & 15 | 112;
|
|
45
|
-
random[2] = (random[2] ?? 0) & 63 | 128;
|
|
46
|
-
const hex = Array.from(random, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
47
|
-
return [
|
|
48
|
-
timeHex.slice(0, 8),
|
|
49
|
-
timeHex.slice(8, 12),
|
|
50
|
-
hex.slice(0, 4),
|
|
51
|
-
hex.slice(4, 8),
|
|
52
|
-
hex.slice(8, 20)
|
|
53
|
-
].join("-");
|
|
54
51
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
this.timer = null;
|
|
63
|
-
this.flushing = false;
|
|
64
|
-
this.retryCount = 0;
|
|
65
|
-
this.retryTimer = null;
|
|
66
|
-
this.startTimer();
|
|
67
|
-
}
|
|
68
|
-
/** Encola un evento; dispara flush si se alcanza `batchSize`. */
|
|
69
|
-
add(item) {
|
|
70
|
-
this.buffer.push(item);
|
|
71
|
-
if (this.buffer.length >= this.config.batchSize) {
|
|
72
|
-
void this.flush();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
/** Flush manual vía `fetch`. Resuelve cuando termina (los retries son diferidos). */
|
|
76
|
-
async flush() {
|
|
77
|
-
if (this.flushing || this.buffer.length === 0) return;
|
|
78
|
-
this.flushing = true;
|
|
79
|
-
const batch = this.buffer.splice(0, this.buffer.length);
|
|
80
|
-
try {
|
|
81
|
-
await this.config.transport.send("/v1/events", { events: batch });
|
|
82
|
-
this.retryCount = 0;
|
|
83
|
-
} catch (error) {
|
|
84
|
-
this.buffer.unshift(...batch);
|
|
85
|
-
this.scheduleRetry();
|
|
86
|
-
this.config.onError?.(error);
|
|
87
|
-
} finally {
|
|
88
|
-
this.flushing = false;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
/** Flush síncrono vía `sendBeacon`, para `pagehide`/`visibilitychange`. */
|
|
92
|
-
flushBeacon() {
|
|
93
|
-
if (this.buffer.length === 0) return;
|
|
94
|
-
const batch = this.buffer.splice(0, this.buffer.length);
|
|
95
|
-
const success = this.config.transport.sendBeacon("/v1/events", { events: batch });
|
|
96
|
-
if (!success) {
|
|
97
|
-
this.buffer.unshift(...batch);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
/** Detiene timers internos. Llamar al destruir el cliente. */
|
|
101
|
-
destroy() {
|
|
102
|
-
if (this.timer) clearInterval(this.timer);
|
|
103
|
-
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
104
|
-
this.timer = null;
|
|
105
|
-
this.retryTimer = null;
|
|
106
|
-
}
|
|
107
|
-
startTimer() {
|
|
108
|
-
this.timer = setInterval(() => {
|
|
109
|
-
void this.flush();
|
|
110
|
-
}, this.config.flushIntervalMs);
|
|
111
|
-
}
|
|
112
|
-
scheduleRetry() {
|
|
113
|
-
const maxRetries = this.config.maxRetries ?? 3;
|
|
114
|
-
if (this.retryCount >= maxRetries) {
|
|
115
|
-
this.retryCount = 0;
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
this.retryCount++;
|
|
119
|
-
const delayMs = Math.min(1e3 * 2 ** (this.retryCount - 1), RETRY_MAX_DELAY_MS);
|
|
120
|
-
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
121
|
-
this.retryTimer = setTimeout(() => {
|
|
122
|
-
void this.flush();
|
|
123
|
-
}, delayMs);
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// src/core/session.ts
|
|
128
|
-
var ANONYMOUS_ID_KEY = "anonymous_user_id";
|
|
129
|
-
var END_USER_ID_KEY = "end_user_id";
|
|
130
|
-
var SESSION_ID_KEY = "session_id";
|
|
131
|
-
var SESSION_LAST_ACTIVE_KEY = "session_last_active";
|
|
132
|
-
var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
133
|
-
var Session = class {
|
|
134
|
-
constructor(storage) {
|
|
135
|
-
this.storage = storage;
|
|
136
|
-
this._endUserId = null;
|
|
137
|
-
this._anonymousId = this.loadOrCreateAnonymousId();
|
|
138
|
-
this._sessionId = this.loadOrCreateSession();
|
|
139
|
-
this._endUserId = this.storage.get(END_USER_ID_KEY);
|
|
140
|
-
}
|
|
141
|
-
get anonymousId() {
|
|
142
|
-
return this._anonymousId;
|
|
143
|
-
}
|
|
144
|
-
get sessionId() {
|
|
145
|
-
return this._sessionId;
|
|
146
|
-
}
|
|
147
|
-
get endUserId() {
|
|
148
|
-
return this._endUserId;
|
|
149
|
-
}
|
|
150
|
-
/** ID a usar como visitor en eventos (end_user_id si está identificado, si no anonymous). */
|
|
151
|
-
get effectiveUserId() {
|
|
152
|
-
return this._endUserId ?? this._anonymousId;
|
|
153
|
-
}
|
|
154
|
-
setEndUserId(id) {
|
|
155
|
-
this._endUserId = id;
|
|
156
|
-
this.storage.set(END_USER_ID_KEY, id);
|
|
157
|
-
}
|
|
158
|
-
/** Renueva el timestamp de actividad. Si pasaron > 30 min, genera nueva sesión. */
|
|
159
|
-
touch() {
|
|
160
|
-
const lastActive = this.storage.get(SESSION_LAST_ACTIVE_KEY);
|
|
161
|
-
const now = Date.now();
|
|
162
|
-
if (lastActive && now - Number.parseInt(lastActive, 10) > SESSION_TIMEOUT_MS) {
|
|
163
|
-
this._sessionId = uuidv7();
|
|
164
|
-
this.storage.set(SESSION_ID_KEY, this._sessionId);
|
|
165
|
-
}
|
|
166
|
-
this.storage.set(SESSION_LAST_ACTIVE_KEY, String(now));
|
|
167
|
-
}
|
|
168
|
-
/** Limpia la identidad (reset) pero conserva el anonymous_id. */
|
|
169
|
-
reset() {
|
|
170
|
-
this._endUserId = null;
|
|
171
|
-
this.storage.remove(END_USER_ID_KEY);
|
|
172
|
-
this._sessionId = uuidv7();
|
|
173
|
-
this.storage.set(SESSION_ID_KEY, this._sessionId);
|
|
174
|
-
this.storage.set(SESSION_LAST_ACTIVE_KEY, String(Date.now()));
|
|
175
|
-
}
|
|
176
|
-
loadOrCreateAnonymousId() {
|
|
177
|
-
let id = this.storage.get(ANONYMOUS_ID_KEY);
|
|
178
|
-
if (!id) {
|
|
179
|
-
id = uuidv7();
|
|
180
|
-
this.storage.set(ANONYMOUS_ID_KEY, id);
|
|
181
|
-
}
|
|
182
|
-
return id;
|
|
183
|
-
}
|
|
184
|
-
loadOrCreateSession() {
|
|
185
|
-
const existing = this.storage.get(SESSION_ID_KEY);
|
|
186
|
-
const lastActive = this.storage.get(SESSION_LAST_ACTIVE_KEY);
|
|
187
|
-
if (existing && lastActive) {
|
|
188
|
-
const elapsed = Date.now() - Number.parseInt(lastActive, 10);
|
|
189
|
-
if (elapsed < SESSION_TIMEOUT_MS) {
|
|
190
|
-
return existing;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
const newId = uuidv7();
|
|
194
|
-
this.storage.set(SESSION_ID_KEY, newId);
|
|
195
|
-
this.storage.set(SESSION_LAST_ACTIVE_KEY, String(Date.now()));
|
|
196
|
-
return newId;
|
|
52
|
+
function maybeLoadBuilder(opts) {
|
|
53
|
+
if (!hasWindow() || !hasDocument()) return;
|
|
54
|
+
let params;
|
|
55
|
+
try {
|
|
56
|
+
params = new URLSearchParams(window.location.search);
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
197
59
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
get(key) {
|
|
205
|
-
return localStorage.getItem(STORAGE_PREFIX + key);
|
|
60
|
+
if (!params.has(BUILDER_TOKEN_PARAM)) return;
|
|
61
|
+
if (opts.apiKey && opts.apiUrl) {
|
|
62
|
+
window.__veoBuilder = {
|
|
63
|
+
apiKey: opts.apiKey,
|
|
64
|
+
apiUrl: opts.apiUrl
|
|
65
|
+
};
|
|
206
66
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
67
|
+
if (window.veoBuilder) return;
|
|
68
|
+
const url = opts.builderUrl ?? deriveBuilderUrl();
|
|
69
|
+
if (!url) {
|
|
70
|
+
if (opts.debug) {
|
|
71
|
+
console.warn("[veo] builder mode pedido pero no pude resolver la URL de veo-builder.js");
|
|
211
72
|
}
|
|
73
|
+
return;
|
|
212
74
|
}
|
|
213
|
-
|
|
214
|
-
|
|
75
|
+
if (document.querySelector(`script[src="${url}"]`)) return;
|
|
76
|
+
const script = document.createElement("script");
|
|
77
|
+
script.src = url;
|
|
78
|
+
script.async = true;
|
|
79
|
+
if (opts.debug) {
|
|
80
|
+
script.addEventListener("error", () => console.error("[veo] no pude cargar", url));
|
|
215
81
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
82
|
+
document.head.appendChild(script);
|
|
83
|
+
}
|
|
84
|
+
function deriveBuilderUrl() {
|
|
85
|
+
const scripts = document.querySelectorAll("script[src]");
|
|
86
|
+
for (const script of Array.from(scripts)) {
|
|
87
|
+
if (/\bveo(\.min)?\.js(\?.*)?$/.test(script.src)) {
|
|
88
|
+
return script.src.replace(/\bveo(\.min)?\.js/, "veo-builder$1.js");
|
|
221
89
|
}
|
|
222
|
-
for (const key of toRemove) localStorage.removeItem(key);
|
|
223
90
|
}
|
|
224
|
-
|
|
225
|
-
function readCookies() {
|
|
226
|
-
const raw = document.cookie;
|
|
227
|
-
return raw === "" ? [] : raw.split("; ");
|
|
91
|
+
return null;
|
|
228
92
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
93
|
+
|
|
94
|
+
// src/core/preview-loader.ts
|
|
95
|
+
var PREVIEW_TOKEN_PARAM = "veoPreview";
|
|
96
|
+
function getPreviewToken() {
|
|
97
|
+
if (!hasWindow()) return null;
|
|
98
|
+
try {
|
|
99
|
+
const value = new URLSearchParams(window.location.search).get(PREVIEW_TOKEN_PARAM);
|
|
100
|
+
return value ? value : null;
|
|
101
|
+
} catch {
|
|
237
102
|
return null;
|
|
238
103
|
}
|
|
239
|
-
set(key, value, options) {
|
|
240
|
-
const maxAgeSec = Math.floor((options?.ttlMs ?? ONE_YEAR_MS) / 1e3);
|
|
241
|
-
const secure = typeof location !== "undefined" && location.protocol === "https:" ? "; Secure" : "";
|
|
242
|
-
document.cookie = `${STORAGE_PREFIX}${key}=${encodeURIComponent(
|
|
243
|
-
value
|
|
244
|
-
)}; Path=/; Max-Age=${maxAgeSec}; SameSite=Lax${secure}`;
|
|
245
|
-
}
|
|
246
|
-
remove(key) {
|
|
247
|
-
document.cookie = `${STORAGE_PREFIX}${key}=; Path=/; Max-Age=0; SameSite=Lax`;
|
|
248
|
-
}
|
|
249
|
-
clear() {
|
|
250
|
-
for (const cookie of readCookies()) {
|
|
251
|
-
const eq = cookie.indexOf("=");
|
|
252
|
-
const name = eq === -1 ? cookie : cookie.slice(0, eq);
|
|
253
|
-
if (name.startsWith(STORAGE_PREFIX)) {
|
|
254
|
-
this.remove(name.slice(STORAGE_PREFIX.length));
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
var MemoryAdapter = class {
|
|
260
|
-
constructor() {
|
|
261
|
-
this.store = /* @__PURE__ */ new Map();
|
|
262
|
-
}
|
|
263
|
-
get(key) {
|
|
264
|
-
return this.store.get(key) ?? null;
|
|
265
|
-
}
|
|
266
|
-
set(key, value) {
|
|
267
|
-
this.store.set(key, value);
|
|
268
|
-
}
|
|
269
|
-
remove(key) {
|
|
270
|
-
this.store.delete(key);
|
|
271
|
-
}
|
|
272
|
-
clear() {
|
|
273
|
-
this.store.clear();
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
function createStorage(preferred = "localStorage") {
|
|
277
|
-
if (preferred === "localStorage" && hasLocalStorage()) return new LocalStorageAdapter();
|
|
278
|
-
if (preferred === "cookie" && hasDocument()) return new CookieAdapter();
|
|
279
|
-
if (preferred === "memory") return new MemoryAdapter();
|
|
280
|
-
if (hasLocalStorage()) return new LocalStorageAdapter();
|
|
281
|
-
if (hasDocument()) return new CookieAdapter();
|
|
282
|
-
return new MemoryAdapter();
|
|
283
104
|
}
|
|
284
105
|
|
|
285
|
-
// src/core/transport.ts
|
|
286
|
-
var TransportError = class extends Error {
|
|
287
|
-
constructor(message, statusCode) {
|
|
288
|
-
super(message);
|
|
289
|
-
this.statusCode = statusCode;
|
|
290
|
-
this.name = "TransportError";
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
var Transport = class {
|
|
294
|
-
constructor(config) {
|
|
295
|
-
this.config = config;
|
|
296
|
-
}
|
|
297
|
-
/** Envío normal con `fetch`. Lanza `TransportError` si la respuesta no es OK. */
|
|
298
|
-
async send(path, body) {
|
|
299
|
-
const url = `${this.config.apiUrl}${path}`;
|
|
300
|
-
const controller = new AbortController();
|
|
301
|
-
const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 1e4);
|
|
302
|
-
try {
|
|
303
|
-
const response = await fetch(url, {
|
|
304
|
-
method: "POST",
|
|
305
|
-
headers: {
|
|
306
|
-
"Content-Type": "application/json",
|
|
307
|
-
"X-Api-Key": this.config.apiKey,
|
|
308
|
-
"X-Sdk-Version": this.config.sdkVersion
|
|
309
|
-
},
|
|
310
|
-
body: JSON.stringify(body),
|
|
311
|
-
keepalive: true,
|
|
312
|
-
signal: controller.signal
|
|
313
|
-
});
|
|
314
|
-
if (!response.ok) {
|
|
315
|
-
throw new TransportError(`HTTP ${response.status}`, response.status);
|
|
316
|
-
}
|
|
317
|
-
return await response.json();
|
|
318
|
-
} finally {
|
|
319
|
-
clearTimeout(timeoutId);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Envío con `navigator.sendBeacon`. Fire-and-forget, pensado para `pagehide`.
|
|
324
|
-
* Como sendBeacon no admite headers, la API key viaja por query string.
|
|
325
|
-
*/
|
|
326
|
-
sendBeacon(path, body) {
|
|
327
|
-
if (!hasNavigatorSendBeacon()) return false;
|
|
328
|
-
const url = new URL(`${this.config.apiUrl}${path}`);
|
|
329
|
-
url.searchParams.set("key", this.config.apiKey);
|
|
330
|
-
url.searchParams.set("v", this.config.sdkVersion);
|
|
331
|
-
const blob = new Blob([JSON.stringify(body)], { type: "application/json" });
|
|
332
|
-
return navigator.sendBeacon(url.toString(), blob);
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
// src/core/client.ts
|
|
337
|
-
var ORGANIZATION_ID_KEY = "organization_id";
|
|
338
|
-
var END_USER_ID_KEY2 = "end_user_id";
|
|
339
|
-
var IDENTIFY_SIG_KEY = "identify_signature";
|
|
340
|
-
function stableStringify(value) {
|
|
341
|
-
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
342
|
-
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
343
|
-
const entries = Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`);
|
|
344
|
-
return `{${entries.join(",")}}`;
|
|
345
|
-
}
|
|
346
|
-
var Client = class {
|
|
347
|
-
constructor(config) {
|
|
348
|
-
this.config = config;
|
|
349
|
-
this.plugins = [];
|
|
350
|
-
this.currentOrganizationId = null;
|
|
351
|
-
this.listeners = {
|
|
352
|
-
pageview: /* @__PURE__ */ new Set(),
|
|
353
|
-
identify: /* @__PURE__ */ new Set()
|
|
354
|
-
};
|
|
355
|
-
if (!config.apiKey) throw new Error("apiKey is required");
|
|
356
|
-
if (!config.apiUrl) throw new Error("apiUrl is required");
|
|
357
|
-
this.storage = createStorage(config.storage ?? "localStorage");
|
|
358
|
-
this.session = new Session(this.storage);
|
|
359
|
-
this.transport = new Transport({
|
|
360
|
-
apiKey: config.apiKey,
|
|
361
|
-
apiUrl: config.apiUrl.replace(/\/$/, ""),
|
|
362
|
-
sdkVersion: SDK_VERSION
|
|
363
|
-
});
|
|
364
|
-
this.queue = new Queue({
|
|
365
|
-
transport: this.transport,
|
|
366
|
-
flushIntervalMs: config.flushInterval ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
367
|
-
batchSize: config.flushBatchSize ?? DEFAULT_FLUSH_BATCH_SIZE,
|
|
368
|
-
onError: (err) => {
|
|
369
|
-
if (config.debug) console.error("[veo] flush error:", err);
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
const persistedUserId = this.storage.get(END_USER_ID_KEY2);
|
|
373
|
-
if (persistedUserId) this.session.setEndUserId(persistedUserId);
|
|
374
|
-
const persistedOrgId = this.storage.get(ORGANIZATION_ID_KEY);
|
|
375
|
-
if (persistedOrgId) this.currentOrganizationId = persistedOrgId;
|
|
376
|
-
this.setupUnloadHandlers();
|
|
377
|
-
if (config.debug) {
|
|
378
|
-
console.log("[veo] initialized", {
|
|
379
|
-
anonymousId: this.session.anonymousId,
|
|
380
|
-
sessionId: this.session.sessionId,
|
|
381
|
-
endUserId: this.session.endUserId
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
identify(payload) {
|
|
386
|
-
const { visitor, organization } = payload;
|
|
387
|
-
const signature = stableStringify({
|
|
388
|
-
id: visitor.id,
|
|
389
|
-
traits: visitor.traits ?? {},
|
|
390
|
-
org: organization ? { id: organization.id, attributes: organization.attributes ?? {} } : null
|
|
391
|
-
});
|
|
392
|
-
if (this.session.endUserId === visitor.id && this.storage.get(IDENTIFY_SIG_KEY) === signature) {
|
|
393
|
-
if (this.config.debug) console.debug("[veo] identify: sin cambios, omitido");
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
const previousAnonymousId = this.session.endUserId === null ? this.session.anonymousId : void 0;
|
|
397
|
-
this.session.setEndUserId(visitor.id);
|
|
398
|
-
if (organization) {
|
|
399
|
-
this.currentOrganizationId = organization.id;
|
|
400
|
-
this.storage.set(ORGANIZATION_ID_KEY, organization.id);
|
|
401
|
-
}
|
|
402
|
-
const endUser = {
|
|
403
|
-
id: visitor.id,
|
|
404
|
-
traits: visitor.traits ?? {},
|
|
405
|
-
...previousAnonymousId !== void 0 ? { anonymousId: previousAnonymousId } : {}
|
|
406
|
-
};
|
|
407
|
-
const body = {
|
|
408
|
-
endUser,
|
|
409
|
-
...organization ? { organization: { id: organization.id, attributes: organization.attributes ?? {} } } : {}
|
|
410
|
-
};
|
|
411
|
-
void this.transport.send("/v1/identify", body).catch((err) => {
|
|
412
|
-
if (this.config.debug) console.error("[veo] identify failed:", err);
|
|
413
|
-
});
|
|
414
|
-
this.enqueue({
|
|
415
|
-
actionType: "identify",
|
|
416
|
-
actionProperties: {
|
|
417
|
-
traits: visitor.traits,
|
|
418
|
-
organizationAttributes: organization?.attributes
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
this.storage.set(IDENTIFY_SIG_KEY, signature);
|
|
422
|
-
this.emit("identify", { endUserId: visitor.id, sessionId: this.session.sessionId });
|
|
423
|
-
}
|
|
424
|
-
track(name, properties) {
|
|
425
|
-
if (!name || typeof name !== "string") {
|
|
426
|
-
if (this.config.debug) console.error("[veo] track: name is required");
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
this.enqueue({
|
|
430
|
-
actionType: "track",
|
|
431
|
-
actionName: name,
|
|
432
|
-
...properties !== void 0 ? { actionProperties: properties } : {}
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
pageview(path) {
|
|
436
|
-
if (!hasDocument()) return;
|
|
437
|
-
const url = window.location.href;
|
|
438
|
-
const pagePath = path ?? window.location.pathname;
|
|
439
|
-
this.enqueue({
|
|
440
|
-
actionType: "pageview",
|
|
441
|
-
pageUrl: url,
|
|
442
|
-
pagePath,
|
|
443
|
-
pageTitle: document.title,
|
|
444
|
-
pageReferrer: document.referrer
|
|
445
|
-
});
|
|
446
|
-
this.emit("pageview", {
|
|
447
|
-
url,
|
|
448
|
-
path: pagePath,
|
|
449
|
-
sessionId: this.session.sessionId,
|
|
450
|
-
endUserId: this.session.endUserId
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
reset() {
|
|
454
|
-
this.session.reset();
|
|
455
|
-
this.currentOrganizationId = null;
|
|
456
|
-
this.storage.remove(ORGANIZATION_ID_KEY);
|
|
457
|
-
this.storage.remove(END_USER_ID_KEY2);
|
|
458
|
-
this.storage.remove(IDENTIFY_SIG_KEY);
|
|
459
|
-
}
|
|
460
|
-
flush() {
|
|
461
|
-
return this.queue.flush();
|
|
462
|
-
}
|
|
463
|
-
use(plugin) {
|
|
464
|
-
this.plugins.push(plugin);
|
|
465
|
-
plugin.install(this);
|
|
466
|
-
return this;
|
|
467
|
-
}
|
|
468
|
-
getEndUserId() {
|
|
469
|
-
return this.session.endUserId;
|
|
470
|
-
}
|
|
471
|
-
getSessionId() {
|
|
472
|
-
return this.session.sessionId;
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Suscribe `listener` a `event`. Devuelve una función para desuscribir.
|
|
476
|
-
* Los errores en listeners se logean en debug mode pero NO se propagan
|
|
477
|
-
* (un plugin no debe poder romper a otro).
|
|
478
|
-
*/
|
|
479
|
-
on(event, listener) {
|
|
480
|
-
this.listeners[event].add(listener);
|
|
481
|
-
return () => {
|
|
482
|
-
this.listeners[event].delete(listener);
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
emit(event, payload) {
|
|
486
|
-
for (const listener of this.listeners[event]) {
|
|
487
|
-
try {
|
|
488
|
-
listener(payload);
|
|
489
|
-
} catch (err) {
|
|
490
|
-
if (this.config.debug) console.error(`[veo] listener for '${event}' failed:`, err);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
/**
|
|
495
|
-
* Construye el evento completo y lo agrega a la queue. Omite los campos
|
|
496
|
-
* opcionales no definidos (exigido por `exactOptionalPropertyTypes`).
|
|
497
|
-
* @internal
|
|
498
|
-
*/
|
|
499
|
-
enqueue(input) {
|
|
500
|
-
this.session.touch();
|
|
501
|
-
const organizationId = this.currentOrganizationId ?? void 0;
|
|
502
|
-
const pageUrl = input.pageUrl ?? (hasWindow() ? window.location.href : void 0);
|
|
503
|
-
const pagePath = input.pagePath ?? (hasWindow() ? window.location.pathname : void 0);
|
|
504
|
-
const pageTitle = input.pageTitle ?? (hasDocument() ? document.title : void 0);
|
|
505
|
-
const pageReferrer = input.pageReferrer ?? (hasDocument() ? document.referrer : void 0);
|
|
506
|
-
const event = {
|
|
507
|
-
actionId: uuidv7(),
|
|
508
|
-
endUserId: this.session.effectiveUserId,
|
|
509
|
-
sessionId: this.session.sessionId,
|
|
510
|
-
actionType: input.actionType,
|
|
511
|
-
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
512
|
-
...organizationId !== void 0 ? { organizationId } : {},
|
|
513
|
-
...input.actionName !== void 0 ? { actionName: input.actionName } : {},
|
|
514
|
-
...pageUrl !== void 0 ? { pageUrl } : {},
|
|
515
|
-
...pagePath !== void 0 ? { pagePath } : {},
|
|
516
|
-
...pageTitle !== void 0 ? { pageTitle } : {},
|
|
517
|
-
...pageReferrer !== void 0 ? { pageReferrer } : {},
|
|
518
|
-
...input.actionProperties !== void 0 ? { actionProperties: input.actionProperties } : {}
|
|
519
|
-
};
|
|
520
|
-
this.queue.add(event);
|
|
521
|
-
}
|
|
522
|
-
setupUnloadHandlers() {
|
|
523
|
-
if (!hasWindow()) return;
|
|
524
|
-
window.addEventListener("pagehide", () => this.queue.flushBeacon());
|
|
525
|
-
window.addEventListener("visibilitychange", () => {
|
|
526
|
-
if (document.visibilityState === "hidden") {
|
|
527
|
-
this.queue.flushBeacon();
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
};
|
|
532
|
-
|
|
533
106
|
// src/plugins/plugin.ts
|
|
534
107
|
function definePlugin(plugin) {
|
|
535
108
|
return plugin;
|
|
@@ -756,119 +329,690 @@ var ElementThrottle = class {
|
|
|
756
329
|
this.windowMs = windowMs;
|
|
757
330
|
this.lastFired = /* @__PURE__ */ new WeakMap();
|
|
758
331
|
}
|
|
759
|
-
/** Devuelve true si el elemento puede emitir ahora (y registra el disparo). */
|
|
760
|
-
shouldFire(element) {
|
|
761
|
-
const now = Date.now();
|
|
762
|
-
const last = this.lastFired.get(element) ?? 0;
|
|
763
|
-
if (now - last < this.windowMs) return false;
|
|
764
|
-
this.lastFired.set(element, now);
|
|
765
|
-
return true;
|
|
332
|
+
/** Devuelve true si el elemento puede emitir ahora (y registra el disparo). */
|
|
333
|
+
shouldFire(element) {
|
|
334
|
+
const now = Date.now();
|
|
335
|
+
const last = this.lastFired.get(element) ?? 0;
|
|
336
|
+
if (now - last < this.windowMs) return false;
|
|
337
|
+
this.lastFired.set(element, now);
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// src/plugins/autocapture/dom-listener.ts
|
|
343
|
+
var DomListener = class {
|
|
344
|
+
constructor(client, config) {
|
|
345
|
+
this.client = client;
|
|
346
|
+
this.cleanups = [];
|
|
347
|
+
const d = DEFAULT_AUTOCAPTURE_CONFIG;
|
|
348
|
+
this.config = {
|
|
349
|
+
enabled: config.enabled ?? d.enabled,
|
|
350
|
+
captureClicks: config.captureClicks ?? d.captureClicks,
|
|
351
|
+
captureSubmits: config.captureSubmits ?? d.captureSubmits,
|
|
352
|
+
captureChanges: config.captureChanges ?? d.captureChanges,
|
|
353
|
+
maskAllInputs: config.maskAllInputs ?? d.maskAllInputs,
|
|
354
|
+
maskAllText: config.maskAllText ?? d.maskAllText,
|
|
355
|
+
blockSelectors: config.blockSelectors ?? [],
|
|
356
|
+
throttleMs: config.throttleMs ?? d.throttleMs,
|
|
357
|
+
maxTextLength: config.maxTextLength ?? d.maxTextLength,
|
|
358
|
+
captureElementMetadata: config.captureElementMetadata ?? d.captureElementMetadata,
|
|
359
|
+
captureAncestors: Math.min(config.captureAncestors ?? d.captureAncestors, MAX_ANCESTORS)
|
|
360
|
+
};
|
|
361
|
+
this.throttle = new ElementThrottle(this.config.throttleMs);
|
|
362
|
+
}
|
|
363
|
+
install() {
|
|
364
|
+
if (!hasDocument() || !this.config.enabled) return;
|
|
365
|
+
if (this.config.captureClicks) this.attach("click", (e) => this.handleClick(e));
|
|
366
|
+
if (this.config.captureSubmits) this.attach("submit", (e) => this.handleSubmit(e), true);
|
|
367
|
+
if (this.config.captureChanges) this.attach("change", (e) => this.handleChange(e), true);
|
|
368
|
+
}
|
|
369
|
+
destroy() {
|
|
370
|
+
for (const cleanup of this.cleanups) cleanup();
|
|
371
|
+
this.cleanups = [];
|
|
372
|
+
}
|
|
373
|
+
attach(eventName, handler, useCapture = false) {
|
|
374
|
+
const listener = (e) => {
|
|
375
|
+
if (!e.isTrusted) return;
|
|
376
|
+
try {
|
|
377
|
+
handler(e);
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
document.addEventListener(eventName, listener, useCapture);
|
|
382
|
+
this.cleanups.push(() => document.removeEventListener(eventName, listener, useCapture));
|
|
383
|
+
}
|
|
384
|
+
handleClick(event) {
|
|
385
|
+
const target = event.target;
|
|
386
|
+
if (!(target instanceof Element)) return;
|
|
387
|
+
const meaningful = this.findMeaningfulAncestor(target);
|
|
388
|
+
if (!meaningful) return;
|
|
389
|
+
if (isElementBlocked(meaningful, this.config.blockSelectors)) return;
|
|
390
|
+
if (!this.throttle.shouldFire(meaningful)) return;
|
|
391
|
+
this.emit("click", meaningful);
|
|
392
|
+
}
|
|
393
|
+
handleSubmit(event) {
|
|
394
|
+
const target = event.target;
|
|
395
|
+
if (!(target instanceof Element) || target.tagName.toLowerCase() !== "form") return;
|
|
396
|
+
if (isElementBlocked(target, this.config.blockSelectors)) return;
|
|
397
|
+
if (!this.throttle.shouldFire(target)) return;
|
|
398
|
+
this.emit("submit", target);
|
|
399
|
+
}
|
|
400
|
+
handleChange(event) {
|
|
401
|
+
const target = event.target;
|
|
402
|
+
if (!(target instanceof Element)) return;
|
|
403
|
+
const tag = target.tagName.toLowerCase();
|
|
404
|
+
if (tag !== "select" && tag !== "input" && tag !== "textarea") return;
|
|
405
|
+
if (target instanceof HTMLInputElement && target.type !== "checkbox" && target.type !== "radio") {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (isElementBlocked(target, this.config.blockSelectors)) return;
|
|
409
|
+
if (!this.throttle.shouldFire(target)) return;
|
|
410
|
+
this.emit("change", target);
|
|
411
|
+
}
|
|
412
|
+
/** Busca hacia arriba un ancestro "interactivo". Devuelve null si no hay ninguno. */
|
|
413
|
+
findMeaningfulAncestor(element) {
|
|
414
|
+
let current = element;
|
|
415
|
+
let depth = 0;
|
|
416
|
+
while (current && current !== document.documentElement && depth < 5) {
|
|
417
|
+
const tag = current.tagName.toLowerCase();
|
|
418
|
+
const isInteractive = tag === "button" || tag === "a" || tag === "input" || tag === "select" || tag === "textarea" || current.hasAttribute("data-veo-tag") || current.hasAttribute("role") || current.hasAttribute("onclick") || current instanceof HTMLElement && current.style.cursor === "pointer";
|
|
419
|
+
if (isInteractive) return current;
|
|
420
|
+
current = current.parentElement;
|
|
421
|
+
depth++;
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
emit(eventType, element) {
|
|
426
|
+
const payload = buildAutocapturePayload(eventType, element, this.config);
|
|
427
|
+
this.client.track("$autocapture", payload);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// src/plugins/autocapture/index.ts
|
|
432
|
+
function autocapturePlugin(config = {}) {
|
|
433
|
+
let listener = null;
|
|
434
|
+
return definePlugin({
|
|
435
|
+
name: "autocapture",
|
|
436
|
+
install(client) {
|
|
437
|
+
listener = new DomListener(client, config);
|
|
438
|
+
listener.install();
|
|
439
|
+
},
|
|
440
|
+
destroy() {
|
|
441
|
+
listener?.destroy();
|
|
442
|
+
listener = null;
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/plugins/spa-router.ts
|
|
448
|
+
function spaRouterPlugin() {
|
|
449
|
+
return definePlugin({
|
|
450
|
+
name: "spa-router",
|
|
451
|
+
install(client) {
|
|
452
|
+
if (!hasWindow()) return;
|
|
453
|
+
client.pageview();
|
|
454
|
+
const fire = () => {
|
|
455
|
+
queueMicrotask(() => client.pageview());
|
|
456
|
+
};
|
|
457
|
+
const origPushState = window.history.pushState;
|
|
458
|
+
const origReplaceState = window.history.replaceState;
|
|
459
|
+
window.history.pushState = function(...args) {
|
|
460
|
+
origPushState.apply(this, args);
|
|
461
|
+
fire();
|
|
462
|
+
};
|
|
463
|
+
window.history.replaceState = function(...args) {
|
|
464
|
+
origReplaceState.apply(this, args);
|
|
465
|
+
fire();
|
|
466
|
+
};
|
|
467
|
+
window.addEventListener("popstate", fire);
|
|
468
|
+
window.addEventListener("hashchange", fire);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/utils/uuid.ts
|
|
474
|
+
function uuidv7() {
|
|
475
|
+
const timestamp = Date.now();
|
|
476
|
+
const timeHex = timestamp.toString(16).padStart(12, "0");
|
|
477
|
+
const random = new Uint8Array(10);
|
|
478
|
+
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
|
|
479
|
+
crypto.getRandomValues(random);
|
|
480
|
+
} else {
|
|
481
|
+
for (let i = 0; i < random.length; i++) {
|
|
482
|
+
random[i] = Math.floor(Math.random() * 256);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
random[0] = (random[0] ?? 0) & 15 | 112;
|
|
486
|
+
random[2] = (random[2] ?? 0) & 63 | 128;
|
|
487
|
+
const hex = Array.from(random, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
488
|
+
return [
|
|
489
|
+
timeHex.slice(0, 8),
|
|
490
|
+
timeHex.slice(8, 12),
|
|
491
|
+
hex.slice(0, 4),
|
|
492
|
+
hex.slice(4, 8),
|
|
493
|
+
hex.slice(8, 20)
|
|
494
|
+
].join("-");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/core/queue.ts
|
|
498
|
+
var RETRY_MAX_DELAY_MS = 3e4;
|
|
499
|
+
var Queue = class {
|
|
500
|
+
constructor(config) {
|
|
501
|
+
this.config = config;
|
|
502
|
+
this.buffer = [];
|
|
503
|
+
this.timer = null;
|
|
504
|
+
this.flushing = false;
|
|
505
|
+
this.retryCount = 0;
|
|
506
|
+
this.retryTimer = null;
|
|
507
|
+
this.startTimer();
|
|
508
|
+
}
|
|
509
|
+
/** Encola un evento; dispara flush si se alcanza `batchSize`. */
|
|
510
|
+
add(item) {
|
|
511
|
+
this.buffer.push(item);
|
|
512
|
+
if (this.buffer.length >= this.config.batchSize) {
|
|
513
|
+
void this.flush();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/** Flush manual vía `fetch`. Resuelve cuando termina (los retries son diferidos). */
|
|
517
|
+
async flush() {
|
|
518
|
+
if (this.flushing || this.buffer.length === 0) return;
|
|
519
|
+
this.flushing = true;
|
|
520
|
+
const batch = this.buffer.splice(0, this.buffer.length);
|
|
521
|
+
try {
|
|
522
|
+
await this.config.transport.send("/v1/events", { events: batch });
|
|
523
|
+
this.retryCount = 0;
|
|
524
|
+
} catch (error) {
|
|
525
|
+
this.buffer.unshift(...batch);
|
|
526
|
+
this.scheduleRetry();
|
|
527
|
+
this.config.onError?.(error);
|
|
528
|
+
} finally {
|
|
529
|
+
this.flushing = false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/** Flush síncrono vía `sendBeacon`, para `pagehide`/`visibilitychange`. */
|
|
533
|
+
flushBeacon() {
|
|
534
|
+
if (this.buffer.length === 0) return;
|
|
535
|
+
const batch = this.buffer.splice(0, this.buffer.length);
|
|
536
|
+
const success = this.config.transport.sendBeacon("/v1/events", { events: batch });
|
|
537
|
+
if (!success) {
|
|
538
|
+
this.buffer.unshift(...batch);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/** Detiene timers internos. Llamar al destruir el cliente. */
|
|
542
|
+
destroy() {
|
|
543
|
+
if (this.timer) clearInterval(this.timer);
|
|
544
|
+
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
545
|
+
this.timer = null;
|
|
546
|
+
this.retryTimer = null;
|
|
547
|
+
}
|
|
548
|
+
startTimer() {
|
|
549
|
+
this.timer = setInterval(() => {
|
|
550
|
+
void this.flush();
|
|
551
|
+
}, this.config.flushIntervalMs);
|
|
552
|
+
}
|
|
553
|
+
scheduleRetry() {
|
|
554
|
+
const maxRetries = this.config.maxRetries ?? 3;
|
|
555
|
+
if (this.retryCount >= maxRetries) {
|
|
556
|
+
this.retryCount = 0;
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
this.retryCount++;
|
|
560
|
+
const delayMs = Math.min(1e3 * 2 ** (this.retryCount - 1), RETRY_MAX_DELAY_MS);
|
|
561
|
+
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
562
|
+
this.retryTimer = setTimeout(() => {
|
|
563
|
+
void this.flush();
|
|
564
|
+
}, delayMs);
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// src/core/session.ts
|
|
569
|
+
var ANONYMOUS_ID_KEY = "anonymous_user_id";
|
|
570
|
+
var END_USER_ID_KEY = "end_user_id";
|
|
571
|
+
var SESSION_ID_KEY = "session_id";
|
|
572
|
+
var SESSION_LAST_ACTIVE_KEY = "session_last_active";
|
|
573
|
+
var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
574
|
+
var Session = class {
|
|
575
|
+
constructor(storage) {
|
|
576
|
+
this.storage = storage;
|
|
577
|
+
this._endUserId = null;
|
|
578
|
+
this._anonymousId = this.loadOrCreateAnonymousId();
|
|
579
|
+
this._sessionId = this.loadOrCreateSession();
|
|
580
|
+
this._endUserId = this.storage.get(END_USER_ID_KEY);
|
|
581
|
+
}
|
|
582
|
+
get anonymousId() {
|
|
583
|
+
return this._anonymousId;
|
|
584
|
+
}
|
|
585
|
+
get sessionId() {
|
|
586
|
+
return this._sessionId;
|
|
587
|
+
}
|
|
588
|
+
get endUserId() {
|
|
589
|
+
return this._endUserId;
|
|
590
|
+
}
|
|
591
|
+
/** ID a usar como visitor en eventos (end_user_id si está identificado, si no anonymous). */
|
|
592
|
+
get effectiveUserId() {
|
|
593
|
+
return this._endUserId ?? this._anonymousId;
|
|
594
|
+
}
|
|
595
|
+
setEndUserId(id) {
|
|
596
|
+
this._endUserId = id;
|
|
597
|
+
this.storage.set(END_USER_ID_KEY, id);
|
|
598
|
+
}
|
|
599
|
+
/** Renueva el timestamp de actividad. Si pasaron > 30 min, genera nueva sesión. */
|
|
600
|
+
touch() {
|
|
601
|
+
const lastActive = this.storage.get(SESSION_LAST_ACTIVE_KEY);
|
|
602
|
+
const now = Date.now();
|
|
603
|
+
if (lastActive && now - Number.parseInt(lastActive, 10) > SESSION_TIMEOUT_MS) {
|
|
604
|
+
this._sessionId = uuidv7();
|
|
605
|
+
this.storage.set(SESSION_ID_KEY, this._sessionId);
|
|
606
|
+
}
|
|
607
|
+
this.storage.set(SESSION_LAST_ACTIVE_KEY, String(now));
|
|
608
|
+
}
|
|
609
|
+
/** Limpia la identidad (reset) pero conserva el anonymous_id. */
|
|
610
|
+
reset() {
|
|
611
|
+
this._endUserId = null;
|
|
612
|
+
this.storage.remove(END_USER_ID_KEY);
|
|
613
|
+
this._sessionId = uuidv7();
|
|
614
|
+
this.storage.set(SESSION_ID_KEY, this._sessionId);
|
|
615
|
+
this.storage.set(SESSION_LAST_ACTIVE_KEY, String(Date.now()));
|
|
616
|
+
}
|
|
617
|
+
loadOrCreateAnonymousId() {
|
|
618
|
+
let id = this.storage.get(ANONYMOUS_ID_KEY);
|
|
619
|
+
if (!id) {
|
|
620
|
+
id = uuidv7();
|
|
621
|
+
this.storage.set(ANONYMOUS_ID_KEY, id);
|
|
622
|
+
}
|
|
623
|
+
return id;
|
|
624
|
+
}
|
|
625
|
+
loadOrCreateSession() {
|
|
626
|
+
const existing = this.storage.get(SESSION_ID_KEY);
|
|
627
|
+
const lastActive = this.storage.get(SESSION_LAST_ACTIVE_KEY);
|
|
628
|
+
if (existing && lastActive) {
|
|
629
|
+
const elapsed = Date.now() - Number.parseInt(lastActive, 10);
|
|
630
|
+
if (elapsed < SESSION_TIMEOUT_MS) {
|
|
631
|
+
return existing;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const newId = uuidv7();
|
|
635
|
+
this.storage.set(SESSION_ID_KEY, newId);
|
|
636
|
+
this.storage.set(SESSION_LAST_ACTIVE_KEY, String(Date.now()));
|
|
637
|
+
return newId;
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// src/core/storage.ts
|
|
642
|
+
var STORAGE_PREFIX = "veo:";
|
|
643
|
+
var ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
644
|
+
var LocalStorageAdapter = class {
|
|
645
|
+
get(key) {
|
|
646
|
+
return localStorage.getItem(STORAGE_PREFIX + key);
|
|
647
|
+
}
|
|
648
|
+
set(key, value) {
|
|
649
|
+
try {
|
|
650
|
+
localStorage.setItem(STORAGE_PREFIX + key, value);
|
|
651
|
+
} catch {
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
remove(key) {
|
|
655
|
+
localStorage.removeItem(STORAGE_PREFIX + key);
|
|
656
|
+
}
|
|
657
|
+
clear() {
|
|
658
|
+
const toRemove = [];
|
|
659
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
660
|
+
const key = localStorage.key(i);
|
|
661
|
+
if (key?.startsWith(STORAGE_PREFIX)) toRemove.push(key);
|
|
662
|
+
}
|
|
663
|
+
for (const key of toRemove) localStorage.removeItem(key);
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
function readCookies() {
|
|
667
|
+
const raw = document.cookie;
|
|
668
|
+
return raw === "" ? [] : raw.split("; ");
|
|
669
|
+
}
|
|
670
|
+
var CookieAdapter = class {
|
|
671
|
+
get(key) {
|
|
672
|
+
const name = `${STORAGE_PREFIX}${key}=`;
|
|
673
|
+
for (const cookie of readCookies()) {
|
|
674
|
+
if (cookie.startsWith(name)) {
|
|
675
|
+
return decodeURIComponent(cookie.slice(name.length));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
set(key, value, options) {
|
|
681
|
+
const maxAgeSec = Math.floor((options?.ttlMs ?? ONE_YEAR_MS) / 1e3);
|
|
682
|
+
const secure = typeof location !== "undefined" && location.protocol === "https:" ? "; Secure" : "";
|
|
683
|
+
document.cookie = `${STORAGE_PREFIX}${key}=${encodeURIComponent(
|
|
684
|
+
value
|
|
685
|
+
)}; Path=/; Max-Age=${maxAgeSec}; SameSite=Lax${secure}`;
|
|
686
|
+
}
|
|
687
|
+
remove(key) {
|
|
688
|
+
document.cookie = `${STORAGE_PREFIX}${key}=; Path=/; Max-Age=0; SameSite=Lax`;
|
|
689
|
+
}
|
|
690
|
+
clear() {
|
|
691
|
+
for (const cookie of readCookies()) {
|
|
692
|
+
const eq = cookie.indexOf("=");
|
|
693
|
+
const name = eq === -1 ? cookie : cookie.slice(0, eq);
|
|
694
|
+
if (name.startsWith(STORAGE_PREFIX)) {
|
|
695
|
+
this.remove(name.slice(STORAGE_PREFIX.length));
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
var MemoryAdapter = class {
|
|
701
|
+
constructor() {
|
|
702
|
+
this.store = /* @__PURE__ */ new Map();
|
|
703
|
+
}
|
|
704
|
+
get(key) {
|
|
705
|
+
return this.store.get(key) ?? null;
|
|
706
|
+
}
|
|
707
|
+
set(key, value) {
|
|
708
|
+
this.store.set(key, value);
|
|
709
|
+
}
|
|
710
|
+
remove(key) {
|
|
711
|
+
this.store.delete(key);
|
|
712
|
+
}
|
|
713
|
+
clear() {
|
|
714
|
+
this.store.clear();
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
function createStorage(preferred = "localStorage") {
|
|
718
|
+
if (preferred === "localStorage" && hasLocalStorage()) return new LocalStorageAdapter();
|
|
719
|
+
if (preferred === "cookie" && hasDocument()) return new CookieAdapter();
|
|
720
|
+
if (preferred === "memory") return new MemoryAdapter();
|
|
721
|
+
if (hasLocalStorage()) return new LocalStorageAdapter();
|
|
722
|
+
if (hasDocument()) return new CookieAdapter();
|
|
723
|
+
return new MemoryAdapter();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// src/core/transport.ts
|
|
727
|
+
var TransportError = class extends Error {
|
|
728
|
+
constructor(message, statusCode) {
|
|
729
|
+
super(message);
|
|
730
|
+
this.statusCode = statusCode;
|
|
731
|
+
this.name = "TransportError";
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
var Transport = class {
|
|
735
|
+
constructor(config) {
|
|
736
|
+
this.config = config;
|
|
737
|
+
}
|
|
738
|
+
/** Envío normal con `fetch`. Lanza `TransportError` si la respuesta no es OK. */
|
|
739
|
+
async send(path, body) {
|
|
740
|
+
const url = `${this.config.apiUrl}${path}`;
|
|
741
|
+
const controller = new AbortController();
|
|
742
|
+
const timeoutMs = this.config.timeoutMs ?? 1e4;
|
|
743
|
+
const timeoutId = setTimeout(
|
|
744
|
+
() => controller.abort(new DOMException(`Veo request timeout (${timeoutMs}ms)`, "TimeoutError")),
|
|
745
|
+
timeoutMs
|
|
746
|
+
);
|
|
747
|
+
try {
|
|
748
|
+
const response = await fetch(url, {
|
|
749
|
+
method: "POST",
|
|
750
|
+
headers: {
|
|
751
|
+
"Content-Type": "application/json",
|
|
752
|
+
"X-Api-Key": this.config.apiKey,
|
|
753
|
+
"X-Sdk-Version": this.config.sdkVersion
|
|
754
|
+
},
|
|
755
|
+
body: JSON.stringify(body),
|
|
756
|
+
// NO usar `keepalive: true` en los flushes normales: tiene límites
|
|
757
|
+
// estrictos del navegador (cuota de requests/64KB) y produce abortos
|
|
758
|
+
// espurios cuando hay varios envíos seguidos. El envío fiable al cerrar
|
|
759
|
+
// la página ya lo cubre `sendBeacon()` (ver Queue.flushBeacon).
|
|
760
|
+
signal: controller.signal
|
|
761
|
+
});
|
|
762
|
+
if (!response.ok) {
|
|
763
|
+
throw new TransportError(`HTTP ${response.status}`, response.status);
|
|
764
|
+
}
|
|
765
|
+
return await response.json();
|
|
766
|
+
} finally {
|
|
767
|
+
clearTimeout(timeoutId);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Envío con `navigator.sendBeacon`. Fire-and-forget, pensado para `pagehide`.
|
|
772
|
+
* Como sendBeacon no admite headers, la API key viaja por query string.
|
|
773
|
+
*/
|
|
774
|
+
sendBeacon(path, body) {
|
|
775
|
+
if (!hasNavigatorSendBeacon()) return false;
|
|
776
|
+
const url = new URL(`${this.config.apiUrl}${path}`);
|
|
777
|
+
url.searchParams.set("key", this.config.apiKey);
|
|
778
|
+
url.searchParams.set("v", this.config.sdkVersion);
|
|
779
|
+
const blob = new Blob([JSON.stringify(body)], { type: "application/json" });
|
|
780
|
+
return navigator.sendBeacon(url.toString(), blob);
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// src/core/client.ts
|
|
785
|
+
var ORGANIZATION_ID_KEY = "organization_id";
|
|
786
|
+
var END_USER_ID_KEY2 = "end_user_id";
|
|
787
|
+
var IDENTIFY_SIG_KEY = "identify_signature";
|
|
788
|
+
function stableStringify(value) {
|
|
789
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
790
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
791
|
+
const entries = Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`);
|
|
792
|
+
return `{${entries.join(",")}}`;
|
|
793
|
+
}
|
|
794
|
+
var Client = class {
|
|
795
|
+
constructor(config) {
|
|
796
|
+
this.config = config;
|
|
797
|
+
this.plugins = [];
|
|
798
|
+
this.currentOrganizationId = null;
|
|
799
|
+
this.listeners = {
|
|
800
|
+
pageview: /* @__PURE__ */ new Set(),
|
|
801
|
+
identify: /* @__PURE__ */ new Set()
|
|
802
|
+
};
|
|
803
|
+
if (!config.apiKey) throw new Error("apiKey is required");
|
|
804
|
+
this.storage = createStorage(config.storage ?? "localStorage");
|
|
805
|
+
this.session = new Session(this.storage);
|
|
806
|
+
this.transport = new Transport({
|
|
807
|
+
apiKey: config.apiKey,
|
|
808
|
+
apiUrl: resolveApiUrl(config).replace(/\/$/, ""),
|
|
809
|
+
sdkVersion: SDK_VERSION
|
|
810
|
+
});
|
|
811
|
+
this.queue = new Queue({
|
|
812
|
+
transport: this.transport,
|
|
813
|
+
flushIntervalMs: config.flushInterval ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
814
|
+
batchSize: config.flushBatchSize ?? DEFAULT_FLUSH_BATCH_SIZE,
|
|
815
|
+
onError: (err) => {
|
|
816
|
+
if (config.debug) console.error("[veo] flush error:", err);
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
const persistedUserId = this.storage.get(END_USER_ID_KEY2);
|
|
820
|
+
if (persistedUserId) this.session.setEndUserId(persistedUserId);
|
|
821
|
+
const persistedOrgId = this.storage.get(ORGANIZATION_ID_KEY);
|
|
822
|
+
if (persistedOrgId) this.currentOrganizationId = persistedOrgId;
|
|
823
|
+
this.setupUnloadHandlers();
|
|
824
|
+
if (config.debug) {
|
|
825
|
+
console.log("[veo] initialized", {
|
|
826
|
+
anonymousId: this.session.anonymousId,
|
|
827
|
+
sessionId: this.session.sessionId,
|
|
828
|
+
endUserId: this.session.endUserId
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
identify(payload) {
|
|
833
|
+
const { visitor, organization } = payload;
|
|
834
|
+
const signature = stableStringify({
|
|
835
|
+
id: visitor.id,
|
|
836
|
+
traits: visitor.traits ?? {},
|
|
837
|
+
org: organization ? { id: organization.id, attributes: organization.attributes ?? {} } : null
|
|
838
|
+
});
|
|
839
|
+
if (this.session.endUserId === visitor.id && this.storage.get(IDENTIFY_SIG_KEY) === signature) {
|
|
840
|
+
if (this.config.debug) console.debug("[veo] identify: sin cambios, omitido");
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const previousAnonymousId = this.session.endUserId === null ? this.session.anonymousId : void 0;
|
|
844
|
+
this.session.setEndUserId(visitor.id);
|
|
845
|
+
if (organization) {
|
|
846
|
+
this.currentOrganizationId = organization.id;
|
|
847
|
+
this.storage.set(ORGANIZATION_ID_KEY, organization.id);
|
|
848
|
+
}
|
|
849
|
+
const endUser = {
|
|
850
|
+
id: visitor.id,
|
|
851
|
+
traits: visitor.traits ?? {},
|
|
852
|
+
...previousAnonymousId !== void 0 ? { anonymousId: previousAnonymousId } : {}
|
|
853
|
+
};
|
|
854
|
+
const body = {
|
|
855
|
+
endUser,
|
|
856
|
+
...organization ? { organization: { id: organization.id, attributes: organization.attributes ?? {} } } : {}
|
|
857
|
+
};
|
|
858
|
+
void this.transport.send("/v1/identify", body).catch((err) => {
|
|
859
|
+
if (this.config.debug) console.error("[veo] identify failed:", err);
|
|
860
|
+
});
|
|
861
|
+
this.enqueue({
|
|
862
|
+
actionType: "identify",
|
|
863
|
+
actionProperties: {
|
|
864
|
+
traits: visitor.traits,
|
|
865
|
+
organizationAttributes: organization?.attributes
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
this.storage.set(IDENTIFY_SIG_KEY, signature);
|
|
869
|
+
this.emit("identify", { endUserId: visitor.id, sessionId: this.session.sessionId });
|
|
870
|
+
}
|
|
871
|
+
track(name, properties) {
|
|
872
|
+
if (!name || typeof name !== "string") {
|
|
873
|
+
if (this.config.debug) console.error("[veo] track: name is required");
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
this.enqueue({
|
|
877
|
+
actionType: "track",
|
|
878
|
+
actionName: name,
|
|
879
|
+
...properties !== void 0 ? { actionProperties: properties } : {}
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
pageview(path) {
|
|
883
|
+
if (!hasDocument()) return;
|
|
884
|
+
const url = window.location.href;
|
|
885
|
+
const pagePath = path ?? window.location.pathname;
|
|
886
|
+
this.enqueue({
|
|
887
|
+
actionType: "pageview",
|
|
888
|
+
pageUrl: url,
|
|
889
|
+
pagePath,
|
|
890
|
+
pageTitle: document.title,
|
|
891
|
+
pageReferrer: document.referrer
|
|
892
|
+
});
|
|
893
|
+
this.emit("pageview", {
|
|
894
|
+
url,
|
|
895
|
+
path: pagePath,
|
|
896
|
+
sessionId: this.session.sessionId,
|
|
897
|
+
endUserId: this.session.endUserId
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
reset() {
|
|
901
|
+
this.session.reset();
|
|
902
|
+
this.currentOrganizationId = null;
|
|
903
|
+
this.storage.remove(ORGANIZATION_ID_KEY);
|
|
904
|
+
this.storage.remove(END_USER_ID_KEY2);
|
|
905
|
+
this.storage.remove(IDENTIFY_SIG_KEY);
|
|
766
906
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
// src/plugins/autocapture/dom-listener.ts
|
|
770
|
-
var DomListener = class {
|
|
771
|
-
constructor(client, config) {
|
|
772
|
-
this.client = client;
|
|
773
|
-
this.cleanups = [];
|
|
774
|
-
const d = DEFAULT_AUTOCAPTURE_CONFIG;
|
|
775
|
-
this.config = {
|
|
776
|
-
enabled: config.enabled ?? d.enabled,
|
|
777
|
-
captureClicks: config.captureClicks ?? d.captureClicks,
|
|
778
|
-
captureSubmits: config.captureSubmits ?? d.captureSubmits,
|
|
779
|
-
captureChanges: config.captureChanges ?? d.captureChanges,
|
|
780
|
-
maskAllInputs: config.maskAllInputs ?? d.maskAllInputs,
|
|
781
|
-
maskAllText: config.maskAllText ?? d.maskAllText,
|
|
782
|
-
blockSelectors: config.blockSelectors ?? [],
|
|
783
|
-
throttleMs: config.throttleMs ?? d.throttleMs,
|
|
784
|
-
maxTextLength: config.maxTextLength ?? d.maxTextLength,
|
|
785
|
-
captureElementMetadata: config.captureElementMetadata ?? d.captureElementMetadata,
|
|
786
|
-
captureAncestors: Math.min(config.captureAncestors ?? d.captureAncestors, MAX_ANCESTORS)
|
|
787
|
-
};
|
|
788
|
-
this.throttle = new ElementThrottle(this.config.throttleMs);
|
|
907
|
+
flush() {
|
|
908
|
+
return this.queue.flush();
|
|
789
909
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
if (this.config.captureChanges) this.attach("change", (e) => this.handleChange(e), true);
|
|
910
|
+
use(plugin) {
|
|
911
|
+
this.plugins.push(plugin);
|
|
912
|
+
plugin.install(this);
|
|
913
|
+
return this;
|
|
795
914
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
this.cleanups = [];
|
|
915
|
+
getEndUserId() {
|
|
916
|
+
return this.session.endUserId;
|
|
799
917
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
if (!e.isTrusted) return;
|
|
803
|
-
try {
|
|
804
|
-
handler(e);
|
|
805
|
-
} catch {
|
|
806
|
-
}
|
|
807
|
-
};
|
|
808
|
-
document.addEventListener(eventName, listener, useCapture);
|
|
809
|
-
this.cleanups.push(() => document.removeEventListener(eventName, listener, useCapture));
|
|
918
|
+
getAnonymousId() {
|
|
919
|
+
return this.session.anonymousId;
|
|
810
920
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
if (!(target instanceof Element)) return;
|
|
814
|
-
const meaningful = this.findMeaningfulAncestor(target);
|
|
815
|
-
if (!meaningful) return;
|
|
816
|
-
if (isElementBlocked(meaningful, this.config.blockSelectors)) return;
|
|
817
|
-
if (!this.throttle.shouldFire(meaningful)) return;
|
|
818
|
-
this.emit("click", meaningful);
|
|
921
|
+
getSessionId() {
|
|
922
|
+
return this.session.sessionId;
|
|
819
923
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
924
|
+
/**
|
|
925
|
+
* Suscribe `listener` a `event`. Devuelve una función para desuscribir.
|
|
926
|
+
* Los errores en listeners se logean en debug mode pero NO se propagan
|
|
927
|
+
* (un plugin no debe poder romper a otro).
|
|
928
|
+
*/
|
|
929
|
+
on(event, listener) {
|
|
930
|
+
this.listeners[event].add(listener);
|
|
931
|
+
return () => {
|
|
932
|
+
this.listeners[event].delete(listener);
|
|
933
|
+
};
|
|
826
934
|
}
|
|
827
|
-
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
935
|
+
emit(event, payload) {
|
|
936
|
+
for (const listener of this.listeners[event]) {
|
|
937
|
+
try {
|
|
938
|
+
listener(payload);
|
|
939
|
+
} catch (err) {
|
|
940
|
+
if (this.config.debug) console.error(`[veo] listener for '${event}' failed:`, err);
|
|
941
|
+
}
|
|
834
942
|
}
|
|
835
|
-
if (isElementBlocked(target, this.config.blockSelectors)) return;
|
|
836
|
-
if (!this.throttle.shouldFire(target)) return;
|
|
837
|
-
this.emit("change", target);
|
|
838
943
|
}
|
|
839
|
-
/**
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
944
|
+
/**
|
|
945
|
+
* Construye el evento completo y lo agrega a la queue. Omite los campos
|
|
946
|
+
* opcionales no definidos (exigido por `exactOptionalPropertyTypes`).
|
|
947
|
+
* @internal
|
|
948
|
+
*/
|
|
949
|
+
enqueue(input) {
|
|
950
|
+
this.session.touch();
|
|
951
|
+
const organizationId = this.currentOrganizationId ?? void 0;
|
|
952
|
+
const pageUrl = input.pageUrl ?? (hasWindow() ? window.location.href : void 0);
|
|
953
|
+
const pagePath = input.pagePath ?? (hasWindow() ? window.location.pathname : void 0);
|
|
954
|
+
const pageTitle = input.pageTitle ?? (hasDocument() ? document.title : void 0);
|
|
955
|
+
const pageReferrer = input.pageReferrer ?? (hasDocument() ? document.referrer : void 0);
|
|
956
|
+
const event = {
|
|
957
|
+
actionId: uuidv7(),
|
|
958
|
+
endUserId: this.session.effectiveUserId,
|
|
959
|
+
sessionId: this.session.sessionId,
|
|
960
|
+
actionType: input.actionType,
|
|
961
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
962
|
+
...organizationId !== void 0 ? { organizationId } : {},
|
|
963
|
+
...input.actionName !== void 0 ? { actionName: input.actionName } : {},
|
|
964
|
+
...pageUrl !== void 0 ? { pageUrl } : {},
|
|
965
|
+
...pagePath !== void 0 ? { pagePath } : {},
|
|
966
|
+
...pageTitle !== void 0 ? { pageTitle } : {},
|
|
967
|
+
...pageReferrer !== void 0 ? { pageReferrer } : {},
|
|
968
|
+
...input.actionProperties !== void 0 ? { actionProperties: input.actionProperties } : {}
|
|
969
|
+
};
|
|
970
|
+
this.queue.add(event);
|
|
851
971
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
972
|
+
setupUnloadHandlers() {
|
|
973
|
+
if (!hasWindow()) return;
|
|
974
|
+
window.addEventListener("pagehide", () => this.queue.flushBeacon());
|
|
975
|
+
window.addEventListener("visibilitychange", () => {
|
|
976
|
+
if (document.visibilityState === "hidden") {
|
|
977
|
+
this.queue.flushBeacon();
|
|
978
|
+
}
|
|
979
|
+
});
|
|
855
980
|
}
|
|
856
981
|
};
|
|
857
982
|
|
|
858
|
-
// src/
|
|
859
|
-
function
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
983
|
+
// src/core/setup.ts
|
|
984
|
+
function setupClient(config, onGuides) {
|
|
985
|
+
const client = new Client(config);
|
|
986
|
+
client.use(spaRouterPlugin());
|
|
987
|
+
if (config.autocapture !== false) {
|
|
988
|
+
const autocaptureConfig = typeof config.autocapture === "object" ? config.autocapture : {};
|
|
989
|
+
client.use(autocapturePlugin(autocaptureConfig));
|
|
990
|
+
}
|
|
991
|
+
onGuides(client);
|
|
992
|
+
if (typeof window !== "undefined") {
|
|
993
|
+
const stub = window.veo;
|
|
994
|
+
const pending = stub?._q ?? [];
|
|
995
|
+
window.veo = client;
|
|
996
|
+
for (const entry of pending) {
|
|
997
|
+
const [method, args] = entry;
|
|
998
|
+
if (method === "init") continue;
|
|
999
|
+
const fn = client[method];
|
|
1000
|
+
if (typeof fn === "function") {
|
|
1001
|
+
try {
|
|
1002
|
+
fn.apply(client, Array.from(args));
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
if (config.debug) console.error("[veo] drain error:", err);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
870
1007
|
}
|
|
871
|
-
|
|
1008
|
+
maybeLoadBuilder({
|
|
1009
|
+
...config.builderUrl !== void 0 ? { builderUrl: config.builderUrl } : {},
|
|
1010
|
+
...config.debug !== void 0 ? { debug: config.debug } : {},
|
|
1011
|
+
...config.apiKey !== void 0 ? { apiKey: config.apiKey } : {},
|
|
1012
|
+
apiUrl: resolveApiUrl(config)
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
return client;
|
|
872
1016
|
}
|
|
873
1017
|
|
|
874
1018
|
// src/plugins/guides/constants.ts
|
|
@@ -905,15 +1049,20 @@ var GuideFrequencyCache = class {
|
|
|
905
1049
|
* Decide si una guía debe filtrarse localmente antes de pintarla.
|
|
906
1050
|
*
|
|
907
1051
|
* - `every_visit` / `always` → nunca filtra (siempre re-pinta).
|
|
908
|
-
* - `once` → cualquier entry registrada filtra.
|
|
909
|
-
* - `until_dismissed` →
|
|
1052
|
+
* - `once` → cualquier entry registrada filtra (verla ya cuenta).
|
|
1053
|
+
* - `until_dismissed` → si fue descartada O COMPLETADA (responder un
|
|
1054
|
+
* form / terminar un walkthrough es más fuerte que descartar — sin
|
|
1055
|
+
* esto, una encuesta seguiría preguntando después de respondida).
|
|
910
1056
|
*/
|
|
911
1057
|
shouldFilter(guideId, displayFrequency) {
|
|
912
1058
|
if (displayFrequency === "every_visit" || displayFrequency === "always") return false;
|
|
913
1059
|
const entry = this.cache.get(guideId);
|
|
914
1060
|
if (!entry) return false;
|
|
915
1061
|
if (displayFrequency === "once") return true;
|
|
916
|
-
if (displayFrequency === "until_dismissed")
|
|
1062
|
+
if (displayFrequency === "until_dismissed") {
|
|
1063
|
+
return entry.status === "dismissed" || entry.status === "completed";
|
|
1064
|
+
}
|
|
1065
|
+
if (displayFrequency === "until_answered") return entry.status === "completed";
|
|
917
1066
|
return false;
|
|
918
1067
|
}
|
|
919
1068
|
/**
|
|
@@ -1303,6 +1452,88 @@ function readCtaUrl(step) {
|
|
|
1303
1452
|
return isSafeUrl(candidate) ? candidate : void 0;
|
|
1304
1453
|
}
|
|
1305
1454
|
|
|
1455
|
+
// src/plugins/guides/guide-design.ts
|
|
1456
|
+
var DARK_THEME = {
|
|
1457
|
+
"--veo-bg": "#1f2937",
|
|
1458
|
+
"--veo-text": "#f9fafb",
|
|
1459
|
+
"--veo-text-secondary": "#d1d5db",
|
|
1460
|
+
"--veo-shadow": "0 20px 60px rgba(0, 0, 0, 0.55)"
|
|
1461
|
+
};
|
|
1462
|
+
var ALIGN_JUSTIFY = {
|
|
1463
|
+
left: "flex-start",
|
|
1464
|
+
center: "center",
|
|
1465
|
+
right: "flex-end"
|
|
1466
|
+
};
|
|
1467
|
+
var SHADOW = {
|
|
1468
|
+
none: "none",
|
|
1469
|
+
soft: "0 4px 14px rgba(0, 0, 0, 0.10)",
|
|
1470
|
+
medium: "0 8px 30px rgba(0, 0, 0, 0.18)",
|
|
1471
|
+
strong: "0 24px 60px rgba(0, 0, 0, 0.32)"
|
|
1472
|
+
};
|
|
1473
|
+
var PRESET_POS = {
|
|
1474
|
+
center: [0.5, 0.5],
|
|
1475
|
+
top: [0.5, 0],
|
|
1476
|
+
bottom: [0.5, 1],
|
|
1477
|
+
left: [0, 0.5],
|
|
1478
|
+
right: [1, 0.5],
|
|
1479
|
+
"top-left": [0, 0],
|
|
1480
|
+
"top-right": [1, 0],
|
|
1481
|
+
"bottom-left": [0, 1],
|
|
1482
|
+
"bottom-right": [1, 1]
|
|
1483
|
+
};
|
|
1484
|
+
var COLOR_RE = /^#[0-9a-f]{3,8}$|^(rgb|rgba|hsl|hsla)\([\d.,%/\sdeg]+\)$|^[a-z]{3,20}$/i;
|
|
1485
|
+
function clamp(n, min, max) {
|
|
1486
|
+
return Math.min(max, Math.max(min, n));
|
|
1487
|
+
}
|
|
1488
|
+
function applyDesignVars(host, style) {
|
|
1489
|
+
if (!style || typeof style !== "object") return;
|
|
1490
|
+
const s = style;
|
|
1491
|
+
if (typeof s.accentColor === "string" && COLOR_RE.test(s.accentColor.trim())) {
|
|
1492
|
+
host.style.setProperty("--veo-primary", s.accentColor.trim());
|
|
1493
|
+
}
|
|
1494
|
+
if (s.theme === "dark") {
|
|
1495
|
+
for (const [key, value] of Object.entries(DARK_THEME)) {
|
|
1496
|
+
host.style.setProperty(key, value);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
if (typeof s.radius === "number" && Number.isFinite(s.radius)) {
|
|
1500
|
+
host.style.setProperty("--veo-radius", `${clamp(s.radius, 0, 48)}px`);
|
|
1501
|
+
}
|
|
1502
|
+
if (typeof s.width === "number" && Number.isFinite(s.width)) {
|
|
1503
|
+
host.style.setProperty("--veo-width", `${clamp(s.width, 220, 720)}px`);
|
|
1504
|
+
}
|
|
1505
|
+
if (s.align === "left" || s.align === "center" || s.align === "right") {
|
|
1506
|
+
host.style.setProperty("--veo-actions-justify", ALIGN_JUSTIFY[s.align]);
|
|
1507
|
+
}
|
|
1508
|
+
if (typeof s.padding === "number" && Number.isFinite(s.padding)) {
|
|
1509
|
+
host.style.setProperty("--veo-pad", `${clamp(s.padding, 0, 64)}px`);
|
|
1510
|
+
}
|
|
1511
|
+
if (typeof s.margin === "number" && Number.isFinite(s.margin)) {
|
|
1512
|
+
host.style.setProperty("--veo-margin", `${clamp(s.margin, 0, 64)}px`);
|
|
1513
|
+
}
|
|
1514
|
+
if (typeof s.borderWidth === "number" && Number.isFinite(s.borderWidth)) {
|
|
1515
|
+
host.style.setProperty("--veo-border-width", `${clamp(s.borderWidth, 0, 16)}px`);
|
|
1516
|
+
}
|
|
1517
|
+
if (typeof s.borderColor === "string" && COLOR_RE.test(s.borderColor.trim())) {
|
|
1518
|
+
host.style.setProperty("--veo-border-color", s.borderColor.trim());
|
|
1519
|
+
}
|
|
1520
|
+
if (typeof s.shadow === "string" && SHADOW[s.shadow]) {
|
|
1521
|
+
host.style.setProperty("--veo-shadow", SHADOW[s.shadow]);
|
|
1522
|
+
}
|
|
1523
|
+
let px;
|
|
1524
|
+
let py;
|
|
1525
|
+
if (typeof s.posX === "number" && typeof s.posY === "number") {
|
|
1526
|
+
px = clamp(s.posX, 0, 1);
|
|
1527
|
+
py = clamp(s.posY, 0, 1);
|
|
1528
|
+
} else if (typeof s.overlayPosition === "string" && PRESET_POS[s.overlayPosition]) {
|
|
1529
|
+
[px, py] = PRESET_POS[s.overlayPosition];
|
|
1530
|
+
}
|
|
1531
|
+
if (px !== void 0 && py !== void 0) {
|
|
1532
|
+
host.style.setProperty("--veo-pos-x", `${px * 100}%`);
|
|
1533
|
+
host.style.setProperty("--veo-pos-y", `${py * 100}%`);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1306
1537
|
// src/plugins/guides/styles.ts
|
|
1307
1538
|
var GUIDE_STYLES = `
|
|
1308
1539
|
:host {
|
|
@@ -1312,6 +1543,8 @@ var GUIDE_STYLES = `
|
|
|
1312
1543
|
--veo-bg: #ffffff;
|
|
1313
1544
|
--veo-radius: 12px;
|
|
1314
1545
|
--veo-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
|
|
1546
|
+
--veo-width: 420px;
|
|
1547
|
+
--veo-actions-justify: flex-end;
|
|
1315
1548
|
all: initial;
|
|
1316
1549
|
}
|
|
1317
1550
|
* { box-sizing: border-box; font-family: -apple-system, system-ui, "Segoe UI", Roboto, sans-serif; }
|
|
@@ -1319,26 +1552,36 @@ var GUIDE_STYLES = `
|
|
|
1319
1552
|
.veo-modal-overlay {
|
|
1320
1553
|
position: fixed; inset: 0;
|
|
1321
1554
|
background: rgba(15, 15, 30, 0.45);
|
|
1322
|
-
display: flex; align-items: center; justify-content: center;
|
|
1323
1555
|
z-index: ${GUIDE_Z_INDEX};
|
|
1324
1556
|
animation: veo-fade-in 180ms ease-out;
|
|
1325
1557
|
}
|
|
1558
|
+
/*
|
|
1559
|
+
* Posici\xF3n libre/preset: --veo-pos-x/y son porcentajes (default 50% = centro).
|
|
1560
|
+
* El truco translate(-pos) alinea la MISMA fracci\xF3n de la tarjeta con esa
|
|
1561
|
+
* fracci\xF3n del overlay, as\xED la tarjeta nunca se sale (0% = pegada izquierda,
|
|
1562
|
+
* 100% = pegada derecha, 50% = centrada) sea cual sea su ancho.
|
|
1563
|
+
*/
|
|
1326
1564
|
.veo-modal-card {
|
|
1565
|
+
position: absolute;
|
|
1566
|
+
left: var(--veo-pos-x, 50%);
|
|
1567
|
+
top: var(--veo-pos-y, 50%);
|
|
1568
|
+
transform: translate(calc(var(--veo-pos-x, 50%) * -1), calc(var(--veo-pos-y, 50%) * -1));
|
|
1327
1569
|
background: var(--veo-bg);
|
|
1328
1570
|
border-radius: var(--veo-radius);
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1571
|
+
border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
|
|
1572
|
+
padding: var(--veo-pad, 24px);
|
|
1573
|
+
max-width: var(--veo-width); width: 90%;
|
|
1332
1574
|
box-shadow: var(--veo-shadow);
|
|
1333
|
-
animation: veo-
|
|
1575
|
+
animation: veo-fade-in 180ms ease-out;
|
|
1334
1576
|
}
|
|
1335
1577
|
|
|
1336
1578
|
.veo-banner {
|
|
1337
1579
|
position: fixed; left: 0; right: 0;
|
|
1338
1580
|
background: var(--veo-bg);
|
|
1339
|
-
|
|
1581
|
+
border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
|
|
1582
|
+
padding: var(--veo-pad, 16px);
|
|
1340
1583
|
z-index: ${GUIDE_Z_INDEX};
|
|
1341
|
-
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
|
1584
|
+
box-shadow: var(--veo-shadow, 0 2px 12px rgba(0, 0, 0, 0.12));
|
|
1342
1585
|
animation: veo-slide-down 200ms ease-out;
|
|
1343
1586
|
}
|
|
1344
1587
|
.veo-banner-top { top: 0; }
|
|
@@ -1348,13 +1591,26 @@ var GUIDE_STYLES = `
|
|
|
1348
1591
|
position: absolute;
|
|
1349
1592
|
background: var(--veo-bg);
|
|
1350
1593
|
border-radius: var(--veo-radius);
|
|
1351
|
-
|
|
1594
|
+
border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
|
|
1595
|
+
padding: var(--veo-pad, 15px);
|
|
1352
1596
|
max-width: 300px;
|
|
1353
1597
|
z-index: ${GUIDE_Z_INDEX};
|
|
1354
|
-
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
|
|
1598
|
+
box-shadow: var(--veo-shadow, 0 8px 30px rgba(0, 0, 0, 0.18));
|
|
1355
1599
|
animation: veo-fade-in 150ms ease-out;
|
|
1356
1600
|
}
|
|
1357
1601
|
|
|
1602
|
+
.veo-inline {
|
|
1603
|
+
background: var(--veo-bg);
|
|
1604
|
+
border-radius: var(--veo-radius);
|
|
1605
|
+
border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
|
|
1606
|
+
padding: var(--veo-pad, 20px);
|
|
1607
|
+
max-width: var(--veo-width);
|
|
1608
|
+
margin: var(--veo-margin, 12px) 0;
|
|
1609
|
+
position: relative;
|
|
1610
|
+
box-shadow: var(--veo-shadow);
|
|
1611
|
+
animation: veo-fade-in 160ms ease-out;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1358
1614
|
.veo-guide-content {
|
|
1359
1615
|
position: relative;
|
|
1360
1616
|
display: flex; flex-direction: column;
|
|
@@ -1373,7 +1629,7 @@ var GUIDE_STYLES = `
|
|
|
1373
1629
|
margin: 0 0 16px; color: var(--veo-text-secondary);
|
|
1374
1630
|
}
|
|
1375
1631
|
.veo-guide-actions {
|
|
1376
|
-
display: flex; gap: 8px; justify-content:
|
|
1632
|
+
display: flex; gap: 8px; justify-content: var(--veo-actions-justify);
|
|
1377
1633
|
}
|
|
1378
1634
|
.veo-guide-cta {
|
|
1379
1635
|
background: var(--veo-primary); color: #fff; border: none;
|
|
@@ -1392,16 +1648,66 @@ var GUIDE_STYLES = `
|
|
|
1392
1648
|
}
|
|
1393
1649
|
.veo-guide-close:hover { color: var(--veo-text); }
|
|
1394
1650
|
|
|
1651
|
+
.veo-form { display: flex; flex-direction: column; gap: 14px; margin-bottom: 16px; }
|
|
1652
|
+
.veo-form-field { display: flex; flex-direction: column; gap: 6px; }
|
|
1653
|
+
.veo-form-label { font-size: 13px; font-weight: 500; color: var(--veo-text); }
|
|
1654
|
+
.veo-form-input {
|
|
1655
|
+
font-size: 14px; color: var(--veo-text);
|
|
1656
|
+
background: var(--veo-bg);
|
|
1657
|
+
border: 1px solid #d1d5db; border-radius: 8px;
|
|
1658
|
+
padding: 8px 10px; width: 100%;
|
|
1659
|
+
}
|
|
1660
|
+
.veo-form-input:focus { outline: 2px solid var(--veo-primary); outline-offset: -1px; border-color: transparent; }
|
|
1661
|
+
.veo-form-options { display: flex; flex-direction: column; gap: 6px; }
|
|
1662
|
+
.veo-form-option {
|
|
1663
|
+
display: flex; align-items: center; gap: 8px;
|
|
1664
|
+
font-size: 14px; color: var(--veo-text); cursor: pointer;
|
|
1665
|
+
}
|
|
1666
|
+
.veo-form-option input { accent-color: var(--veo-primary); margin: 0; cursor: pointer; }
|
|
1667
|
+
.veo-form-scale { display: flex; gap: 4px; flex-wrap: wrap; }
|
|
1668
|
+
.veo-nps-btn {
|
|
1669
|
+
min-width: 30px; height: 30px; padding: 0 4px;
|
|
1670
|
+
font-size: 13px; font-weight: 500; color: var(--veo-text);
|
|
1671
|
+
background: var(--veo-bg); border: 1px solid #d1d5db; border-radius: 6px;
|
|
1672
|
+
cursor: pointer; transition: all 100ms ease;
|
|
1673
|
+
}
|
|
1674
|
+
.veo-nps-btn:hover { border-color: var(--veo-primary); }
|
|
1675
|
+
.veo-rating-btn {
|
|
1676
|
+
background: none; border: none; padding: 0 2px;
|
|
1677
|
+
font-size: 26px; line-height: 1; color: #d1d5db;
|
|
1678
|
+
cursor: pointer; transition: color 100ms ease;
|
|
1679
|
+
}
|
|
1680
|
+
.veo-scale-active.veo-nps-btn { background: var(--veo-primary); color: #fff; border-color: var(--veo-primary); }
|
|
1681
|
+
.veo-scale-active.veo-rating-btn { color: #f59e0b; }
|
|
1682
|
+
.veo-form-error { font-size: 13px; color: #dc2626; margin: 0; }
|
|
1683
|
+
.veo-form-thanks {
|
|
1684
|
+
text-align: center; padding: 24px 8px;
|
|
1685
|
+
font-size: 16px; font-weight: 600; color: var(--veo-text);
|
|
1686
|
+
}
|
|
1687
|
+
.veo-form-preview-note {
|
|
1688
|
+
text-align: center; margin: 0 0 12px;
|
|
1689
|
+
font-size: 12px; color: #b45309;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1395
1692
|
.veo-walkthrough {
|
|
1396
1693
|
position: absolute;
|
|
1397
1694
|
background: var(--veo-bg);
|
|
1398
1695
|
border-radius: var(--veo-radius);
|
|
1399
|
-
|
|
1696
|
+
border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
|
|
1697
|
+
padding: var(--veo-pad, 16px);
|
|
1400
1698
|
max-width: 340px;
|
|
1401
1699
|
z-index: ${GUIDE_Z_INDEX};
|
|
1402
|
-
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
|
|
1700
|
+
box-shadow: var(--veo-shadow, 0 8px 30px rgba(0, 0, 0, 0.2));
|
|
1403
1701
|
animation: veo-fade-in 150ms ease-out;
|
|
1404
1702
|
}
|
|
1703
|
+
.veo-walkthrough-arrow {
|
|
1704
|
+
position: absolute;
|
|
1705
|
+
width: 12px;
|
|
1706
|
+
height: 12px;
|
|
1707
|
+
background: var(--veo-bg);
|
|
1708
|
+
transform: rotate(45deg);
|
|
1709
|
+
pointer-events: none;
|
|
1710
|
+
}
|
|
1405
1711
|
.veo-walkthrough-counter {
|
|
1406
1712
|
font-size: 11px; color: #9ca3af;
|
|
1407
1713
|
margin-bottom: 8px; letter-spacing: 0.02em;
|
|
@@ -1455,6 +1761,10 @@ var GUIDE_STYLES = `
|
|
|
1455
1761
|
max-width: calc(100vw - 16px);
|
|
1456
1762
|
animation: veo-fade-in 160ms ease;
|
|
1457
1763
|
}
|
|
1764
|
+
.veo-custom-inline {
|
|
1765
|
+
display: block;
|
|
1766
|
+
margin: 12px 0;
|
|
1767
|
+
}
|
|
1458
1768
|
.veo-custom-frame {
|
|
1459
1769
|
display: block;
|
|
1460
1770
|
border: 0;
|
|
@@ -1500,6 +1810,14 @@ var BaseRenderer = class {
|
|
|
1500
1810
|
this.shadow = shadow;
|
|
1501
1811
|
return { host, shadow, root };
|
|
1502
1812
|
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Aplica la tematización por datos del step (`step.style`) como variables
|
|
1815
|
+
* CSS sobre el host. Llamar tras `createHost()`. No-op si no hay host o
|
|
1816
|
+
* style. Ver `guide-design.ts`.
|
|
1817
|
+
*/
|
|
1818
|
+
applyDesign(style) {
|
|
1819
|
+
if (this.host) applyDesignVars(this.host, style);
|
|
1820
|
+
}
|
|
1503
1821
|
/**
|
|
1504
1822
|
* Registra una función a ejecutar en `destroy()`. Útil para limpiar
|
|
1505
1823
|
* listeners globales (scroll, resize) o intervalos.
|
|
@@ -1530,6 +1848,7 @@ var BannerRenderer = class extends BaseRenderer {
|
|
|
1530
1848
|
const step = context.guide.guideSteps[0];
|
|
1531
1849
|
if (!step) return;
|
|
1532
1850
|
const { root } = this.createHost();
|
|
1851
|
+
this.applyDesign(step.style);
|
|
1533
1852
|
const ownerDocument = root.ownerDocument ?? document;
|
|
1534
1853
|
const position = step.style?.position === "bottom" ? "bottom" : "top";
|
|
1535
1854
|
const banner = ownerDocument.createElement("div");
|
|
@@ -1591,6 +1910,77 @@ function waitForElement(selector, timeoutMs = DEFAULT_ANCHOR_WAIT_MS) {
|
|
|
1591
1910
|
});
|
|
1592
1911
|
}
|
|
1593
1912
|
|
|
1913
|
+
// src/plugins/guides/renderers/custom-frame.ts
|
|
1914
|
+
function mountCustomFrame(doc, step, context, opts) {
|
|
1915
|
+
const iframe = doc.createElement("iframe");
|
|
1916
|
+
iframe.className = "veo-custom-frame";
|
|
1917
|
+
iframe.setAttribute("sandbox", "allow-scripts");
|
|
1918
|
+
iframe.setAttribute("title", context.guide.guideName || "Veo");
|
|
1919
|
+
iframe.style.width = opts.width;
|
|
1920
|
+
const hasFixedHeight = opts.fixedHeight !== void 0 && opts.fixedHeight !== null;
|
|
1921
|
+
if (hasFixedHeight) iframe.style.height = toCssSize(opts.fixedHeight);
|
|
1922
|
+
const token = uuidv7();
|
|
1923
|
+
iframe.srcdoc = buildSrcdoc(step.html ?? "", step.css ?? "", step.js ?? "", token);
|
|
1924
|
+
const close = () => context.onClose();
|
|
1925
|
+
const onMessage = (event) => {
|
|
1926
|
+
if (event.source !== iframe.contentWindow) return;
|
|
1927
|
+
const data = event.data;
|
|
1928
|
+
if (!data || typeof data !== "object" || data.__veo !== token) return;
|
|
1929
|
+
switch (data.type) {
|
|
1930
|
+
case "dismiss":
|
|
1931
|
+
context.onInteraction({
|
|
1932
|
+
guideId: context.guide.guideId,
|
|
1933
|
+
stepIndex: 0,
|
|
1934
|
+
action: "dismissed"
|
|
1935
|
+
});
|
|
1936
|
+
close();
|
|
1937
|
+
break;
|
|
1938
|
+
case "cta":
|
|
1939
|
+
context.onInteraction({
|
|
1940
|
+
guideId: context.guide.guideId,
|
|
1941
|
+
stepIndex: 0,
|
|
1942
|
+
action: "cta_clicked"
|
|
1943
|
+
});
|
|
1944
|
+
if (typeof data.url === "string" && isSafeUrl(data.url)) {
|
|
1945
|
+
window.open(data.url, "_blank", "noopener,noreferrer");
|
|
1946
|
+
}
|
|
1947
|
+
if (data.close !== false) close();
|
|
1948
|
+
break;
|
|
1949
|
+
case "navigate":
|
|
1950
|
+
if (typeof data.url === "string" && isSafeUrl(data.url)) {
|
|
1951
|
+
window.location.assign(data.url);
|
|
1952
|
+
}
|
|
1953
|
+
break;
|
|
1954
|
+
case "track":
|
|
1955
|
+
if (typeof data.name === "string" && context.onTrack) {
|
|
1956
|
+
context.onTrack(data.name, isRecord(data.props) ? data.props : void 0);
|
|
1957
|
+
}
|
|
1958
|
+
break;
|
|
1959
|
+
case "resize":
|
|
1960
|
+
if (!hasFixedHeight && typeof data.height === "number" && data.height > 0) {
|
|
1961
|
+
iframe.style.height = `${Math.ceil(data.height)}px`;
|
|
1962
|
+
}
|
|
1963
|
+
break;
|
|
1964
|
+
}
|
|
1965
|
+
};
|
|
1966
|
+
window.addEventListener("message", onMessage);
|
|
1967
|
+
return { iframe, cleanup: () => window.removeEventListener("message", onMessage) };
|
|
1968
|
+
}
|
|
1969
|
+
function toCssSize(value) {
|
|
1970
|
+
return typeof value === "number" ? `${value}px` : value;
|
|
1971
|
+
}
|
|
1972
|
+
function isRecord(value) {
|
|
1973
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1974
|
+
}
|
|
1975
|
+
function buildSrcdoc(html, css, js, token) {
|
|
1976
|
+
const bridge = `(function(){var T=${JSON.stringify(token)};function P(m){m.__veo=T;parent.postMessage(m,'*');}window.veo={dismiss:function(){P({type:'dismiss'});},cta:function(u){P({type:'cta',url:u});},track:function(n,p){P({type:'track',name:n,props:p||{}});},navigate:function(u){P({type:'navigate',url:u});}};document.addEventListener('click',function(e){var a=e.target&&e.target.closest?e.target.closest('a[href]'):null;if(!a)return;var h=a.getAttribute('href');if(!h||h.charAt(0)==='#'||/^(javascript|mailto|tel):/i.test(h))return;e.preventDefault();if(a.target==='_blank'){P({type:'cta',url:a.href,close:false});}else{P({type:'navigate',url:h});}},true);function H(){P({type:'resize',height:Math.ceil(document.documentElement.scrollHeight)});}window.addEventListener('load',H);try{if(typeof ResizeObserver!=='undefined'){new ResizeObserver(H).observe(document.documentElement);}}catch(e){}})();`;
|
|
1977
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>html,body{margin:0;padding:0;background:transparent;}</style><style>${escapeClosing(css, "style")}</style></head><body>${html}<script>${bridge}</script>` + (js ? `<script>${escapeClosing(js, "script")}</script>` : "") + "</body></html>";
|
|
1978
|
+
}
|
|
1979
|
+
function escapeClosing(code, tag) {
|
|
1980
|
+
const rx = tag === "script" ? /<\/script/gi : /<\/style/gi;
|
|
1981
|
+
return code.replace(rx, `<\\/${tag}`);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1594
1984
|
// src/plugins/guides/renderers/custom-renderer.ts
|
|
1595
1985
|
var DEFAULT_PLACEMENT = { mode: "floating", position: "bottom-right" };
|
|
1596
1986
|
var DEFAULT_OFFSET = 16;
|
|
@@ -1611,17 +2001,11 @@ var CustomRenderer = class extends BaseRenderer {
|
|
|
1611
2001
|
const doc = root.ownerDocument ?? document;
|
|
1612
2002
|
const wrapper = doc.createElement("div");
|
|
1613
2003
|
wrapper.className = placement.mode === "anchored" ? "veo-custom-anchored" : "veo-custom-floating";
|
|
1614
|
-
const iframe = doc
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
const hasFixedHeight = placement.height !== void 0 && placement.height !== null;
|
|
1620
|
-
if (hasFixedHeight) {
|
|
1621
|
-
iframe.style.height = toCssSize(placement.height);
|
|
1622
|
-
}
|
|
1623
|
-
const token = uuidv7();
|
|
1624
|
-
iframe.srcdoc = buildSrcdoc(step.html, step.css ?? "", step.js ?? "", token);
|
|
2004
|
+
const { iframe, cleanup } = mountCustomFrame(doc, step, context, {
|
|
2005
|
+
width: toCssSize(placement.width ?? DEFAULT_WIDTH),
|
|
2006
|
+
fixedHeight: placement.height ?? null
|
|
2007
|
+
});
|
|
2008
|
+
this.registerCleanup(cleanup);
|
|
1625
2009
|
wrapper.appendChild(iframe);
|
|
1626
2010
|
root.appendChild(wrapper);
|
|
1627
2011
|
if (placement.mode === "floating") {
|
|
@@ -1629,50 +2013,6 @@ var CustomRenderer = class extends BaseRenderer {
|
|
|
1629
2013
|
} else if (anchor) {
|
|
1630
2014
|
this.registerCleanup(await this.setupAnchoredPosition(anchor, wrapper, placement));
|
|
1631
2015
|
}
|
|
1632
|
-
const close = () => context.onClose();
|
|
1633
|
-
const onMessage = (event) => {
|
|
1634
|
-
if (event.source !== iframe.contentWindow) return;
|
|
1635
|
-
const data = event.data;
|
|
1636
|
-
if (!data || typeof data !== "object" || data.__veo !== token) return;
|
|
1637
|
-
switch (data.type) {
|
|
1638
|
-
case "dismiss":
|
|
1639
|
-
context.onInteraction({
|
|
1640
|
-
guideId: context.guide.guideId,
|
|
1641
|
-
stepIndex: 0,
|
|
1642
|
-
action: "dismissed"
|
|
1643
|
-
});
|
|
1644
|
-
close();
|
|
1645
|
-
break;
|
|
1646
|
-
case "cta":
|
|
1647
|
-
context.onInteraction({
|
|
1648
|
-
guideId: context.guide.guideId,
|
|
1649
|
-
stepIndex: 0,
|
|
1650
|
-
action: "cta_clicked"
|
|
1651
|
-
});
|
|
1652
|
-
if (typeof data.url === "string" && isSafeUrl(data.url)) {
|
|
1653
|
-
window.open(data.url, "_blank", "noopener,noreferrer");
|
|
1654
|
-
}
|
|
1655
|
-
if (data.close !== false) close();
|
|
1656
|
-
break;
|
|
1657
|
-
case "navigate":
|
|
1658
|
-
if (typeof data.url === "string" && isSafeUrl(data.url)) {
|
|
1659
|
-
window.location.assign(data.url);
|
|
1660
|
-
}
|
|
1661
|
-
break;
|
|
1662
|
-
case "track":
|
|
1663
|
-
if (typeof data.name === "string" && context.onTrack) {
|
|
1664
|
-
context.onTrack(data.name, isRecord(data.props) ? data.props : void 0);
|
|
1665
|
-
}
|
|
1666
|
-
break;
|
|
1667
|
-
case "resize":
|
|
1668
|
-
if (!hasFixedHeight && typeof data.height === "number" && data.height > 0) {
|
|
1669
|
-
iframe.style.height = `${Math.ceil(data.height)}px`;
|
|
1670
|
-
}
|
|
1671
|
-
break;
|
|
1672
|
-
}
|
|
1673
|
-
};
|
|
1674
|
-
window.addEventListener("message", onMessage);
|
|
1675
|
-
this.registerCleanup(() => window.removeEventListener("message", onMessage));
|
|
1676
2016
|
context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
|
|
1677
2017
|
}
|
|
1678
2018
|
async setupAnchoredPosition(anchor, wrapper, placement) {
|
|
@@ -1698,12 +2038,6 @@ var CustomRenderer = class extends BaseRenderer {
|
|
|
1698
2038
|
};
|
|
1699
2039
|
}
|
|
1700
2040
|
};
|
|
1701
|
-
function toCssSize(value) {
|
|
1702
|
-
return typeof value === "number" ? `${value}px` : value;
|
|
1703
|
-
}
|
|
1704
|
-
function isRecord(value) {
|
|
1705
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1706
|
-
}
|
|
1707
2041
|
function applyFloatingPosition(wrapper, placement) {
|
|
1708
2042
|
const position = placement.position ?? "bottom-right";
|
|
1709
2043
|
const ox = `${placement.offsetX ?? DEFAULT_OFFSET}px`;
|
|
@@ -1727,21 +2061,428 @@ function applyFloatingPosition(wrapper, placement) {
|
|
|
1727
2061
|
s.transform = "translateX(-50%)";
|
|
1728
2062
|
}
|
|
1729
2063
|
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
2064
|
+
|
|
2065
|
+
// src/plugins/guides/form-content.ts
|
|
2066
|
+
function mountFormContent(card, context, doc) {
|
|
2067
|
+
const step = context.guide.guideSteps[0];
|
|
2068
|
+
if (!step) return;
|
|
2069
|
+
const fields = step.fields ?? [];
|
|
2070
|
+
const dismiss = () => {
|
|
2071
|
+
context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "dismissed" });
|
|
2072
|
+
context.onClose();
|
|
2073
|
+
};
|
|
2074
|
+
const content = doc.createElement("div");
|
|
2075
|
+
content.className = "veo-guide-content";
|
|
2076
|
+
if (step.title) {
|
|
2077
|
+
const heading = doc.createElement("h2");
|
|
2078
|
+
heading.className = "veo-guide-title";
|
|
2079
|
+
heading.textContent = step.title;
|
|
2080
|
+
content.appendChild(heading);
|
|
2081
|
+
}
|
|
2082
|
+
if (step.content) {
|
|
2083
|
+
const paragraph = doc.createElement("p");
|
|
2084
|
+
paragraph.className = "veo-guide-text";
|
|
2085
|
+
paragraph.textContent = step.content;
|
|
2086
|
+
content.appendChild(paragraph);
|
|
2087
|
+
}
|
|
2088
|
+
const readers = [];
|
|
2089
|
+
const form = doc.createElement("form");
|
|
2090
|
+
form.className = "veo-form";
|
|
2091
|
+
form.noValidate = true;
|
|
2092
|
+
for (const field of fields) {
|
|
2093
|
+
const { wrapper, read } = buildField(field, doc);
|
|
2094
|
+
readers.push({ field, read });
|
|
2095
|
+
form.appendChild(wrapper);
|
|
2096
|
+
}
|
|
2097
|
+
const error = doc.createElement("p");
|
|
2098
|
+
error.className = "veo-form-error";
|
|
2099
|
+
error.style.display = "none";
|
|
2100
|
+
form.appendChild(error);
|
|
2101
|
+
const actions = doc.createElement("div");
|
|
2102
|
+
actions.className = "veo-guide-actions";
|
|
2103
|
+
const submit = doc.createElement("button");
|
|
2104
|
+
submit.type = "submit";
|
|
2105
|
+
submit.className = "veo-guide-cta";
|
|
2106
|
+
submit.textContent = step.ctaText || "Enviar";
|
|
2107
|
+
actions.appendChild(submit);
|
|
2108
|
+
form.appendChild(actions);
|
|
2109
|
+
form.addEventListener("submit", (e) => {
|
|
2110
|
+
e.preventDefault();
|
|
2111
|
+
error.style.display = "none";
|
|
2112
|
+
const answers = {};
|
|
2113
|
+
for (const { field, read } of readers) {
|
|
2114
|
+
const value = read();
|
|
2115
|
+
const empty = value === void 0 || value === null || value === "";
|
|
2116
|
+
if (empty) {
|
|
2117
|
+
if (field.required) {
|
|
2118
|
+
error.textContent = `Complet\xE1 "${field.label}"`;
|
|
2119
|
+
error.style.display = "block";
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
continue;
|
|
2123
|
+
}
|
|
2124
|
+
answers[field.key] = value;
|
|
2125
|
+
}
|
|
2126
|
+
if (Object.keys(answers).length === 0) {
|
|
2127
|
+
error.textContent = "Respond\xE9 al menos un campo";
|
|
2128
|
+
error.style.display = "block";
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
submit.disabled = true;
|
|
2132
|
+
const submitFn = context.onFormSubmit ?? (() => Promise.resolve(true));
|
|
2133
|
+
void submitFn(answers).then((ok) => {
|
|
2134
|
+
if (!ok) {
|
|
2135
|
+
submit.disabled = false;
|
|
2136
|
+
error.textContent = "No se pudo enviar. Prob\xE1 de nuevo.";
|
|
2137
|
+
error.style.display = "block";
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "completed" });
|
|
2141
|
+
const note = context.isPreview ? context.previewNote ?? "Vista previa: la respuesta NO se guard\xF3." : null;
|
|
2142
|
+
showThanks(card, doc, note);
|
|
2143
|
+
window.setTimeout(() => context.onClose(), note ? 2200 : 1400);
|
|
2144
|
+
});
|
|
2145
|
+
});
|
|
2146
|
+
content.appendChild(form);
|
|
2147
|
+
const closeBtn = doc.createElement("button");
|
|
2148
|
+
closeBtn.type = "button";
|
|
2149
|
+
closeBtn.className = "veo-guide-close";
|
|
2150
|
+
closeBtn.setAttribute("aria-label", "Cerrar");
|
|
2151
|
+
closeBtn.textContent = "\xD7";
|
|
2152
|
+
closeBtn.addEventListener("click", dismiss);
|
|
2153
|
+
content.appendChild(closeBtn);
|
|
2154
|
+
card.appendChild(content);
|
|
2155
|
+
}
|
|
2156
|
+
function showThanks(card, doc, note) {
|
|
2157
|
+
while (card.firstChild) card.removeChild(card.firstChild);
|
|
2158
|
+
const thanks = doc.createElement("div");
|
|
2159
|
+
thanks.className = "veo-form-thanks";
|
|
2160
|
+
thanks.textContent = "\u2713 \xA1Gracias por tu respuesta!";
|
|
2161
|
+
card.appendChild(thanks);
|
|
2162
|
+
if (note) {
|
|
2163
|
+
const noteEl = doc.createElement("p");
|
|
2164
|
+
noteEl.className = "veo-form-preview-note";
|
|
2165
|
+
noteEl.textContent = note;
|
|
2166
|
+
card.appendChild(noteEl);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
function buildField(field, doc) {
|
|
2170
|
+
const wrapper = doc.createElement("div");
|
|
2171
|
+
wrapper.className = "veo-form-field";
|
|
2172
|
+
const label = doc.createElement("label");
|
|
2173
|
+
label.className = "veo-form-label";
|
|
2174
|
+
label.textContent = field.required ? `${field.label} *` : field.label;
|
|
2175
|
+
wrapper.appendChild(label);
|
|
2176
|
+
switch (field.type) {
|
|
2177
|
+
case "textarea": {
|
|
2178
|
+
const input = doc.createElement("textarea");
|
|
2179
|
+
input.className = "veo-form-input";
|
|
2180
|
+
input.rows = 3;
|
|
2181
|
+
if (field.placeholder) input.placeholder = field.placeholder;
|
|
2182
|
+
wrapper.appendChild(input);
|
|
2183
|
+
return { wrapper, read: () => input.value.trim() };
|
|
2184
|
+
}
|
|
2185
|
+
case "number": {
|
|
2186
|
+
const input = doc.createElement("input");
|
|
2187
|
+
input.type = "number";
|
|
2188
|
+
input.className = "veo-form-input";
|
|
2189
|
+
if (field.placeholder) input.placeholder = field.placeholder;
|
|
2190
|
+
wrapper.appendChild(input);
|
|
2191
|
+
return {
|
|
2192
|
+
wrapper,
|
|
2193
|
+
read: () => input.value.trim() === "" ? void 0 : Number(input.value)
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
case "select": {
|
|
2197
|
+
const select = doc.createElement("select");
|
|
2198
|
+
select.className = "veo-form-input";
|
|
2199
|
+
const blank = doc.createElement("option");
|
|
2200
|
+
blank.value = "";
|
|
2201
|
+
blank.textContent = field.placeholder || "Eleg\xED una opci\xF3n\u2026";
|
|
2202
|
+
select.appendChild(blank);
|
|
2203
|
+
for (const opt of field.options ?? []) {
|
|
2204
|
+
const option = doc.createElement("option");
|
|
2205
|
+
option.value = opt;
|
|
2206
|
+
option.textContent = opt;
|
|
2207
|
+
select.appendChild(option);
|
|
2208
|
+
}
|
|
2209
|
+
wrapper.appendChild(select);
|
|
2210
|
+
return { wrapper, read: () => select.value || void 0 };
|
|
2211
|
+
}
|
|
2212
|
+
case "radio": {
|
|
2213
|
+
const group = doc.createElement("div");
|
|
2214
|
+
group.className = "veo-form-options";
|
|
2215
|
+
const name = `veo-${field.key}`;
|
|
2216
|
+
const inputs = [];
|
|
2217
|
+
for (const opt of field.options ?? []) {
|
|
2218
|
+
const optionLabel = doc.createElement("label");
|
|
2219
|
+
optionLabel.className = "veo-form-option";
|
|
2220
|
+
const radio = doc.createElement("input");
|
|
2221
|
+
radio.type = "radio";
|
|
2222
|
+
radio.name = name;
|
|
2223
|
+
radio.value = opt;
|
|
2224
|
+
inputs.push(radio);
|
|
2225
|
+
const text = doc.createElement("span");
|
|
2226
|
+
text.textContent = opt;
|
|
2227
|
+
optionLabel.appendChild(radio);
|
|
2228
|
+
optionLabel.appendChild(text);
|
|
2229
|
+
group.appendChild(optionLabel);
|
|
2230
|
+
}
|
|
2231
|
+
wrapper.appendChild(group);
|
|
2232
|
+
return { wrapper, read: () => inputs.find((i) => i.checked)?.value };
|
|
2233
|
+
}
|
|
2234
|
+
case "multiselect": {
|
|
2235
|
+
const group = doc.createElement("div");
|
|
2236
|
+
group.className = "veo-form-options";
|
|
2237
|
+
const inputs = [];
|
|
2238
|
+
for (const opt of field.options ?? []) {
|
|
2239
|
+
const optionLabel = doc.createElement("label");
|
|
2240
|
+
optionLabel.className = "veo-form-option";
|
|
2241
|
+
const checkbox = doc.createElement("input");
|
|
2242
|
+
checkbox.type = "checkbox";
|
|
2243
|
+
checkbox.value = opt;
|
|
2244
|
+
inputs.push(checkbox);
|
|
2245
|
+
const text = doc.createElement("span");
|
|
2246
|
+
text.textContent = opt;
|
|
2247
|
+
optionLabel.appendChild(checkbox);
|
|
2248
|
+
optionLabel.appendChild(text);
|
|
2249
|
+
group.appendChild(optionLabel);
|
|
2250
|
+
}
|
|
2251
|
+
wrapper.appendChild(group);
|
|
2252
|
+
return {
|
|
2253
|
+
wrapper,
|
|
2254
|
+
read: () => {
|
|
2255
|
+
const values = inputs.filter((i) => i.checked).map((i) => i.value);
|
|
2256
|
+
return values.length > 0 ? values : void 0;
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
case "checkbox": {
|
|
2261
|
+
const optionLabel = doc.createElement("label");
|
|
2262
|
+
optionLabel.className = "veo-form-option";
|
|
2263
|
+
const checkbox = doc.createElement("input");
|
|
2264
|
+
checkbox.type = "checkbox";
|
|
2265
|
+
const text = doc.createElement("span");
|
|
2266
|
+
text.textContent = field.placeholder || "S\xED";
|
|
2267
|
+
optionLabel.appendChild(checkbox);
|
|
2268
|
+
optionLabel.appendChild(text);
|
|
2269
|
+
wrapper.appendChild(optionLabel);
|
|
2270
|
+
return { wrapper, read: () => checkbox.checked ? true : field.required ? "" : false };
|
|
2271
|
+
}
|
|
2272
|
+
case "yesno": {
|
|
2273
|
+
const group = doc.createElement("div");
|
|
2274
|
+
group.className = "veo-form-options";
|
|
2275
|
+
const name = `veo-${field.key}`;
|
|
2276
|
+
const [yesLabel, noLabel] = field.options?.length === 2 ? field.options : ["S\xED", "No"];
|
|
2277
|
+
const inputs = [];
|
|
2278
|
+
for (const { label: optText, value } of [
|
|
2279
|
+
{ label: yesLabel, value: true },
|
|
2280
|
+
{ label: noLabel, value: false }
|
|
2281
|
+
]) {
|
|
2282
|
+
const optionLabel = doc.createElement("label");
|
|
2283
|
+
optionLabel.className = "veo-form-option";
|
|
2284
|
+
const radio = doc.createElement("input");
|
|
2285
|
+
radio.type = "radio";
|
|
2286
|
+
radio.name = name;
|
|
2287
|
+
inputs.push({ input: radio, value });
|
|
2288
|
+
const text = doc.createElement("span");
|
|
2289
|
+
text.textContent = optText;
|
|
2290
|
+
optionLabel.appendChild(radio);
|
|
2291
|
+
optionLabel.appendChild(text);
|
|
2292
|
+
group.appendChild(optionLabel);
|
|
2293
|
+
}
|
|
2294
|
+
wrapper.appendChild(group);
|
|
2295
|
+
return { wrapper, read: () => inputs.find((i) => i.input.checked)?.value };
|
|
2296
|
+
}
|
|
2297
|
+
case "nps":
|
|
2298
|
+
return buildScale(wrapper, doc, 0, 10, "veo-nps-btn");
|
|
2299
|
+
case "rating":
|
|
2300
|
+
return buildScale(wrapper, doc, 1, 5, "veo-rating-btn", "\u2605");
|
|
2301
|
+
default: {
|
|
2302
|
+
const input = doc.createElement("input");
|
|
2303
|
+
input.type = "text";
|
|
2304
|
+
input.className = "veo-form-input";
|
|
2305
|
+
if (field.placeholder) input.placeholder = field.placeholder;
|
|
2306
|
+
wrapper.appendChild(input);
|
|
2307
|
+
return { wrapper, read: () => input.value.trim() };
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
function buildScale(wrapper, doc, min, max, btnClass, symbol) {
|
|
2312
|
+
const row = doc.createElement("div");
|
|
2313
|
+
row.className = "veo-form-scale";
|
|
2314
|
+
let selected;
|
|
2315
|
+
const buttons = [];
|
|
2316
|
+
for (let n = min; n <= max; n++) {
|
|
2317
|
+
const btn = doc.createElement("button");
|
|
2318
|
+
btn.type = "button";
|
|
2319
|
+
btn.className = btnClass;
|
|
2320
|
+
btn.textContent = symbol ?? String(n);
|
|
2321
|
+
btn.setAttribute("aria-label", String(n));
|
|
2322
|
+
btn.addEventListener("click", () => {
|
|
2323
|
+
selected = n;
|
|
2324
|
+
buttons.forEach((b, i) => {
|
|
2325
|
+
const active2 = symbol ? i + min <= n : i + min === n;
|
|
2326
|
+
b.classList.toggle("veo-scale-active", active2);
|
|
2327
|
+
});
|
|
2328
|
+
});
|
|
2329
|
+
buttons.push(btn);
|
|
2330
|
+
row.appendChild(btn);
|
|
2331
|
+
}
|
|
2332
|
+
wrapper.appendChild(row);
|
|
2333
|
+
return { wrapper, read: () => selected };
|
|
1733
2334
|
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
2335
|
+
|
|
2336
|
+
// src/plugins/guides/renderers/form-renderer.ts
|
|
2337
|
+
var FormRenderer = class extends BaseRenderer {
|
|
2338
|
+
render(context) {
|
|
2339
|
+
const step = context.guide.guideSteps[0];
|
|
2340
|
+
if (!step || (step.fields ?? []).length === 0) return;
|
|
2341
|
+
const { root } = this.createHost();
|
|
2342
|
+
this.applyDesign(step.style);
|
|
2343
|
+
const doc = root.ownerDocument ?? document;
|
|
2344
|
+
const overlay = doc.createElement("div");
|
|
2345
|
+
overlay.className = "veo-modal-overlay";
|
|
2346
|
+
const card = doc.createElement("div");
|
|
2347
|
+
card.className = "veo-modal-card";
|
|
2348
|
+
mountFormContent(card, context, doc);
|
|
2349
|
+
overlay.appendChild(card);
|
|
2350
|
+
root.appendChild(overlay);
|
|
2351
|
+
const dismiss = () => {
|
|
2352
|
+
context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "dismissed" });
|
|
2353
|
+
context.onClose();
|
|
2354
|
+
};
|
|
2355
|
+
overlay.addEventListener("click", (e) => {
|
|
2356
|
+
if (e.target === overlay) dismiss();
|
|
2357
|
+
});
|
|
2358
|
+
const onKey = (e) => {
|
|
2359
|
+
if (e.key === "Escape") dismiss();
|
|
2360
|
+
};
|
|
2361
|
+
document.addEventListener("keydown", onKey);
|
|
2362
|
+
this.registerCleanup(() => document.removeEventListener("keydown", onKey));
|
|
2363
|
+
context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
|
|
2364
|
+
}
|
|
2365
|
+
};
|
|
2366
|
+
|
|
2367
|
+
// src/plugins/guides/inline-host.ts
|
|
2368
|
+
function readInlinePosition(style) {
|
|
2369
|
+
const p = style?.inlinePosition;
|
|
2370
|
+
return p === "before" || p === "prepend" || p === "append" ? p : "after";
|
|
2371
|
+
}
|
|
2372
|
+
function insertHost(anchor, host, position) {
|
|
2373
|
+
switch (position) {
|
|
2374
|
+
case "before":
|
|
2375
|
+
anchor.before(host);
|
|
2376
|
+
break;
|
|
2377
|
+
case "after":
|
|
2378
|
+
anchor.after(host);
|
|
2379
|
+
break;
|
|
2380
|
+
case "prepend":
|
|
2381
|
+
anchor.prepend(host);
|
|
2382
|
+
break;
|
|
2383
|
+
case "append":
|
|
2384
|
+
anchor.append(host);
|
|
2385
|
+
break;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
function keepHostAttached(host, selector, position) {
|
|
2389
|
+
const id = window.setInterval(() => {
|
|
2390
|
+
if (host.isConnected) return;
|
|
2391
|
+
const anchor = document.querySelector(selector);
|
|
2392
|
+
if (anchor) insertHost(anchor, host, position);
|
|
2393
|
+
}, 1e3);
|
|
2394
|
+
return () => window.clearInterval(id);
|
|
1737
2395
|
}
|
|
1738
2396
|
|
|
2397
|
+
// src/plugins/guides/renderers/inline-custom-renderer.ts
|
|
2398
|
+
var InlineCustomRenderer = class extends BaseRenderer {
|
|
2399
|
+
async render(context) {
|
|
2400
|
+
const step = context.guide.guideSteps[0];
|
|
2401
|
+
if (!step || typeof step.html !== "string" || step.html.length === 0) return;
|
|
2402
|
+
const selector = step.selector ?? context.guide.activationRules.selector;
|
|
2403
|
+
if (typeof selector !== "string" || selector.length === 0) return;
|
|
2404
|
+
const anchor = await waitForElement(selector);
|
|
2405
|
+
if (!anchor) return;
|
|
2406
|
+
const { host, root } = this.createHost();
|
|
2407
|
+
host.style.display = "block";
|
|
2408
|
+
const position = readInlinePosition(step.style);
|
|
2409
|
+
insertHost(anchor, host, position);
|
|
2410
|
+
this.registerCleanup(keepHostAttached(host, selector, position));
|
|
2411
|
+
const doc = root.ownerDocument ?? document;
|
|
2412
|
+
const wrapper = doc.createElement("div");
|
|
2413
|
+
wrapper.className = "veo-custom-inline";
|
|
2414
|
+
const { iframe, cleanup } = mountCustomFrame(doc, step, context, { width: "100%" });
|
|
2415
|
+
this.registerCleanup(cleanup);
|
|
2416
|
+
wrapper.appendChild(iframe);
|
|
2417
|
+
root.appendChild(wrapper);
|
|
2418
|
+
context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
|
|
2419
|
+
}
|
|
2420
|
+
};
|
|
2421
|
+
|
|
2422
|
+
// src/plugins/guides/renderers/inline-form-renderer.ts
|
|
2423
|
+
var InlineFormRenderer = class extends BaseRenderer {
|
|
2424
|
+
async render(context) {
|
|
2425
|
+
const step = context.guide.guideSteps[0];
|
|
2426
|
+
if (!step || (step.fields ?? []).length === 0) return;
|
|
2427
|
+
const selector = step.selector ?? context.guide.activationRules.selector;
|
|
2428
|
+
if (typeof selector !== "string" || selector.length === 0) return;
|
|
2429
|
+
const anchor = await waitForElement(selector);
|
|
2430
|
+
if (!anchor) return;
|
|
2431
|
+
const { host, root } = this.createHost();
|
|
2432
|
+
host.style.display = "block";
|
|
2433
|
+
const position = readInlinePosition(step.style);
|
|
2434
|
+
insertHost(anchor, host, position);
|
|
2435
|
+
this.registerCleanup(keepHostAttached(host, selector, position));
|
|
2436
|
+
this.applyDesign(step.style);
|
|
2437
|
+
const doc = root.ownerDocument ?? document;
|
|
2438
|
+
const card = doc.createElement("div");
|
|
2439
|
+
card.className = "veo-inline";
|
|
2440
|
+
mountFormContent(card, context, doc);
|
|
2441
|
+
root.appendChild(card);
|
|
2442
|
+
context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
|
|
2443
|
+
}
|
|
2444
|
+
};
|
|
2445
|
+
|
|
2446
|
+
// src/plugins/guides/renderers/inline-renderer.ts
|
|
2447
|
+
var InlineRenderer = class extends BaseRenderer {
|
|
2448
|
+
async render(context) {
|
|
2449
|
+
const step = context.guide.guideSteps[0];
|
|
2450
|
+
if (!step) return;
|
|
2451
|
+
const selector = step.selector ?? context.guide.activationRules.selector;
|
|
2452
|
+
if (typeof selector !== "string" || selector.length === 0) return;
|
|
2453
|
+
const anchor = await waitForElement(selector);
|
|
2454
|
+
if (!anchor) return;
|
|
2455
|
+
const { host, root } = this.createHost();
|
|
2456
|
+
host.style.display = "block";
|
|
2457
|
+
const position = readInlinePosition(step.style);
|
|
2458
|
+
insertHost(anchor, host, position);
|
|
2459
|
+
this.registerCleanup(keepHostAttached(host, selector, position));
|
|
2460
|
+
this.applyDesign(step.style);
|
|
2461
|
+
const ownerDocument = root.ownerDocument ?? document;
|
|
2462
|
+
const card = ownerDocument.createElement("div");
|
|
2463
|
+
card.className = "veo-inline";
|
|
2464
|
+
const dismiss = (action, ctaUrl) => {
|
|
2465
|
+
context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action });
|
|
2466
|
+
if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
|
|
2467
|
+
context.onClose();
|
|
2468
|
+
};
|
|
2469
|
+
const content = buildStepContent(step, ownerDocument, {
|
|
2470
|
+
onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
|
|
2471
|
+
onDismiss: () => dismiss("dismissed")
|
|
2472
|
+
});
|
|
2473
|
+
card.appendChild(content);
|
|
2474
|
+
root.appendChild(card);
|
|
2475
|
+
context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
|
|
2476
|
+
}
|
|
2477
|
+
};
|
|
2478
|
+
|
|
1739
2479
|
// src/plugins/guides/renderers/modal-renderer.ts
|
|
1740
2480
|
var ModalRenderer = class extends BaseRenderer {
|
|
1741
2481
|
render(context) {
|
|
1742
2482
|
const step = context.guide.guideSteps[0];
|
|
1743
2483
|
if (!step) return;
|
|
1744
2484
|
const { root } = this.createHost();
|
|
2485
|
+
this.applyDesign(step.style);
|
|
1745
2486
|
const ownerDocument = root.ownerDocument ?? document;
|
|
1746
2487
|
const overlay = ownerDocument.createElement("div");
|
|
1747
2488
|
overlay.className = "veo-modal-overlay";
|
|
@@ -1789,6 +2530,7 @@ var TooltipRenderer = class extends BaseRenderer {
|
|
|
1789
2530
|
const anchor = await waitForElement(selector);
|
|
1790
2531
|
if (!anchor) return;
|
|
1791
2532
|
const { root } = this.createHost();
|
|
2533
|
+
this.applyDesign(step.style);
|
|
1792
2534
|
const ownerDocument = root.ownerDocument ?? document;
|
|
1793
2535
|
const tooltip = ownerDocument.createElement("div");
|
|
1794
2536
|
tooltip.className = "veo-tooltip";
|
|
@@ -1916,6 +2658,7 @@ var WalkthroughRenderer = class extends BaseRenderer {
|
|
|
1916
2658
|
const anchor = await waitForElement(selector, WALKTHROUGH_STEP_SELECTOR_TIMEOUT_MS);
|
|
1917
2659
|
if (!anchor) return false;
|
|
1918
2660
|
const { root } = this.createHost();
|
|
2661
|
+
this.applyDesign(step.style);
|
|
1919
2662
|
const ownerDocument = root.ownerDocument ?? document;
|
|
1920
2663
|
const tooltip = ownerDocument.createElement("div");
|
|
1921
2664
|
tooltip.className = "veo-walkthrough";
|
|
@@ -1927,14 +2670,18 @@ var WalkthroughRenderer = class extends BaseRenderer {
|
|
|
1927
2670
|
context.callbacks
|
|
1928
2671
|
);
|
|
1929
2672
|
tooltip.appendChild(content);
|
|
2673
|
+
const arrowEl = ownerDocument.createElement("div");
|
|
2674
|
+
arrowEl.className = "veo-walkthrough-arrow";
|
|
2675
|
+
tooltip.appendChild(arrowEl);
|
|
1930
2676
|
root.appendChild(tooltip);
|
|
1931
2677
|
const updatePosition = async () => {
|
|
1932
|
-
const { x, y } = await dom.computePosition(anchor, tooltip, {
|
|
2678
|
+
const { x, y, placement, middlewareData } = await dom.computePosition(anchor, tooltip, {
|
|
1933
2679
|
placement: "bottom",
|
|
1934
|
-
middleware: [dom.offset(
|
|
2680
|
+
middleware: [dom.offset(10), dom.flip(), dom.shift({ padding: 8 }), dom.arrow({ element: arrowEl })]
|
|
1935
2681
|
});
|
|
1936
2682
|
tooltip.style.left = `${x}px`;
|
|
1937
2683
|
tooltip.style.top = `${y}px`;
|
|
2684
|
+
positionArrow(arrowEl, placement, middlewareData.arrow);
|
|
1938
2685
|
};
|
|
1939
2686
|
await updatePosition();
|
|
1940
2687
|
const reposition = () => {
|
|
@@ -1949,6 +2696,21 @@ var WalkthroughRenderer = class extends BaseRenderer {
|
|
|
1949
2696
|
return true;
|
|
1950
2697
|
}
|
|
1951
2698
|
};
|
|
2699
|
+
var STATIC_SIDE = {
|
|
2700
|
+
top: "bottom",
|
|
2701
|
+
bottom: "top",
|
|
2702
|
+
left: "right",
|
|
2703
|
+
right: "left"
|
|
2704
|
+
};
|
|
2705
|
+
function positionArrow(arrowEl, placement, data) {
|
|
2706
|
+
const side = STATIC_SIDE[placement.split("-")[0] ?? "bottom"] ?? "top";
|
|
2707
|
+
for (const prop of ["top", "bottom", "left", "right"]) {
|
|
2708
|
+
arrowEl.style.setProperty(prop, "");
|
|
2709
|
+
}
|
|
2710
|
+
if (data?.x != null) arrowEl.style.setProperty("left", `${data.x}px`);
|
|
2711
|
+
if (data?.y != null) arrowEl.style.setProperty("top", `${data.y}px`);
|
|
2712
|
+
arrowEl.style.setProperty(side, "-6px");
|
|
2713
|
+
}
|
|
1952
2714
|
|
|
1953
2715
|
// src/plugins/guides/walkthrough-state.ts
|
|
1954
2716
|
var WalkthroughStateManager = class {
|
|
@@ -2078,10 +2840,18 @@ var GuidesController = class {
|
|
|
2078
2840
|
constructor(client, config) {
|
|
2079
2841
|
this.client = client;
|
|
2080
2842
|
this.activeRenderers = /* @__PURE__ */ new Set();
|
|
2843
|
+
/**
|
|
2844
|
+
* Guías single-step actualmente en pantalla, por `guideId`. Evita repintar/
|
|
2845
|
+
* duplicar la misma guía en cada pageview de una SPA (p.ej. una guía inline
|
|
2846
|
+
* anclada a un elemento persistente). Se libera al cerrarse la guía.
|
|
2847
|
+
*/
|
|
2848
|
+
this.activeByGuideId = /* @__PURE__ */ new Map();
|
|
2081
2849
|
this.dispatched = /* @__PURE__ */ new Set();
|
|
2082
2850
|
this.activeWalkthroughRenderer = null;
|
|
2083
2851
|
this.activeWalkthroughGuide = null;
|
|
2084
2852
|
this.debug = config.debug === true;
|
|
2853
|
+
this.apiUrl = config.apiUrl;
|
|
2854
|
+
this.apiKey = config.apiKey;
|
|
2085
2855
|
this.resolver = config.resolver ?? new GuideResolverClient(config.apiUrl, config.apiKey, this.debug);
|
|
2086
2856
|
if (config.trackerQueue) {
|
|
2087
2857
|
this.trackerQueue = config.trackerQueue;
|
|
@@ -2141,6 +2911,10 @@ var GuidesController = class {
|
|
|
2141
2911
|
if (this.debug) console.log(`[veo] guides: filtered locally guideId=${guide.guideId}`);
|
|
2142
2912
|
continue;
|
|
2143
2913
|
}
|
|
2914
|
+
if (this.activeByGuideId.has(guide.guideId)) {
|
|
2915
|
+
if (this.debug) console.log(`[veo] guides: already shown guideId=${guide.guideId}`);
|
|
2916
|
+
continue;
|
|
2917
|
+
}
|
|
2144
2918
|
if (guide.guideType === "walkthrough" && guide.guideSteps.length > 1) {
|
|
2145
2919
|
if (this.walkthroughState.hasActive()) {
|
|
2146
2920
|
if (this.debug)
|
|
@@ -2163,6 +2937,7 @@ var GuidesController = class {
|
|
|
2163
2937
|
}
|
|
2164
2938
|
}
|
|
2165
2939
|
this.activeRenderers.clear();
|
|
2940
|
+
this.activeByGuideId.clear();
|
|
2166
2941
|
if (this.activeWalkthroughRenderer) {
|
|
2167
2942
|
try {
|
|
2168
2943
|
this.activeWalkthroughRenderer.destroy();
|
|
@@ -2177,6 +2952,7 @@ var GuidesController = class {
|
|
|
2177
2952
|
runSingleStepRender(guide, sessionId, pageUrl) {
|
|
2178
2953
|
const renderer = this.createSingleStepRenderer(guide.guideType);
|
|
2179
2954
|
if (!renderer) return;
|
|
2955
|
+
this.activeByGuideId.set(guide.guideId, renderer);
|
|
2180
2956
|
const delayMs = guide.activationRules.delayMs ?? 0;
|
|
2181
2957
|
window.setTimeout(
|
|
2182
2958
|
() => this.mountSingleStepRenderer(guide, renderer, sessionId, pageUrl),
|
|
@@ -2185,9 +2961,13 @@ var GuidesController = class {
|
|
|
2185
2961
|
}
|
|
2186
2962
|
mountSingleStepRenderer(guide, renderer, sessionId, pageUrl) {
|
|
2187
2963
|
this.activeRenderers.add(renderer);
|
|
2964
|
+
this.activeByGuideId.set(guide.guideId, renderer);
|
|
2188
2965
|
const onClose = () => {
|
|
2189
2966
|
renderer.destroy();
|
|
2190
2967
|
this.activeRenderers.delete(renderer);
|
|
2968
|
+
if (this.activeByGuideId.get(guide.guideId) === renderer) {
|
|
2969
|
+
this.activeByGuideId.delete(guide.guideId);
|
|
2970
|
+
}
|
|
2191
2971
|
};
|
|
2192
2972
|
const onInteraction = (event) => {
|
|
2193
2973
|
this.handleInteraction(guide, sessionId, pageUrl, event);
|
|
@@ -2195,11 +2975,15 @@ var GuidesController = class {
|
|
|
2195
2975
|
const onTrack = (eventName, properties) => {
|
|
2196
2976
|
this.client.track(eventName, properties);
|
|
2197
2977
|
};
|
|
2978
|
+
const onFormSubmit = (answers) => this.submitFormResponse(guide.guideId, answers);
|
|
2198
2979
|
try {
|
|
2199
|
-
void renderer.render({ guide, onInteraction, onClose, onTrack });
|
|
2980
|
+
void renderer.render({ guide, onInteraction, onClose, onTrack, onFormSubmit });
|
|
2200
2981
|
} catch (err) {
|
|
2201
2982
|
if (this.debug) console.error("[veo] renderer.render threw:", err);
|
|
2202
2983
|
this.activeRenderers.delete(renderer);
|
|
2984
|
+
if (this.activeByGuideId.get(guide.guideId) === renderer) {
|
|
2985
|
+
this.activeByGuideId.delete(guide.guideId);
|
|
2986
|
+
}
|
|
2203
2987
|
}
|
|
2204
2988
|
}
|
|
2205
2989
|
/**
|
|
@@ -2431,10 +3215,40 @@ var GuidesController = class {
|
|
|
2431
3215
|
return new TooltipRenderer();
|
|
2432
3216
|
case "custom":
|
|
2433
3217
|
return new CustomRenderer();
|
|
3218
|
+
case "inline":
|
|
3219
|
+
return new InlineRenderer();
|
|
3220
|
+
case "inline-custom":
|
|
3221
|
+
return new InlineCustomRenderer();
|
|
3222
|
+
case "form":
|
|
3223
|
+
return new FormRenderer();
|
|
3224
|
+
case "inline-form":
|
|
3225
|
+
return new InlineFormRenderer();
|
|
2434
3226
|
case "walkthrough":
|
|
2435
3227
|
return null;
|
|
2436
3228
|
}
|
|
2437
3229
|
}
|
|
3230
|
+
/**
|
|
3231
|
+
* Envía las respuestas de una guía `form` al backend, que las mergea como
|
|
3232
|
+
* propiedades del usuario. Usa el endUserId actual (o el anónimo si aún no
|
|
3233
|
+
* hubo identify — el backend pre-crea la fila igual que identify).
|
|
3234
|
+
*/
|
|
3235
|
+
async submitFormResponse(guideId, answers) {
|
|
3236
|
+
const endUserId = this.client.getEndUserId() ?? this.client.getAnonymousId();
|
|
3237
|
+
try {
|
|
3238
|
+
const res = await fetch(`${this.apiUrl}/v1/guides/${guideId}/form-response`, {
|
|
3239
|
+
method: "POST",
|
|
3240
|
+
headers: { "Content-Type": "application/json", "X-Api-Key": this.apiKey },
|
|
3241
|
+
body: JSON.stringify({ endUserId, answers })
|
|
3242
|
+
});
|
|
3243
|
+
if (!res.ok && this.debug) {
|
|
3244
|
+
console.warn("[veo] form-response respondi\xF3", res.status);
|
|
3245
|
+
}
|
|
3246
|
+
return res.ok;
|
|
3247
|
+
} catch (err) {
|
|
3248
|
+
if (this.debug) console.warn("[veo] form-response fall\xF3:", err);
|
|
3249
|
+
return false;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
2438
3252
|
};
|
|
2439
3253
|
function extractPath(url) {
|
|
2440
3254
|
try {
|
|
@@ -2444,6 +3258,283 @@ function extractPath(url) {
|
|
|
2444
3258
|
}
|
|
2445
3259
|
}
|
|
2446
3260
|
|
|
3261
|
+
// src/plugins/guides/guide-preview.ts
|
|
3262
|
+
var PREVIEW_GUIDE_ID = "__veo_preview__";
|
|
3263
|
+
var GuidePreviewController = class {
|
|
3264
|
+
constructor() {
|
|
3265
|
+
this.singleStep = null;
|
|
3266
|
+
this.walkthrough = null;
|
|
3267
|
+
this.walkthroughGuide = null;
|
|
3268
|
+
this.walkthroughIndex = 0;
|
|
3269
|
+
/** Input de la preview ACTIVA (para formSubmit/nota de forms). */
|
|
3270
|
+
this.input = null;
|
|
3271
|
+
}
|
|
3272
|
+
preview(input) {
|
|
3273
|
+
this.close();
|
|
3274
|
+
this.input = input;
|
|
3275
|
+
const guide = normalize(input);
|
|
3276
|
+
const start = typeof input.startStepIndex === "number" ? input.startStepIndex : 0;
|
|
3277
|
+
const ready = guide.guideType === "walkthrough" ? this.startWalkthrough(guide, start) : this.renderSingleStep(guide);
|
|
3278
|
+
return { close: () => this.close(), ready };
|
|
3279
|
+
}
|
|
3280
|
+
close() {
|
|
3281
|
+
if (this.singleStep) {
|
|
3282
|
+
safeDestroy(this.singleStep);
|
|
3283
|
+
this.singleStep = null;
|
|
3284
|
+
}
|
|
3285
|
+
if (this.walkthrough) {
|
|
3286
|
+
safeDestroy(this.walkthrough);
|
|
3287
|
+
this.walkthrough = null;
|
|
3288
|
+
}
|
|
3289
|
+
this.walkthroughGuide = null;
|
|
3290
|
+
this.walkthroughIndex = 0;
|
|
3291
|
+
this.input = null;
|
|
3292
|
+
}
|
|
3293
|
+
async renderSingleStep(guide) {
|
|
3294
|
+
const renderer = createSingleStepRenderer(guide.guideType);
|
|
3295
|
+
if (!renderer) return { rendered: false };
|
|
3296
|
+
this.singleStep = renderer;
|
|
3297
|
+
let shown = false;
|
|
3298
|
+
try {
|
|
3299
|
+
await renderer.render({
|
|
3300
|
+
guide,
|
|
3301
|
+
onInteraction: (event) => {
|
|
3302
|
+
if (event.action === "shown") shown = true;
|
|
3303
|
+
},
|
|
3304
|
+
onClose: () => this.close(),
|
|
3305
|
+
onTrack: () => {
|
|
3306
|
+
},
|
|
3307
|
+
// Preview de forms: por defecto simula el envío SIN escribir props;
|
|
3308
|
+
// el caller puede pasar `formSubmit` real (link de preview).
|
|
3309
|
+
onFormSubmit: this.input?.formSubmit ?? (() => Promise.resolve(true)),
|
|
3310
|
+
isPreview: true,
|
|
3311
|
+
...this.input?.formSubmit && this.input.formSubmitNote !== void 0 ? { previewNote: this.input.formSubmitNote } : {}
|
|
3312
|
+
});
|
|
3313
|
+
} catch {
|
|
3314
|
+
this.close();
|
|
3315
|
+
return { rendered: false };
|
|
3316
|
+
}
|
|
3317
|
+
if (this.singleStep !== renderer) {
|
|
3318
|
+
safeDestroy(renderer);
|
|
3319
|
+
return { rendered: false };
|
|
3320
|
+
}
|
|
3321
|
+
return { rendered: shown };
|
|
3322
|
+
}
|
|
3323
|
+
async startWalkthrough(guide, startIndex = 0) {
|
|
3324
|
+
if (guide.guideSteps.length === 0) return { rendered: false };
|
|
3325
|
+
this.walkthroughGuide = guide;
|
|
3326
|
+
const idx = Math.min(Math.max(0, Math.floor(startIndex)), guide.guideSteps.length - 1);
|
|
3327
|
+
this.walkthroughIndex = idx;
|
|
3328
|
+
const rendered = await this.renderWalkthroughStep(idx);
|
|
3329
|
+
return { rendered };
|
|
3330
|
+
}
|
|
3331
|
+
async renderWalkthroughStep(index) {
|
|
3332
|
+
const guide = this.walkthroughGuide;
|
|
3333
|
+
if (!guide) return false;
|
|
3334
|
+
if (this.walkthrough) {
|
|
3335
|
+
safeDestroy(this.walkthrough);
|
|
3336
|
+
this.walkthrough = null;
|
|
3337
|
+
}
|
|
3338
|
+
const renderer = new WalkthroughRenderer();
|
|
3339
|
+
const callbacks = {
|
|
3340
|
+
onNext: () => {
|
|
3341
|
+
const next = this.walkthroughIndex + 1;
|
|
3342
|
+
if (next >= guide.guideSteps.length) {
|
|
3343
|
+
this.close();
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3346
|
+
this.walkthroughIndex = next;
|
|
3347
|
+
void this.renderWalkthroughStep(next);
|
|
3348
|
+
},
|
|
3349
|
+
onBack: () => {
|
|
3350
|
+
const prev = this.walkthroughIndex - 1;
|
|
3351
|
+
if (prev < 0) return;
|
|
3352
|
+
this.walkthroughIndex = prev;
|
|
3353
|
+
void this.renderWalkthroughStep(prev);
|
|
3354
|
+
},
|
|
3355
|
+
onSkip: () => this.close(),
|
|
3356
|
+
onComplete: () => this.close()
|
|
3357
|
+
};
|
|
3358
|
+
let success = false;
|
|
3359
|
+
try {
|
|
3360
|
+
success = await renderer.render({ guide, currentStepIndex: index, callbacks });
|
|
3361
|
+
} catch {
|
|
3362
|
+
success = false;
|
|
3363
|
+
}
|
|
3364
|
+
if (!success || this.walkthroughGuide !== guide) {
|
|
3365
|
+
safeDestroy(renderer);
|
|
3366
|
+
return false;
|
|
3367
|
+
}
|
|
3368
|
+
this.walkthrough = renderer;
|
|
3369
|
+
return true;
|
|
3370
|
+
}
|
|
3371
|
+
};
|
|
3372
|
+
var activeController = null;
|
|
3373
|
+
function previewGuide(input) {
|
|
3374
|
+
if (!hasDocument()) {
|
|
3375
|
+
return { close: () => {
|
|
3376
|
+
}, ready: Promise.resolve({ rendered: false }) };
|
|
3377
|
+
}
|
|
3378
|
+
if (!activeController) activeController = new GuidePreviewController();
|
|
3379
|
+
return activeController.preview(input);
|
|
3380
|
+
}
|
|
3381
|
+
function closeGuidePreview() {
|
|
3382
|
+
activeController?.close();
|
|
3383
|
+
}
|
|
3384
|
+
function normalize(input) {
|
|
3385
|
+
const selector = input.activationRules?.selector;
|
|
3386
|
+
const activationRules = {
|
|
3387
|
+
url: input.activationRules?.url ?? { type: "prefix", pattern: "/" },
|
|
3388
|
+
trigger: "immediate",
|
|
3389
|
+
...selector !== void 0 ? { selector } : {}
|
|
3390
|
+
};
|
|
3391
|
+
return {
|
|
3392
|
+
guideId: PREVIEW_GUIDE_ID,
|
|
3393
|
+
guideName: input.guideName ?? "Preview",
|
|
3394
|
+
guideType: input.guideType,
|
|
3395
|
+
guideSteps: input.guideSteps,
|
|
3396
|
+
activationRules,
|
|
3397
|
+
displayFrequency: "always",
|
|
3398
|
+
displayPriority: 0
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
function createSingleStepRenderer(type) {
|
|
3402
|
+
switch (type) {
|
|
3403
|
+
case "modal":
|
|
3404
|
+
return new ModalRenderer();
|
|
3405
|
+
case "banner":
|
|
3406
|
+
return new BannerRenderer();
|
|
3407
|
+
case "tooltip":
|
|
3408
|
+
return new TooltipRenderer();
|
|
3409
|
+
case "custom":
|
|
3410
|
+
return new CustomRenderer();
|
|
3411
|
+
case "inline":
|
|
3412
|
+
return new InlineRenderer();
|
|
3413
|
+
case "inline-custom":
|
|
3414
|
+
return new InlineCustomRenderer();
|
|
3415
|
+
case "form":
|
|
3416
|
+
return new FormRenderer();
|
|
3417
|
+
case "inline-form":
|
|
3418
|
+
return new InlineFormRenderer();
|
|
3419
|
+
case "walkthrough":
|
|
3420
|
+
return null;
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
function safeDestroy(renderer) {
|
|
3424
|
+
try {
|
|
3425
|
+
renderer.destroy();
|
|
3426
|
+
} catch {
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
// src/plugins/guides/preview-mode.ts
|
|
3431
|
+
var active = null;
|
|
3432
|
+
async function runPreviewMode(opts) {
|
|
3433
|
+
if (!hasDocument()) return null;
|
|
3434
|
+
active?.teardown();
|
|
3435
|
+
let guide = null;
|
|
3436
|
+
try {
|
|
3437
|
+
const res = await fetch(`${opts.apiUrl}/v1/guides/preview-resolve`, {
|
|
3438
|
+
method: "POST",
|
|
3439
|
+
headers: { "Content-Type": "application/json", "X-Api-Key": opts.apiKey },
|
|
3440
|
+
body: JSON.stringify({ token: opts.token })
|
|
3441
|
+
});
|
|
3442
|
+
if (res.ok) {
|
|
3443
|
+
const data = await res.json();
|
|
3444
|
+
guide = data.guide ?? null;
|
|
3445
|
+
} else if (opts.debug) {
|
|
3446
|
+
console.warn("[veo] preview-resolve respondi\xF3", res.status);
|
|
3447
|
+
}
|
|
3448
|
+
} catch (err) {
|
|
3449
|
+
if (opts.debug) console.warn("[veo] preview-resolve fall\xF3:", err);
|
|
3450
|
+
}
|
|
3451
|
+
const submitToBackend = async (answers) => {
|
|
3452
|
+
if (!guide) return false;
|
|
3453
|
+
const veo = window.veo;
|
|
3454
|
+
const endUserId = veo?.getEndUserId?.() ?? veo?.getAnonymousId?.() ?? null;
|
|
3455
|
+
if (!endUserId) return false;
|
|
3456
|
+
try {
|
|
3457
|
+
const res = await fetch(`${opts.apiUrl}/v1/guides/${guide.guideId}/form-response`, {
|
|
3458
|
+
method: "POST",
|
|
3459
|
+
headers: { "Content-Type": "application/json", "X-Api-Key": opts.apiKey },
|
|
3460
|
+
body: JSON.stringify({ endUserId, answers })
|
|
3461
|
+
});
|
|
3462
|
+
return res.ok;
|
|
3463
|
+
} catch {
|
|
3464
|
+
return false;
|
|
3465
|
+
}
|
|
3466
|
+
};
|
|
3467
|
+
let preview = null;
|
|
3468
|
+
const show = () => {
|
|
3469
|
+
if (!guide) return;
|
|
3470
|
+
preview?.close();
|
|
3471
|
+
preview = previewGuide({
|
|
3472
|
+
guideType: guide.guideType,
|
|
3473
|
+
guideSteps: guide.guideSteps,
|
|
3474
|
+
activationRules: guide.activationRules,
|
|
3475
|
+
guideName: guide.guideName,
|
|
3476
|
+
formSubmit: submitToBackend,
|
|
3477
|
+
formSubmitNote: "Vista previa: guardado en TU usuario para probar el flujo."
|
|
3478
|
+
});
|
|
3479
|
+
};
|
|
3480
|
+
const chip = mountChip({
|
|
3481
|
+
label: guide ? `Vista previa: ${guide.guideName}` : "Link de preview inv\xE1lido o vencido",
|
|
3482
|
+
error: !guide,
|
|
3483
|
+
onReshow: guide ? show : null,
|
|
3484
|
+
onClose: () => handle.teardown()
|
|
3485
|
+
});
|
|
3486
|
+
const handle = {
|
|
3487
|
+
teardown() {
|
|
3488
|
+
preview?.close();
|
|
3489
|
+
preview = null;
|
|
3490
|
+
chip.remove();
|
|
3491
|
+
if (active === handle) active = null;
|
|
3492
|
+
}
|
|
3493
|
+
};
|
|
3494
|
+
active = handle;
|
|
3495
|
+
show();
|
|
3496
|
+
return handle;
|
|
3497
|
+
}
|
|
3498
|
+
function mountChip(opts) {
|
|
3499
|
+
const host = document.createElement("div");
|
|
3500
|
+
host.setAttribute("data-veo-preview-chip", "");
|
|
3501
|
+
const root = host.attachShadow({ mode: "open" });
|
|
3502
|
+
const style = document.createElement("style");
|
|
3503
|
+
style.textContent = `
|
|
3504
|
+
.chip { position: fixed; bottom: 16px; right: 16px; z-index: 2147483647;
|
|
3505
|
+
display: flex; align-items: center; gap: 8px; padding: 8px 12px;
|
|
3506
|
+
background: #111827; color: #f9fafb; border-radius: 9999px;
|
|
3507
|
+
font: 500 12px/1.2 system-ui, sans-serif; box-shadow: 0 4px 12px rgba(0,0,0,.25); }
|
|
3508
|
+
.chip.error { background: #7f1d1d; }
|
|
3509
|
+
.label { max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3510
|
+
button { all: unset; cursor: pointer; padding: 2px 6px; border-radius: 9999px;
|
|
3511
|
+
font-size: 12px; line-height: 1; }
|
|
3512
|
+
button:hover { background: rgba(255,255,255,.15); }
|
|
3513
|
+
`;
|
|
3514
|
+
root.appendChild(style);
|
|
3515
|
+
const chip = document.createElement("div");
|
|
3516
|
+
chip.className = opts.error ? "chip error" : "chip";
|
|
3517
|
+
const label = document.createElement("span");
|
|
3518
|
+
label.className = "label";
|
|
3519
|
+
label.textContent = opts.label;
|
|
3520
|
+
chip.appendChild(label);
|
|
3521
|
+
if (opts.onReshow) {
|
|
3522
|
+
const reshow = document.createElement("button");
|
|
3523
|
+
reshow.textContent = "\u21BB";
|
|
3524
|
+
reshow.title = "Volver a mostrar la gu\xEDa";
|
|
3525
|
+
reshow.addEventListener("click", opts.onReshow);
|
|
3526
|
+
chip.appendChild(reshow);
|
|
3527
|
+
}
|
|
3528
|
+
const close = document.createElement("button");
|
|
3529
|
+
close.textContent = "\u2715";
|
|
3530
|
+
close.title = "Salir de la vista previa";
|
|
3531
|
+
close.addEventListener("click", opts.onClose);
|
|
3532
|
+
chip.appendChild(close);
|
|
3533
|
+
root.appendChild(chip);
|
|
3534
|
+
document.body.appendChild(host);
|
|
3535
|
+
return { remove: () => host.remove() };
|
|
3536
|
+
}
|
|
3537
|
+
|
|
2447
3538
|
// src/plugins/guides/index.ts
|
|
2448
3539
|
function guidesPlugin(config) {
|
|
2449
3540
|
return definePlugin({
|
|
@@ -2458,79 +3549,46 @@ function guidesPlugin(config) {
|
|
|
2458
3549
|
client.on("pageview", ({ url, sessionId }) => {
|
|
2459
3550
|
void controller.checkGuides(url, sessionId);
|
|
2460
3551
|
});
|
|
2461
|
-
|
|
2462
|
-
});
|
|
2463
|
-
}
|
|
2464
|
-
|
|
2465
|
-
// src/plugins/spa-router.ts
|
|
2466
|
-
function spaRouterPlugin() {
|
|
2467
|
-
return definePlugin({
|
|
2468
|
-
name: "spa-router",
|
|
2469
|
-
install(client) {
|
|
2470
|
-
if (!hasWindow()) return;
|
|
2471
|
-
client.pageview();
|
|
2472
|
-
const fire = () => {
|
|
2473
|
-
queueMicrotask(() => client.pageview());
|
|
2474
|
-
};
|
|
2475
|
-
const origPushState = window.history.pushState;
|
|
2476
|
-
const origReplaceState = window.history.replaceState;
|
|
2477
|
-
window.history.pushState = function(...args) {
|
|
2478
|
-
origPushState.apply(this, args);
|
|
2479
|
-
fire();
|
|
2480
|
-
};
|
|
2481
|
-
window.history.replaceState = function(...args) {
|
|
2482
|
-
origReplaceState.apply(this, args);
|
|
2483
|
-
fire();
|
|
2484
|
-
};
|
|
2485
|
-
window.addEventListener("popstate", fire);
|
|
2486
|
-
window.addEventListener("hashchange", fire);
|
|
3552
|
+
void controller.checkGuides(window.location.href, client.getSessionId());
|
|
2487
3553
|
}
|
|
2488
3554
|
});
|
|
2489
3555
|
}
|
|
2490
3556
|
|
|
2491
3557
|
// src/index.ts
|
|
2492
3558
|
function init(config) {
|
|
2493
|
-
const
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
3559
|
+
const apiUrl = resolveApiUrl(config);
|
|
3560
|
+
return setupClient(config, (client) => {
|
|
3561
|
+
const previewToken = getPreviewToken();
|
|
3562
|
+
if (config.guides === true && !isBuilderMode() && previewToken === null) {
|
|
3563
|
+
client.use(
|
|
3564
|
+
guidesPlugin({
|
|
3565
|
+
apiUrl,
|
|
3566
|
+
apiKey: config.apiKey,
|
|
3567
|
+
...config.debug !== void 0 ? { debug: config.debug } : {}
|
|
3568
|
+
})
|
|
3569
|
+
);
|
|
3570
|
+
}
|
|
3571
|
+
if (previewToken !== null) {
|
|
3572
|
+
void runPreviewMode({
|
|
2503
3573
|
apiKey: config.apiKey,
|
|
3574
|
+
apiUrl,
|
|
3575
|
+
token: previewToken,
|
|
2504
3576
|
...config.debug !== void 0 ? { debug: config.debug } : {}
|
|
2505
|
-
})
|
|
2506
|
-
);
|
|
2507
|
-
}
|
|
2508
|
-
if (typeof window !== "undefined") {
|
|
2509
|
-
const stub = window.veo;
|
|
2510
|
-
const pending = stub?._q ?? [];
|
|
2511
|
-
window.veo = client;
|
|
2512
|
-
for (const entry of pending) {
|
|
2513
|
-
const [method, args] = entry;
|
|
2514
|
-
if (method === "init") continue;
|
|
2515
|
-
const fn = client[method];
|
|
2516
|
-
if (typeof fn === "function") {
|
|
2517
|
-
try {
|
|
2518
|
-
fn.apply(client, Array.from(args));
|
|
2519
|
-
} catch (err) {
|
|
2520
|
-
if (config.debug) console.error("[veo] drain error:", err);
|
|
2521
|
-
}
|
|
2522
|
-
}
|
|
3577
|
+
});
|
|
2523
3578
|
}
|
|
2524
|
-
|
|
2525
|
-
|
|
3579
|
+
client.preview = previewGuide;
|
|
3580
|
+
client.closePreview = closeGuidePreview;
|
|
3581
|
+
});
|
|
2526
3582
|
}
|
|
2527
3583
|
|
|
2528
3584
|
exports.Client = Client;
|
|
2529
3585
|
exports.SDK_VERSION = SDK_VERSION;
|
|
2530
3586
|
exports.autocapturePlugin = autocapturePlugin;
|
|
3587
|
+
exports.closeGuidePreview = closeGuidePreview;
|
|
2531
3588
|
exports.definePlugin = definePlugin;
|
|
2532
3589
|
exports.guidesPlugin = guidesPlugin;
|
|
2533
3590
|
exports.init = init;
|
|
3591
|
+
exports.previewGuide = previewGuide;
|
|
2534
3592
|
exports.spaRouterPlugin = spaRouterPlugin;
|
|
2535
3593
|
//# sourceMappingURL=index.cjs.map
|
|
2536
3594
|
//# sourceMappingURL=index.cjs.map
|