@magicfeedback/popup-sdk 1.1.3 → 1.1.5
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/dist/assets/assets/style.css +10 -1
- package/dist/{chunk-6WTOQGET.mjs → chunk-7TOLYAFL.mjs} +35 -2
- package/dist/chunk-7TOLYAFL.mjs.map +1 -0
- package/dist/{chunk-IOAZQPZE.mjs → chunk-LSTKGFU7.mjs} +35 -2
- package/dist/chunk-LSTKGFU7.mjs.map +1 -0
- package/dist/index.d.mts +38 -2
- package/dist/index.d.ts +38 -2
- package/dist/index.js +98 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +34 -8
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +34 -1
- package/dist/react-native.d.ts +34 -1
- package/dist/react-native.js +97 -6
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +33 -8
- package/dist/react-native.mjs.map +1 -1
- package/dist/{renderPopup-YLFRGPQN.mjs → renderPopup-MES7IINW.mjs} +34 -3
- package/dist/renderPopup-MES7IINW.mjs.map +1 -0
- package/dist/{renderPopup-KBDWWSUU.mjs → renderPopup-YNH2TD3B.mjs} +34 -3
- package/dist/renderPopup-YNH2TD3B.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-6WTOQGET.mjs.map +0 -1
- package/dist/chunk-IOAZQPZE.mjs.map +0 -1
- package/dist/renderPopup-KBDWWSUU.mjs.map +0 -1
- package/dist/renderPopup-YLFRGPQN.mjs.map +0 -1
|
@@ -138,13 +138,22 @@
|
|
|
138
138
|
padding: 0;
|
|
139
139
|
font-weight: 900;
|
|
140
140
|
color: #222;
|
|
141
|
-
font-family: 'Montserrat';
|
|
141
|
+
font-family: var(--deepdots-font, 'Montserrat');
|
|
142
142
|
font-size: 18px;
|
|
143
143
|
text-transform: uppercase;
|
|
144
144
|
text-align: center;
|
|
145
145
|
margin-bottom: 40px;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/* Los controles de formulario no heredan font-family por defecto: cuando hay
|
|
149
|
+
fuente custom (clase deepdots-has-font), los forzamos a usar la del contenedor. */
|
|
150
|
+
.deepdots-popup.deepdots-has-font button,
|
|
151
|
+
.deepdots-popup.deepdots-has-font input,
|
|
152
|
+
.deepdots-popup.deepdots-has-font textarea,
|
|
153
|
+
.deepdots-popup.deepdots-has-font select {
|
|
154
|
+
font-family: var(--deepdots-font, inherit);
|
|
155
|
+
}
|
|
156
|
+
|
|
148
157
|
.deepdots-popup h6 {
|
|
149
158
|
margin-bottom: 1rem;
|
|
150
159
|
}
|
|
@@ -129,6 +129,37 @@ var TrackingManager = class {
|
|
|
129
129
|
}
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
+
// src/ui/font.ts
|
|
133
|
+
var FORMAT_BY_EXT = {
|
|
134
|
+
woff2: "woff2",
|
|
135
|
+
woff: "woff",
|
|
136
|
+
ttf: "truetype",
|
|
137
|
+
otf: "opentype"
|
|
138
|
+
};
|
|
139
|
+
function fontFormatFromUrl(url) {
|
|
140
|
+
const clean = url.split("?")[0].split("#")[0];
|
|
141
|
+
const ext = clean.slice(clean.lastIndexOf(".") + 1).toLowerCase();
|
|
142
|
+
return FORMAT_BY_EXT[ext];
|
|
143
|
+
}
|
|
144
|
+
function sanitizeFamily(family) {
|
|
145
|
+
return family.replace(/[^A-Za-z0-9 ._-]/g, "").trim();
|
|
146
|
+
}
|
|
147
|
+
function isSafeFontUrl(url) {
|
|
148
|
+
if (!/^(https?:|data:)/i.test(url)) return false;
|
|
149
|
+
return !/[\x00-\x20"<>\\]/.test(url);
|
|
150
|
+
}
|
|
151
|
+
function buildFontFamilyValue(family) {
|
|
152
|
+
const safe = sanitizeFamily(family);
|
|
153
|
+
return `"${safe}", -apple-system, system-ui, sans-serif`;
|
|
154
|
+
}
|
|
155
|
+
function buildFontFaceCss(family, url) {
|
|
156
|
+
if (!url || !isSafeFontUrl(url)) return "";
|
|
157
|
+
const safe = sanitizeFamily(family);
|
|
158
|
+
const fmt = fontFormatFromUrl(url);
|
|
159
|
+
const src = fmt ? `url("${url}") format("${fmt}")` : `url("${url}")`;
|
|
160
|
+
return `@font-face{font-family:"${safe}";src:${src};font-display:swap;}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
132
163
|
export {
|
|
133
164
|
__commonJS,
|
|
134
165
|
__toESM,
|
|
@@ -136,6 +167,8 @@ export {
|
|
|
136
167
|
createDefaultStorage,
|
|
137
168
|
STORAGE_KEYS,
|
|
138
169
|
buildSurveyIdentity,
|
|
139
|
-
TrackingManager
|
|
170
|
+
TrackingManager,
|
|
171
|
+
buildFontFamilyValue,
|
|
172
|
+
buildFontFaceCss
|
|
140
173
|
};
|
|
141
|
-
//# sourceMappingURL=chunk-
|
|
174
|
+
//# sourceMappingURL=chunk-7TOLYAFL.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tracking/tracking-manager.ts","../src/ui/font.ts"],"sourcesContent":["/**\n * Identidad + sesión para el tracking de navegación (Fase 1).\n * Implementa el \"Contrato común Web+KMP\".\n *\n * Modelo: el SDK gestiona el `user_id` (persistente). El `session_id` es propiedad\n * del BACKEND: llega en la respuesta de `POST /sdk/popups` y se cachea; el backend\n * cose sesiones por `user_id` + ventana. El SDK NO genera ni expira sesiones.\n *\n * Sin dependencias de DOM: recibe el storage y (opcionalmente) el reloj y el\n * generador de uuid por inyección. El binding a `localStorage` vive en `createDefaultStorage`.\n */\n\n/** Store clave/valor mínimo (espejo conceptual del `KeyValueStorage` de KMP). */\nexport interface KeyValueStorage {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\n/** Impl en memoria — usada en tests y como fallback sin `localStorage`. */\nexport class InMemoryStorage implements KeyValueStorage {\n private map = new Map<string, string>();\n getItem(key: string): string | null {\n return this.map.has(key) ? (this.map.get(key) as string) : null;\n }\n setItem(key: string, value: string): void {\n this.map.set(key, value);\n }\n removeItem(key: string): void {\n this.map.delete(key);\n }\n}\n\n/** Storage por defecto en Web: `localStorage` si está disponible, si no in-memory. */\nexport function createDefaultStorage(): KeyValueStorage {\n try {\n if (typeof window !== 'undefined' && window.localStorage) {\n const ls = window.localStorage;\n const probe = '__deepdots_probe__';\n ls.setItem(probe, '1');\n ls.removeItem(probe);\n return {\n getItem: (k) => ls.getItem(k),\n setItem: (k, v) => ls.setItem(k, v),\n removeItem: (k) => ls.removeItem(k),\n };\n }\n } catch {\n // cae a in-memory\n }\n return new InMemoryStorage();\n}\n\n/** Claves de storage — namespace `deepdots.*`. Solo identidad (la sesión no se persiste). */\nexport const STORAGE_KEYS = {\n userId: 'deepdots.user_id',\n firstSeen: 'deepdots.user.first_seen',\n contactAttributes: 'deepdots.contact.attributes',\n} as const;\n\n/** Par clave/valor con el formato `NativeAnswer` que espera `@magicfeedback/native`. */\nexport interface IdentityAnswer {\n key: string;\n value: string[];\n}\n\n/**\n * Construye el `profile` y el `metadata` de identidad para inyectar en el survey\n * (`magicfeedback.form(appId, publicKey, profile, metadata)`), según el contrato §5.\n * `session_id` solo se incluye si el backend ya lo proveyó.\n */\nexport function buildSurveyIdentity(\n userId: string | null,\n sessionId: string | null,\n miniService: string | null = null,\n analyticsFeedbackSessionId: string | null = null,\n): { profile: IdentityAnswer[]; metadata: IdentityAnswer[] } {\n const profile: IdentityAnswer[] = userId ? [{ key: 'external-user-id', value: [userId] }] : [];\n const metadata: IdentityAnswer[] = [];\n if (sessionId) metadata.push({ key: 'session_id', value: [sessionId] });\n if (userId) metadata.push({ key: 'user_id', value: [userId] });\n if (miniService) metadata.push({ key: 'mini_service', value: [miniService] });\n if (analyticsFeedbackSessionId) metadata.push({ key: 'deepdots_analytics_feedback_session_id', value: [analyticsFeedbackSessionId] });\n return { profile, metadata };\n}\n\nexport interface TrackingManagerOptions {\n storage: KeyValueStorage;\n /** Id provisto por el cliente; si existe se usa y NO se persiste. */\n clientUserId?: string;\n /** Reloj inyectable (default `Date.now`). */\n now?: () => number;\n /** Generador de uuid inyectable (default uuid v4). */\n uuid?: () => string;\n /** Estado inicial del tracking (default `true`). */\n enabled?: boolean;\n}\n\nfunction defaultUuid(): string {\n const g = (typeof globalThis !== 'undefined' ? globalThis : {}) as {\n crypto?: { randomUUID?: () => string; getRandomValues?: (a: Uint8Array) => Uint8Array };\n };\n if (g.crypto?.randomUUID) return g.crypto.randomUUID();\n const bytes = new Uint8Array(16);\n if (g.crypto?.getRandomValues) {\n g.crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));\n return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex\n .slice(6, 8)\n .join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;\n}\n\nexport class TrackingManager {\n private storage: KeyValueStorage;\n private clientUserId?: string;\n private now: () => number;\n private uuid: () => string;\n private enabled: boolean;\n\n /** session_id cacheado, provisto por el backend (no se genera ni se persiste). */\n private sessionId: string | null = null;\n private userWasJustCreated = false;\n\n constructor(options: TrackingManagerOptions) {\n this.storage = options.storage;\n this.clientUserId = options.clientUserId;\n this.now = options.now ?? (() => Date.now());\n this.uuid = options.uuid ?? defaultUuid;\n this.enabled = options.enabled ?? true;\n }\n\n isTrackingEnabled(): boolean {\n return this.enabled;\n }\n\n setTrackingEnabled(enabled: boolean): void {\n this.enabled = enabled;\n }\n\n /** Resuelve el user_id según la regla: cliente > persistido > generar+persistir. */\n getUserId(): string | null {\n if (!this.enabled) return null;\n if (this.clientUserId) return this.clientUserId;\n\n const persisted = this.storage.getItem(STORAGE_KEYS.userId);\n if (persisted) return persisted;\n\n const id = this.uuid();\n this.storage.setItem(STORAGE_KEYS.userId, id);\n this.storage.setItem(STORAGE_KEYS.firstSeen, String(this.now()));\n this.userWasJustCreated = true;\n return id;\n }\n\n /** `true` solo en la primera sesión tras crear el user_id propio del SDK. */\n isNewUser(): boolean {\n return this.userWasJustCreated;\n }\n\n /** session_id actual provisto por el backend, o null si aún no llegó. */\n getSessionId(): string | null {\n if (!this.enabled) return null;\n return this.sessionId;\n }\n\n /** Cachea el session_id devuelto por el backend (respuesta de POST /sdk/popups). */\n setSessionId(sessionId: string | null): void {\n if (!this.enabled) return;\n this.sessionId = sessionId;\n }\n}\n","/**\n * Helpers puros (sin DOM) para la fuente personalizable del popup/survey.\n * Espejo exacto en KMP: `ui/Font.kt`. Cualquier cambio aquí se replica allí.\n */\n\nconst FORMAT_BY_EXT: Record<string, string> = {\n woff2: 'woff2',\n woff: 'woff',\n ttf: 'truetype',\n otf: 'opentype',\n};\n\n/** Deriva el `format()` del `@font-face` a partir de la extensión de la URL. */\nexport function fontFormatFromUrl(url: string): string | undefined {\n const clean = url.split('?')[0].split('#')[0];\n const ext = clean.slice(clean.lastIndexOf('.') + 1).toLowerCase();\n return FORMAT_BY_EXT[ext];\n}\n\n// family/url provienen de la API y se interpolan en CSS y en el <style> del HTML del WebView.\n// Saneamos para evitar romper el string/rule (CSS injection) o cerrar </style> (script injection).\nfunction sanitizeFamily(family: string): string {\n return family.replace(/[^A-Za-z0-9 ._-]/g, '').trim();\n}\n\nfunction isSafeFontUrl(url: string): boolean {\n // Solo http(s)/data y sin caracteres que rompan url(\"...\") o el <style>:\n // comillas, <>, backslash, y cualquier whitespace o carácter de control.\n if (!/^(https?:|data:)/i.test(url)) return false;\n // eslint-disable-next-line no-control-regex\n return !/[\\x00-\\x20\"<>\\\\]/.test(url);\n}\n\n/** Valor a aplicar en `font-family`: nombre custom + fallback de sistema. */\nexport function buildFontFamilyValue(family: string): string {\n const safe = sanitizeFamily(family);\n return `\"${safe}\", -apple-system, system-ui, sans-serif`;\n}\n\n/** CSS del `@font-face`. Devuelve \"\" si no hay url (o no es segura). */\nexport function buildFontFaceCss(family: string, url?: string): string {\n if (!url || !isSafeFontUrl(url)) return '';\n const safe = sanitizeFamily(family);\n const fmt = fontFormatFromUrl(url);\n const src = fmt ? `url(\"${url}\") format(\"${fmt}\")` : `url(\"${url}\")`;\n return `@font-face{font-family:\"${safe}\";src:${src};font-display:swap;}`;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -110,11 +110,44 @@ var TrackingManager = class {
|
|
|
110
110
|
}
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
+
// src/ui/font.ts
|
|
114
|
+
var FORMAT_BY_EXT = {
|
|
115
|
+
woff2: "woff2",
|
|
116
|
+
woff: "woff",
|
|
117
|
+
ttf: "truetype",
|
|
118
|
+
otf: "opentype"
|
|
119
|
+
};
|
|
120
|
+
function fontFormatFromUrl(url) {
|
|
121
|
+
const clean = url.split("?")[0].split("#")[0];
|
|
122
|
+
const ext = clean.slice(clean.lastIndexOf(".") + 1).toLowerCase();
|
|
123
|
+
return FORMAT_BY_EXT[ext];
|
|
124
|
+
}
|
|
125
|
+
function sanitizeFamily(family) {
|
|
126
|
+
return family.replace(/[^A-Za-z0-9 ._-]/g, "").trim();
|
|
127
|
+
}
|
|
128
|
+
function isSafeFontUrl(url) {
|
|
129
|
+
if (!/^(https?:|data:)/i.test(url)) return false;
|
|
130
|
+
return !/[\x00-\x20"<>\\]/.test(url);
|
|
131
|
+
}
|
|
132
|
+
function buildFontFamilyValue(family) {
|
|
133
|
+
const safe = sanitizeFamily(family);
|
|
134
|
+
return `"${safe}", -apple-system, system-ui, sans-serif`;
|
|
135
|
+
}
|
|
136
|
+
function buildFontFaceCss(family, url) {
|
|
137
|
+
if (!url || !isSafeFontUrl(url)) return "";
|
|
138
|
+
const safe = sanitizeFamily(family);
|
|
139
|
+
const fmt = fontFormatFromUrl(url);
|
|
140
|
+
const src = fmt ? `url("${url}") format("${fmt}")` : `url("${url}")`;
|
|
141
|
+
return `@font-face{font-family:"${safe}";src:${src};font-display:swap;}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
113
144
|
export {
|
|
114
145
|
__require,
|
|
115
146
|
createDefaultStorage,
|
|
116
147
|
STORAGE_KEYS,
|
|
117
148
|
buildSurveyIdentity,
|
|
118
|
-
TrackingManager
|
|
149
|
+
TrackingManager,
|
|
150
|
+
buildFontFamilyValue,
|
|
151
|
+
buildFontFaceCss
|
|
119
152
|
};
|
|
120
|
-
//# sourceMappingURL=chunk-
|
|
153
|
+
//# sourceMappingURL=chunk-LSTKGFU7.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tracking/tracking-manager.ts","../src/ui/font.ts"],"sourcesContent":["/**\n * Identidad + sesión para el tracking de navegación (Fase 1).\n * Implementa el \"Contrato común Web+KMP\".\n *\n * Modelo: el SDK gestiona el `user_id` (persistente). El `session_id` es propiedad\n * del BACKEND: llega en la respuesta de `POST /sdk/popups` y se cachea; el backend\n * cose sesiones por `user_id` + ventana. El SDK NO genera ni expira sesiones.\n *\n * Sin dependencias de DOM: recibe el storage y (opcionalmente) el reloj y el\n * generador de uuid por inyección. El binding a `localStorage` vive en `createDefaultStorage`.\n */\n\n/** Store clave/valor mínimo (espejo conceptual del `KeyValueStorage` de KMP). */\nexport interface KeyValueStorage {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\n/** Impl en memoria — usada en tests y como fallback sin `localStorage`. */\nexport class InMemoryStorage implements KeyValueStorage {\n private map = new Map<string, string>();\n getItem(key: string): string | null {\n return this.map.has(key) ? (this.map.get(key) as string) : null;\n }\n setItem(key: string, value: string): void {\n this.map.set(key, value);\n }\n removeItem(key: string): void {\n this.map.delete(key);\n }\n}\n\n/** Storage por defecto en Web: `localStorage` si está disponible, si no in-memory. */\nexport function createDefaultStorage(): KeyValueStorage {\n try {\n if (typeof window !== 'undefined' && window.localStorage) {\n const ls = window.localStorage;\n const probe = '__deepdots_probe__';\n ls.setItem(probe, '1');\n ls.removeItem(probe);\n return {\n getItem: (k) => ls.getItem(k),\n setItem: (k, v) => ls.setItem(k, v),\n removeItem: (k) => ls.removeItem(k),\n };\n }\n } catch {\n // cae a in-memory\n }\n return new InMemoryStorage();\n}\n\n/** Claves de storage — namespace `deepdots.*`. Solo identidad (la sesión no se persiste). */\nexport const STORAGE_KEYS = {\n userId: 'deepdots.user_id',\n firstSeen: 'deepdots.user.first_seen',\n contactAttributes: 'deepdots.contact.attributes',\n} as const;\n\n/** Par clave/valor con el formato `NativeAnswer` que espera `@magicfeedback/native`. */\nexport interface IdentityAnswer {\n key: string;\n value: string[];\n}\n\n/**\n * Construye el `profile` y el `metadata` de identidad para inyectar en el survey\n * (`magicfeedback.form(appId, publicKey, profile, metadata)`), según el contrato §5.\n * `session_id` solo se incluye si el backend ya lo proveyó.\n */\nexport function buildSurveyIdentity(\n userId: string | null,\n sessionId: string | null,\n miniService: string | null = null,\n analyticsFeedbackSessionId: string | null = null,\n): { profile: IdentityAnswer[]; metadata: IdentityAnswer[] } {\n const profile: IdentityAnswer[] = userId ? [{ key: 'external-user-id', value: [userId] }] : [];\n const metadata: IdentityAnswer[] = [];\n if (sessionId) metadata.push({ key: 'session_id', value: [sessionId] });\n if (userId) metadata.push({ key: 'user_id', value: [userId] });\n if (miniService) metadata.push({ key: 'mini_service', value: [miniService] });\n if (analyticsFeedbackSessionId) metadata.push({ key: 'deepdots_analytics_feedback_session_id', value: [analyticsFeedbackSessionId] });\n return { profile, metadata };\n}\n\nexport interface TrackingManagerOptions {\n storage: KeyValueStorage;\n /** Id provisto por el cliente; si existe se usa y NO se persiste. */\n clientUserId?: string;\n /** Reloj inyectable (default `Date.now`). */\n now?: () => number;\n /** Generador de uuid inyectable (default uuid v4). */\n uuid?: () => string;\n /** Estado inicial del tracking (default `true`). */\n enabled?: boolean;\n}\n\nfunction defaultUuid(): string {\n const g = (typeof globalThis !== 'undefined' ? globalThis : {}) as {\n crypto?: { randomUUID?: () => string; getRandomValues?: (a: Uint8Array) => Uint8Array };\n };\n if (g.crypto?.randomUUID) return g.crypto.randomUUID();\n const bytes = new Uint8Array(16);\n if (g.crypto?.getRandomValues) {\n g.crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));\n return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex\n .slice(6, 8)\n .join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;\n}\n\nexport class TrackingManager {\n private storage: KeyValueStorage;\n private clientUserId?: string;\n private now: () => number;\n private uuid: () => string;\n private enabled: boolean;\n\n /** session_id cacheado, provisto por el backend (no se genera ni se persiste). */\n private sessionId: string | null = null;\n private userWasJustCreated = false;\n\n constructor(options: TrackingManagerOptions) {\n this.storage = options.storage;\n this.clientUserId = options.clientUserId;\n this.now = options.now ?? (() => Date.now());\n this.uuid = options.uuid ?? defaultUuid;\n this.enabled = options.enabled ?? true;\n }\n\n isTrackingEnabled(): boolean {\n return this.enabled;\n }\n\n setTrackingEnabled(enabled: boolean): void {\n this.enabled = enabled;\n }\n\n /** Resuelve el user_id según la regla: cliente > persistido > generar+persistir. */\n getUserId(): string | null {\n if (!this.enabled) return null;\n if (this.clientUserId) return this.clientUserId;\n\n const persisted = this.storage.getItem(STORAGE_KEYS.userId);\n if (persisted) return persisted;\n\n const id = this.uuid();\n this.storage.setItem(STORAGE_KEYS.userId, id);\n this.storage.setItem(STORAGE_KEYS.firstSeen, String(this.now()));\n this.userWasJustCreated = true;\n return id;\n }\n\n /** `true` solo en la primera sesión tras crear el user_id propio del SDK. */\n isNewUser(): boolean {\n return this.userWasJustCreated;\n }\n\n /** session_id actual provisto por el backend, o null si aún no llegó. */\n getSessionId(): string | null {\n if (!this.enabled) return null;\n return this.sessionId;\n }\n\n /** Cachea el session_id devuelto por el backend (respuesta de POST /sdk/popups). */\n setSessionId(sessionId: string | null): void {\n if (!this.enabled) return;\n this.sessionId = sessionId;\n }\n}\n","/**\n * Helpers puros (sin DOM) para la fuente personalizable del popup/survey.\n * Espejo exacto en KMP: `ui/Font.kt`. Cualquier cambio aquí se replica allí.\n */\n\nconst FORMAT_BY_EXT: Record<string, string> = {\n woff2: 'woff2',\n woff: 'woff',\n ttf: 'truetype',\n otf: 'opentype',\n};\n\n/** Deriva el `format()` del `@font-face` a partir de la extensión de la URL. */\nexport function fontFormatFromUrl(url: string): string | undefined {\n const clean = url.split('?')[0].split('#')[0];\n const ext = clean.slice(clean.lastIndexOf('.') + 1).toLowerCase();\n return FORMAT_BY_EXT[ext];\n}\n\n// family/url provienen de la API y se interpolan en CSS y en el <style> del HTML del WebView.\n// Saneamos para evitar romper el string/rule (CSS injection) o cerrar </style> (script injection).\nfunction sanitizeFamily(family: string): string {\n return family.replace(/[^A-Za-z0-9 ._-]/g, '').trim();\n}\n\nfunction isSafeFontUrl(url: string): boolean {\n // Solo http(s)/data y sin caracteres que rompan url(\"...\") o el <style>:\n // comillas, <>, backslash, y cualquier whitespace o carácter de control.\n if (!/^(https?:|data:)/i.test(url)) return false;\n // eslint-disable-next-line no-control-regex\n return !/[\\x00-\\x20\"<>\\\\]/.test(url);\n}\n\n/** Valor a aplicar en `font-family`: nombre custom + fallback de sistema. */\nexport function buildFontFamilyValue(family: string): string {\n const safe = sanitizeFamily(family);\n return `\"${safe}\", -apple-system, system-ui, sans-serif`;\n}\n\n/** CSS del `@font-face`. Devuelve \"\" si no hay url (o no es segura). */\nexport function buildFontFaceCss(family: string, url?: string): string {\n if (!url || !isSafeFontUrl(url)) return '';\n const safe = sanitizeFamily(family);\n const fmt = fontFormatFromUrl(url);\n const src = fmt ? `url(\"${url}\") format(\"${fmt}\")` : `url(\"${url}\")`;\n return `@font-face{font-family:\"${safe}\";src:${src};font-display:swap;}`;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.mts
CHANGED
|
@@ -267,12 +267,21 @@ interface PopupActions {
|
|
|
267
267
|
start?: PopupActionStart;
|
|
268
268
|
back?: PopupActionDecline;
|
|
269
269
|
}
|
|
270
|
+
/** Fuente personalizable del popup + survey (viene de la API por popup) */
|
|
271
|
+
interface PopupFont {
|
|
272
|
+
/** Nombre de la familia. Ej: "Inter" (nombre limpio, NO un stack). */
|
|
273
|
+
family: string;
|
|
274
|
+
/** Opcional: URL a un archivo de fuente (woff2/ttf/otf) para armar un @font-face. */
|
|
275
|
+
url?: string;
|
|
276
|
+
}
|
|
270
277
|
/** Configurable popup styles */
|
|
271
278
|
interface PopupStyle {
|
|
272
279
|
theme: 'light' | 'dark';
|
|
273
280
|
position: 'bottom' | 'bottom-right' | 'bottom-left' | 'top' | 'top-right' | 'top-left' | 'center';
|
|
274
281
|
/** @deprecated Not used by the SDK; kept only for API compatibility */
|
|
275
282
|
imageUrl?: string | null;
|
|
283
|
+
/** Fuente personalizada. Si falta, se mantiene el comportamiento actual. */
|
|
284
|
+
font?: PopupFont;
|
|
276
285
|
}
|
|
277
286
|
/** Segments/targeting rules for showing the popup */
|
|
278
287
|
interface PopupSegments {
|
|
@@ -431,6 +440,30 @@ declare class CrashReporter {
|
|
|
431
440
|
_persistForTest(error: unknown): void;
|
|
432
441
|
}
|
|
433
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Messaging (#18–22): tracking de notificaciones del host (push / in-app).
|
|
445
|
+
* Un único evento reservado `deepdots_message` con un campo `stage` discriminador;
|
|
446
|
+
* el funnel se correlaciona por `message_id` y se agrupa por `message_title`.
|
|
447
|
+
*/
|
|
448
|
+
type MessageStage = 'delivered' | 'clicked' | 'converted';
|
|
449
|
+
interface TrackMessageOptions {
|
|
450
|
+
/** Identificador del mensaje/notificación — correlaciona el funnel delivered→clicked→converted. */
|
|
451
|
+
id: string;
|
|
452
|
+
/** Título del mensaje — dimensión de agrupación de #18–22. */
|
|
453
|
+
title: string;
|
|
454
|
+
/** Canal de entrega. */
|
|
455
|
+
channel: 'push' | 'in_app';
|
|
456
|
+
/** Nombre de la campaña (opcional). */
|
|
457
|
+
campaign?: string;
|
|
458
|
+
/** Valor de conversión (opcional, típico en `stage: 'converted'`). */
|
|
459
|
+
value?: number;
|
|
460
|
+
currency?: string;
|
|
461
|
+
/** Parámetros libres adicionales. */
|
|
462
|
+
params?: Record<string, unknown>;
|
|
463
|
+
}
|
|
464
|
+
/** Construye los params del evento `deepdots_message` (omite opcionales ausentes). */
|
|
465
|
+
declare function buildMessageParams(stage: MessageStage, o: TrackMessageOptions): Record<string, unknown>;
|
|
466
|
+
|
|
434
467
|
/**
|
|
435
468
|
* Main class for managing survey popups
|
|
436
469
|
*/
|
|
@@ -502,6 +535,8 @@ declare class DeepdotsPopups {
|
|
|
502
535
|
trackFindabilityFriction(frictionTopic: string, params?: Record<string, unknown>): void;
|
|
503
536
|
/** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
|
|
504
537
|
trackFunnelStep(funnel: string, step: string, taskId: string, params?: Record<string, unknown>): void;
|
|
538
|
+
/** Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app). No-op si tracking off. */
|
|
539
|
+
trackMessage(stage: MessageStage, options: TrackMessageOptions): void;
|
|
505
540
|
/**
|
|
506
541
|
* Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
|
|
507
542
|
* El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.
|
|
@@ -632,7 +667,7 @@ declare class ReactNativePopupRenderer implements PopupRenderer {
|
|
|
632
667
|
private partialEmitted;
|
|
633
668
|
constructor(options?: ReactNativeRendererOptions);
|
|
634
669
|
init(): void;
|
|
635
|
-
show(surveyId: string, productId: string, _actions: PopupActions | undefined, emit: EmitFn, onClose: () => void, env?: string, userId?: string,
|
|
670
|
+
show(surveyId: string, productId: string, _actions: PopupActions | undefined, emit: EmitFn, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string): void;
|
|
636
671
|
hide(): void;
|
|
637
672
|
/**
|
|
638
673
|
* El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
|
|
@@ -658,6 +693,7 @@ interface BuildSurveyHtmlOptions {
|
|
|
658
693
|
profile?: IdentityAnswer[];
|
|
659
694
|
metadata?: IdentityAnswer[];
|
|
660
695
|
version?: string;
|
|
696
|
+
font?: PopupFont;
|
|
661
697
|
}
|
|
662
698
|
declare function buildSurveyHtml(opts: BuildSurveyHtmlOptions): string;
|
|
663
699
|
|
|
@@ -709,4 +745,4 @@ declare function collectRnDevice(d: DeviceInfoLike): DeviceInfo;
|
|
|
709
745
|
*/
|
|
710
746
|
declare function setupReactNative(sdk: DeepdotsPopups, config: DeepdotsInitParams, deps?: ReactNativeSetupDeps): () => void;
|
|
711
747
|
|
|
712
|
-
export { type AnalyticsFeedbackBody, type AnalyticsKeys, type AppStateLike, BrowserPopupRenderer, type ContactAttributeValue, type ContactAttributes, type ContactBody, ContactManager, type CrashRecord, CrashReporter, type CrashSeverity, type DeepdotsConfig, type DeepdotsEvent, type DeepdotsEventType, DeepdotsPopups, type DeviceInfo, type DeviceInfoLike, type DeviceSnapshot, type EventListener, type FeedbackKV, InMemoryStorage, type KeyValueStorage, type MmkvLike, NoopPopupRenderer, POPUP_TRIGGER_CONDITION_STATUSES, type PopupDefinition, type PopupRenderer, type PopupTrigger, type PopupTriggerCondition, type PopupTriggerConditionStatus, type ReactNativeErrorUtils, ReactNativePopupRenderer, type ReactNativeRendererOptions, type ReactNativeSetupDeps, type ReactNativeSurveyPayload, type ReportErrorOptions, type TriggerConfig, buildAnalyticsFeedbackBody, buildSurveyHtml, collectRnDevice, crashRecordToParams, createDefaultRenderer, createFeedbackSink, mmkvStorage, setupReactNative };
|
|
748
|
+
export { type AnalyticsFeedbackBody, type AnalyticsKeys, type AppStateLike, BrowserPopupRenderer, type ContactAttributeValue, type ContactAttributes, type ContactBody, ContactManager, type CrashRecord, CrashReporter, type CrashSeverity, type DeepdotsConfig, type DeepdotsEvent, type DeepdotsEventType, DeepdotsPopups, type DeviceInfo, type DeviceInfoLike, type DeviceSnapshot, type EventListener, type FeedbackKV, InMemoryStorage, type KeyValueStorage, type MessageStage, type MmkvLike, NoopPopupRenderer, POPUP_TRIGGER_CONDITION_STATUSES, type PopupDefinition, type PopupRenderer, type PopupTrigger, type PopupTriggerCondition, type PopupTriggerConditionStatus, type ReactNativeErrorUtils, ReactNativePopupRenderer, type ReactNativeRendererOptions, type ReactNativeSetupDeps, type ReactNativeSurveyPayload, type ReportErrorOptions, type TrackMessageOptions, type TriggerConfig, buildAnalyticsFeedbackBody, buildMessageParams, buildSurveyHtml, collectRnDevice, crashRecordToParams, createDefaultRenderer, createFeedbackSink, mmkvStorage, setupReactNative };
|
package/dist/index.d.ts
CHANGED
|
@@ -267,12 +267,21 @@ interface PopupActions {
|
|
|
267
267
|
start?: PopupActionStart;
|
|
268
268
|
back?: PopupActionDecline;
|
|
269
269
|
}
|
|
270
|
+
/** Fuente personalizable del popup + survey (viene de la API por popup) */
|
|
271
|
+
interface PopupFont {
|
|
272
|
+
/** Nombre de la familia. Ej: "Inter" (nombre limpio, NO un stack). */
|
|
273
|
+
family: string;
|
|
274
|
+
/** Opcional: URL a un archivo de fuente (woff2/ttf/otf) para armar un @font-face. */
|
|
275
|
+
url?: string;
|
|
276
|
+
}
|
|
270
277
|
/** Configurable popup styles */
|
|
271
278
|
interface PopupStyle {
|
|
272
279
|
theme: 'light' | 'dark';
|
|
273
280
|
position: 'bottom' | 'bottom-right' | 'bottom-left' | 'top' | 'top-right' | 'top-left' | 'center';
|
|
274
281
|
/** @deprecated Not used by the SDK; kept only for API compatibility */
|
|
275
282
|
imageUrl?: string | null;
|
|
283
|
+
/** Fuente personalizada. Si falta, se mantiene el comportamiento actual. */
|
|
284
|
+
font?: PopupFont;
|
|
276
285
|
}
|
|
277
286
|
/** Segments/targeting rules for showing the popup */
|
|
278
287
|
interface PopupSegments {
|
|
@@ -431,6 +440,30 @@ declare class CrashReporter {
|
|
|
431
440
|
_persistForTest(error: unknown): void;
|
|
432
441
|
}
|
|
433
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Messaging (#18–22): tracking de notificaciones del host (push / in-app).
|
|
445
|
+
* Un único evento reservado `deepdots_message` con un campo `stage` discriminador;
|
|
446
|
+
* el funnel se correlaciona por `message_id` y se agrupa por `message_title`.
|
|
447
|
+
*/
|
|
448
|
+
type MessageStage = 'delivered' | 'clicked' | 'converted';
|
|
449
|
+
interface TrackMessageOptions {
|
|
450
|
+
/** Identificador del mensaje/notificación — correlaciona el funnel delivered→clicked→converted. */
|
|
451
|
+
id: string;
|
|
452
|
+
/** Título del mensaje — dimensión de agrupación de #18–22. */
|
|
453
|
+
title: string;
|
|
454
|
+
/** Canal de entrega. */
|
|
455
|
+
channel: 'push' | 'in_app';
|
|
456
|
+
/** Nombre de la campaña (opcional). */
|
|
457
|
+
campaign?: string;
|
|
458
|
+
/** Valor de conversión (opcional, típico en `stage: 'converted'`). */
|
|
459
|
+
value?: number;
|
|
460
|
+
currency?: string;
|
|
461
|
+
/** Parámetros libres adicionales. */
|
|
462
|
+
params?: Record<string, unknown>;
|
|
463
|
+
}
|
|
464
|
+
/** Construye los params del evento `deepdots_message` (omite opcionales ausentes). */
|
|
465
|
+
declare function buildMessageParams(stage: MessageStage, o: TrackMessageOptions): Record<string, unknown>;
|
|
466
|
+
|
|
434
467
|
/**
|
|
435
468
|
* Main class for managing survey popups
|
|
436
469
|
*/
|
|
@@ -502,6 +535,8 @@ declare class DeepdotsPopups {
|
|
|
502
535
|
trackFindabilityFriction(frictionTopic: string, params?: Record<string, unknown>): void;
|
|
503
536
|
/** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
|
|
504
537
|
trackFunnelStep(funnel: string, step: string, taskId: string, params?: Record<string, unknown>): void;
|
|
538
|
+
/** Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app). No-op si tracking off. */
|
|
539
|
+
trackMessage(stage: MessageStage, options: TrackMessageOptions): void;
|
|
505
540
|
/**
|
|
506
541
|
* Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
|
|
507
542
|
* El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.
|
|
@@ -632,7 +667,7 @@ declare class ReactNativePopupRenderer implements PopupRenderer {
|
|
|
632
667
|
private partialEmitted;
|
|
633
668
|
constructor(options?: ReactNativeRendererOptions);
|
|
634
669
|
init(): void;
|
|
635
|
-
show(surveyId: string, productId: string, _actions: PopupActions | undefined, emit: EmitFn, onClose: () => void, env?: string, userId?: string,
|
|
670
|
+
show(surveyId: string, productId: string, _actions: PopupActions | undefined, emit: EmitFn, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string): void;
|
|
636
671
|
hide(): void;
|
|
637
672
|
/**
|
|
638
673
|
* El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
|
|
@@ -658,6 +693,7 @@ interface BuildSurveyHtmlOptions {
|
|
|
658
693
|
profile?: IdentityAnswer[];
|
|
659
694
|
metadata?: IdentityAnswer[];
|
|
660
695
|
version?: string;
|
|
696
|
+
font?: PopupFont;
|
|
661
697
|
}
|
|
662
698
|
declare function buildSurveyHtml(opts: BuildSurveyHtmlOptions): string;
|
|
663
699
|
|
|
@@ -709,4 +745,4 @@ declare function collectRnDevice(d: DeviceInfoLike): DeviceInfo;
|
|
|
709
745
|
*/
|
|
710
746
|
declare function setupReactNative(sdk: DeepdotsPopups, config: DeepdotsInitParams, deps?: ReactNativeSetupDeps): () => void;
|
|
711
747
|
|
|
712
|
-
export { type AnalyticsFeedbackBody, type AnalyticsKeys, type AppStateLike, BrowserPopupRenderer, type ContactAttributeValue, type ContactAttributes, type ContactBody, ContactManager, type CrashRecord, CrashReporter, type CrashSeverity, type DeepdotsConfig, type DeepdotsEvent, type DeepdotsEventType, DeepdotsPopups, type DeviceInfo, type DeviceInfoLike, type DeviceSnapshot, type EventListener, type FeedbackKV, InMemoryStorage, type KeyValueStorage, type MmkvLike, NoopPopupRenderer, POPUP_TRIGGER_CONDITION_STATUSES, type PopupDefinition, type PopupRenderer, type PopupTrigger, type PopupTriggerCondition, type PopupTriggerConditionStatus, type ReactNativeErrorUtils, ReactNativePopupRenderer, type ReactNativeRendererOptions, type ReactNativeSetupDeps, type ReactNativeSurveyPayload, type ReportErrorOptions, type TriggerConfig, buildAnalyticsFeedbackBody, buildSurveyHtml, collectRnDevice, crashRecordToParams, createDefaultRenderer, createFeedbackSink, mmkvStorage, setupReactNative };
|
|
748
|
+
export { type AnalyticsFeedbackBody, type AnalyticsKeys, type AppStateLike, BrowserPopupRenderer, type ContactAttributeValue, type ContactAttributes, type ContactBody, ContactManager, type CrashRecord, CrashReporter, type CrashSeverity, type DeepdotsConfig, type DeepdotsEvent, type DeepdotsEventType, DeepdotsPopups, type DeviceInfo, type DeviceInfoLike, type DeviceSnapshot, type EventListener, type FeedbackKV, InMemoryStorage, type KeyValueStorage, type MessageStage, type MmkvLike, NoopPopupRenderer, POPUP_TRIGGER_CONDITION_STATUSES, type PopupDefinition, type PopupRenderer, type PopupTrigger, type PopupTriggerCondition, type PopupTriggerConditionStatus, type ReactNativeErrorUtils, ReactNativePopupRenderer, type ReactNativeRendererOptions, type ReactNativeSetupDeps, type ReactNativeSurveyPayload, type ReportErrorOptions, type TrackMessageOptions, type TriggerConfig, buildAnalyticsFeedbackBody, buildMessageParams, buildSurveyHtml, collectRnDevice, crashRecordToParams, createDefaultRenderer, createFeedbackSink, mmkvStorage, setupReactNative };
|
package/dist/index.js
CHANGED
|
@@ -144,6 +144,43 @@ var init_tracking_manager = __esm({
|
|
|
144
144
|
}
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
+
// src/ui/font.ts
|
|
148
|
+
function fontFormatFromUrl(url) {
|
|
149
|
+
const clean = url.split("?")[0].split("#")[0];
|
|
150
|
+
const ext = clean.slice(clean.lastIndexOf(".") + 1).toLowerCase();
|
|
151
|
+
return FORMAT_BY_EXT[ext];
|
|
152
|
+
}
|
|
153
|
+
function sanitizeFamily(family) {
|
|
154
|
+
return family.replace(/[^A-Za-z0-9 ._-]/g, "").trim();
|
|
155
|
+
}
|
|
156
|
+
function isSafeFontUrl(url) {
|
|
157
|
+
if (!/^(https?:|data:)/i.test(url)) return false;
|
|
158
|
+
return !/[\x00-\x20"<>\\]/.test(url);
|
|
159
|
+
}
|
|
160
|
+
function buildFontFamilyValue(family) {
|
|
161
|
+
const safe = sanitizeFamily(family);
|
|
162
|
+
return `"${safe}", -apple-system, system-ui, sans-serif`;
|
|
163
|
+
}
|
|
164
|
+
function buildFontFaceCss(family, url) {
|
|
165
|
+
if (!url || !isSafeFontUrl(url)) return "";
|
|
166
|
+
const safe = sanitizeFamily(family);
|
|
167
|
+
const fmt = fontFormatFromUrl(url);
|
|
168
|
+
const src = fmt ? `url("${url}") format("${fmt}")` : `url("${url}")`;
|
|
169
|
+
return `@font-face{font-family:"${safe}";src:${src};font-display:swap;}`;
|
|
170
|
+
}
|
|
171
|
+
var FORMAT_BY_EXT;
|
|
172
|
+
var init_font = __esm({
|
|
173
|
+
"src/ui/font.ts"() {
|
|
174
|
+
"use strict";
|
|
175
|
+
FORMAT_BY_EXT = {
|
|
176
|
+
woff2: "woff2",
|
|
177
|
+
woff: "woff",
|
|
178
|
+
ttf: "truetype",
|
|
179
|
+
otf: "opentype"
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
147
184
|
// node_modules/@magicfeedback/native/dist/magicfeedback-sdk.browser.js
|
|
148
185
|
var require_magicfeedback_sdk_browser = __commonJS({
|
|
149
186
|
"node_modules/@magicfeedback/native/dist/magicfeedback-sdk.browser.js"(exports, module2) {
|
|
@@ -2213,13 +2250,22 @@ var init_style = __esm({
|
|
|
2213
2250
|
padding: 0;
|
|
2214
2251
|
font-weight: 900;
|
|
2215
2252
|
color: #222;
|
|
2216
|
-
font-family: 'Montserrat';
|
|
2253
|
+
font-family: var(--deepdots-font, 'Montserrat');
|
|
2217
2254
|
font-size: 18px;
|
|
2218
2255
|
text-transform: uppercase;
|
|
2219
2256
|
text-align: center;
|
|
2220
2257
|
margin-bottom: 40px;
|
|
2221
2258
|
}
|
|
2222
2259
|
|
|
2260
|
+
/* Los controles de formulario no heredan font-family por defecto: cuando hay
|
|
2261
|
+
fuente custom (clase deepdots-has-font), los forzamos a usar la del contenedor. */
|
|
2262
|
+
.deepdots-popup.deepdots-has-font button,
|
|
2263
|
+
.deepdots-popup.deepdots-has-font input,
|
|
2264
|
+
.deepdots-popup.deepdots-has-font textarea,
|
|
2265
|
+
.deepdots-popup.deepdots-has-font select {
|
|
2266
|
+
font-family: var(--deepdots-font, inherit);
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2223
2269
|
.deepdots-popup h6 {
|
|
2224
2270
|
margin-bottom: 1rem;
|
|
2225
2271
|
}
|
|
@@ -2992,6 +3038,18 @@ function ensureResponsiveStyles(_popup) {
|
|
|
2992
3038
|
`;
|
|
2993
3039
|
document.head.appendChild(style);
|
|
2994
3040
|
}
|
|
3041
|
+
function ensureFontFace(family, url) {
|
|
3042
|
+
const css = buildFontFaceCss(family, url);
|
|
3043
|
+
if (!css) return;
|
|
3044
|
+
const STYLE_ID = "deepdots-font-face";
|
|
3045
|
+
let el = document.getElementById(STYLE_ID);
|
|
3046
|
+
if (!el) {
|
|
3047
|
+
el = document.createElement("style");
|
|
3048
|
+
el.id = STYLE_ID;
|
|
3049
|
+
document.head.appendChild(el);
|
|
3050
|
+
}
|
|
3051
|
+
if (el.textContent !== css) el.textContent = css;
|
|
3052
|
+
}
|
|
2995
3053
|
async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
|
|
2996
3054
|
let surveyCompletedEmitted = false;
|
|
2997
3055
|
let stylesInjected = false;
|
|
@@ -3027,6 +3085,14 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
3027
3085
|
width: 90%;
|
|
3028
3086
|
min-height: 200px;
|
|
3029
3087
|
`;
|
|
3088
|
+
const font = style?.font;
|
|
3089
|
+
if (font?.family) {
|
|
3090
|
+
const familyValue = buildFontFamilyValue(font.family);
|
|
3091
|
+
popup.style.fontFamily = familyValue;
|
|
3092
|
+
popup.style.setProperty("--deepdots-font", familyValue);
|
|
3093
|
+
popup.classList.add("deepdots-has-font");
|
|
3094
|
+
if (font.url) ensureFontFace(font.family, font.url);
|
|
3095
|
+
}
|
|
3030
3096
|
const header = document.createElement("div");
|
|
3031
3097
|
header.className = "deepdots-popup-header";
|
|
3032
3098
|
header.style.cssText = "display:flex; justify-content:flex-end; align-items:center; width:100%;";
|
|
@@ -3448,6 +3514,7 @@ var init_renderPopup = __esm({
|
|
|
3448
3514
|
"src/ui/renderPopup.ts"() {
|
|
3449
3515
|
"use strict";
|
|
3450
3516
|
init_tracking_manager();
|
|
3517
|
+
init_font();
|
|
3451
3518
|
import_native = __toESM(require_magicfeedback_sdk_browser());
|
|
3452
3519
|
init_style();
|
|
3453
3520
|
}
|
|
@@ -3465,6 +3532,7 @@ __export(index_exports, {
|
|
|
3465
3532
|
POPUP_TRIGGER_CONDITION_STATUSES: () => POPUP_TRIGGER_CONDITION_STATUSES,
|
|
3466
3533
|
ReactNativePopupRenderer: () => ReactNativePopupRenderer,
|
|
3467
3534
|
buildAnalyticsFeedbackBody: () => buildAnalyticsFeedbackBody,
|
|
3535
|
+
buildMessageParams: () => buildMessageParams,
|
|
3468
3536
|
buildSurveyHtml: () => buildSurveyHtml,
|
|
3469
3537
|
collectRnDevice: () => collectRnDevice,
|
|
3470
3538
|
crashRecordToParams: () => crashRecordToParams,
|
|
@@ -3663,6 +3731,7 @@ function resolveEnvironment(nodeEnv) {
|
|
|
3663
3731
|
init_tracking_manager();
|
|
3664
3732
|
|
|
3665
3733
|
// src/ui/surveyHtml.ts
|
|
3734
|
+
init_font();
|
|
3666
3735
|
function buildSurveyHtml(opts) {
|
|
3667
3736
|
const env = opts.env === "development" ? "dev" : "prod";
|
|
3668
3737
|
const version = opts.version ?? "2.2.4";
|
|
@@ -3671,10 +3740,12 @@ function buildSurveyHtml(opts) {
|
|
|
3671
3740
|
const pid = JSON.stringify(opts.productId);
|
|
3672
3741
|
const profileJson = JSON.stringify(opts.profile ?? []);
|
|
3673
3742
|
const metaJson = JSON.stringify(opts.metadata ?? []);
|
|
3743
|
+
const fontFaceCss = opts.font ? buildFontFaceCss(opts.font.family, opts.font.url) : "";
|
|
3744
|
+
const fontFamilyValue = opts.font ? buildFontFamilyValue(opts.font.family) : "-apple-system,system-ui,sans-serif";
|
|
3674
3745
|
return `<!DOCTYPE html><html><head>
|
|
3675
3746
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
3676
3747
|
<meta name="color-scheme" content="light"/>
|
|
3677
|
-
<style
|
|
3748
|
+
<style>${fontFaceCss}html,body{margin:0;padding:0;background:transparent;font-family:${fontFamilyValue}}#mf{width:100%;box-sizing:border-box}#mf *{max-width:100%;box-sizing:border-box}</style>
|
|
3678
3749
|
</head><body>
|
|
3679
3750
|
<div id="mf"></div>
|
|
3680
3751
|
<script>
|
|
@@ -3725,13 +3796,13 @@ var ReactNativePopupRenderer = class {
|
|
|
3725
3796
|
}
|
|
3726
3797
|
init() {
|
|
3727
3798
|
}
|
|
3728
|
-
show(surveyId, productId, _actions, emit, onClose, env = "production", userId,
|
|
3799
|
+
show(surveyId, productId, _actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
|
|
3729
3800
|
this.emitFn = emit;
|
|
3730
3801
|
this.onCloseFn = onClose;
|
|
3731
3802
|
this.currentSurveyId = surveyId;
|
|
3732
3803
|
this.partialEmitted = false;
|
|
3733
3804
|
const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);
|
|
3734
|
-
const html = buildSurveyHtml({ surveyId, productId, env, profile, metadata });
|
|
3805
|
+
const html = buildSurveyHtml({ surveyId, productId, env, profile, metadata, font: style?.font });
|
|
3735
3806
|
if (this.options.onShow) {
|
|
3736
3807
|
this.options.onShow({ surveyId, productId, html });
|
|
3737
3808
|
} else {
|
|
@@ -4397,11 +4468,27 @@ var CrashReporter = class {
|
|
|
4397
4468
|
}
|
|
4398
4469
|
};
|
|
4399
4470
|
|
|
4471
|
+
// src/analytics/messaging.ts
|
|
4472
|
+
function buildMessageParams(stage, o) {
|
|
4473
|
+
const p = {
|
|
4474
|
+
stage,
|
|
4475
|
+
message_id: o.id,
|
|
4476
|
+
message_title: o.title,
|
|
4477
|
+
channel: o.channel
|
|
4478
|
+
};
|
|
4479
|
+
if (o.campaign) p.campaign = o.campaign;
|
|
4480
|
+
if (o.value !== void 0) p.value = o.value;
|
|
4481
|
+
if (o.currency) p.currency = o.currency;
|
|
4482
|
+
return { ...p, ...o.params ?? {} };
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4400
4485
|
// src/core/deepdots-popups.ts
|
|
4401
4486
|
var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
|
|
4402
4487
|
var DAY_IN_MS = 24 * 60 * 60 * 1e3;
|
|
4403
4488
|
var ANALYTICS_MAX_BATCH_SIZE = 20;
|
|
4404
4489
|
var ANALYTICS_FLUSH_INTERVAL_MS = 3e4;
|
|
4490
|
+
var RESERVED_EVENT_PREFIX = "deepdots_";
|
|
4491
|
+
var CUSTOM_EVENT_PREFIX = "deepdots_event_";
|
|
4405
4492
|
var DeepdotsPopups = class {
|
|
4406
4493
|
constructor() {
|
|
4407
4494
|
this.config = null;
|
|
@@ -4554,8 +4641,9 @@ var DeepdotsPopups = class {
|
|
|
4554
4641
|
/** Registra un evento de analítica (modelo GA: nombre + params). No-op si tracking off. */
|
|
4555
4642
|
track(name, params) {
|
|
4556
4643
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
4557
|
-
|
|
4558
|
-
this.
|
|
4644
|
+
const eventName = name.startsWith(RESERVED_EVENT_PREFIX) ? name : `${CUSTOM_EVENT_PREFIX}${name}`;
|
|
4645
|
+
this.analytics?.track(eventName, params);
|
|
4646
|
+
this.log("analytics \xB7 track:", eventName, params ?? {});
|
|
4559
4647
|
}
|
|
4560
4648
|
/** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
|
|
4561
4649
|
setUserAttributes(attributes) {
|
|
@@ -4607,6 +4695,10 @@ var DeepdotsPopups = class {
|
|
|
4607
4695
|
trackFunnelStep(funnel, step, taskId, params) {
|
|
4608
4696
|
this.track("deepdots_funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
|
|
4609
4697
|
}
|
|
4698
|
+
/** Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app). No-op si tracking off. */
|
|
4699
|
+
trackMessage(stage, options) {
|
|
4700
|
+
this.track("deepdots_message", buildMessageParams(stage, options));
|
|
4701
|
+
}
|
|
4610
4702
|
/**
|
|
4611
4703
|
* Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
|
|
4612
4704
|
* El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.
|