@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.
@@ -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-6WTOQGET.mjs.map
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAoBO,IAAM,kBAAN,MAAiD;AAAA,EAAjD;AACL,SAAQ,MAAM,oBAAI,IAAoB;AAAA;AAAA,EACtC,QAAQ,KAA4B;AAClC,WAAO,KAAK,IAAI,IAAI,GAAG,IAAK,KAAK,IAAI,IAAI,GAAG,IAAe;AAAA,EAC7D;AAAA,EACA,QAAQ,KAAa,OAAqB;AACxC,SAAK,IAAI,IAAI,KAAK,KAAK;AAAA,EACzB;AAAA,EACA,WAAW,KAAmB;AAC5B,SAAK,IAAI,OAAO,GAAG;AAAA,EACrB;AACF;AAGO,SAAS,uBAAwC;AACtD,MAAI;AACF,QAAI,OAAO,WAAW,eAAe,OAAO,cAAc;AACxD,YAAM,KAAK,OAAO;AAClB,YAAM,QAAQ;AACd,SAAG,QAAQ,OAAO,GAAG;AACrB,SAAG,WAAW,KAAK;AACnB,aAAO;AAAA,QACL,SAAS,CAAC,MAAM,GAAG,QAAQ,CAAC;AAAA,QAC5B,SAAS,CAAC,GAAG,MAAM,GAAG,QAAQ,GAAG,CAAC;AAAA,QAClC,YAAY,CAAC,MAAM,GAAG,WAAW,CAAC;AAAA,MACpC;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,IAAI,gBAAgB;AAC7B;AAGO,IAAM,eAAe;AAAA,EAC1B,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,mBAAmB;AACrB;AAaO,SAAS,oBACd,QACA,WACA,cAA6B,MAC7B,6BAA4C,MACe;AAC3D,QAAM,UAA4B,SAAS,CAAC,EAAE,KAAK,oBAAoB,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC;AAC7F,QAAM,WAA6B,CAAC;AACpC,MAAI,UAAW,UAAS,KAAK,EAAE,KAAK,cAAc,OAAO,CAAC,SAAS,EAAE,CAAC;AACtE,MAAI,OAAQ,UAAS,KAAK,EAAE,KAAK,WAAW,OAAO,CAAC,MAAM,EAAE,CAAC;AAC7D,MAAI,YAAa,UAAS,KAAK,EAAE,KAAK,gBAAgB,OAAO,CAAC,WAAW,EAAE,CAAC;AAC5E,MAAI,2BAA4B,UAAS,KAAK,EAAE,KAAK,0CAA0C,OAAO,CAAC,0BAA0B,EAAE,CAAC;AACpI,SAAO,EAAE,SAAS,SAAS;AAC7B;AAcA,SAAS,cAAsB;AAC7B,QAAM,IAAK,OAAO,eAAe,cAAc,aAAa,CAAC;AAG7D,MAAI,EAAE,QAAQ,WAAY,QAAO,EAAE,OAAO,WAAW;AACrD,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,MAAI,EAAE,QAAQ,iBAAiB;AAC7B,MAAE,OAAO,gBAAgB,KAAK;AAAA,EAChC,OAAO;AACL,aAAS,IAAI,GAAG,IAAI,IAAI,IAAK,OAAM,CAAC,IAAI,KAAK,MAAM,KAAK,OAAO,IAAI,GAAG;AAAA,EACxE;AACA,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,MAAM,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC;AACpE,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,IAAI,MAAM,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,IAC/D,MAAM,GAAG,CAAC,EACV,KAAK,EAAE,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC;AACxE;AAEO,IAAM,kBAAN,MAAsB;AAAA,EAW3B,YAAY,SAAiC;AAH7C;AAAA,SAAQ,YAA2B;AACnC,SAAQ,qBAAqB;AAG3B,SAAK,UAAU,QAAQ;AACvB,SAAK,eAAe,QAAQ;AAC5B,SAAK,MAAM,QAAQ,QAAQ,MAAM,KAAK,IAAI;AAC1C,SAAK,OAAO,QAAQ,QAAQ;AAC5B,SAAK,UAAU,QAAQ,WAAW;AAAA,EACpC;AAAA,EAEA,oBAA6B;AAC3B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,mBAAmB,SAAwB;AACzC,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EAGA,YAA2B;AACzB,QAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,QAAI,KAAK,aAAc,QAAO,KAAK;AAEnC,UAAM,YAAY,KAAK,QAAQ,QAAQ,aAAa,MAAM;AAC1D,QAAI,UAAW,QAAO;AAEtB,UAAM,KAAK,KAAK,KAAK;AACrB,SAAK,QAAQ,QAAQ,aAAa,QAAQ,EAAE;AAC5C,SAAK,QAAQ,QAAQ,aAAa,WAAW,OAAO,KAAK,IAAI,CAAC,CAAC;AAC/D,SAAK,qBAAqB;AAC1B,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,YAAqB;AACnB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,eAA8B;AAC5B,QAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,aAAa,WAAgC;AAC3C,QAAI,CAAC,KAAK,QAAS;AACnB,SAAK,YAAY;AAAA,EACnB;AACF;;;AC1KA,IAAM,gBAAwC;AAAA,EAC5C,OAAO;AAAA,EACP,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AACP;AAGO,SAAS,kBAAkB,KAAiC;AACjE,QAAM,QAAQ,IAAI,MAAM,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC;AAC5C,QAAM,MAAM,MAAM,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,EAAE,YAAY;AAChE,SAAO,cAAc,GAAG;AAC1B;AAIA,SAAS,eAAe,QAAwB;AAC9C,SAAO,OAAO,QAAQ,qBAAqB,EAAE,EAAE,KAAK;AACtD;AAEA,SAAS,cAAc,KAAsB;AAG3C,MAAI,CAAC,oBAAoB,KAAK,GAAG,EAAG,QAAO;AAE3C,SAAO,CAAC,mBAAmB,KAAK,GAAG;AACrC;AAGO,SAAS,qBAAqB,QAAwB;AAC3D,QAAM,OAAO,eAAe,MAAM;AAClC,SAAO,IAAI,IAAI;AACjB;AAGO,SAAS,iBAAiB,QAAgB,KAAsB;AACrE,MAAI,CAAC,OAAO,CAAC,cAAc,GAAG,EAAG,QAAO;AACxC,QAAM,OAAO,eAAe,MAAM;AAClC,QAAM,MAAM,kBAAkB,GAAG;AACjC,QAAM,MAAM,MAAM,QAAQ,GAAG,cAAc,GAAG,OAAO,QAAQ,GAAG;AAChE,SAAO,2BAA2B,IAAI,SAAS,GAAG;AACpD;","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-IOAZQPZE.mjs.map
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":";;;;;;;;AAoBO,IAAM,kBAAN,MAAiD;AAAA,EAAjD;AACL,SAAQ,MAAM,oBAAI,IAAoB;AAAA;AAAA,EACtC,QAAQ,KAA4B;AAClC,WAAO,KAAK,IAAI,IAAI,GAAG,IAAK,KAAK,IAAI,IAAI,GAAG,IAAe;AAAA,EAC7D;AAAA,EACA,QAAQ,KAAa,OAAqB;AACxC,SAAK,IAAI,IAAI,KAAK,KAAK;AAAA,EACzB;AAAA,EACA,WAAW,KAAmB;AAC5B,SAAK,IAAI,OAAO,GAAG;AAAA,EACrB;AACF;AAGO,SAAS,uBAAwC;AACtD,MAAI;AACF,QAAI,OAAO,WAAW,eAAe,OAAO,cAAc;AACxD,YAAM,KAAK,OAAO;AAClB,YAAM,QAAQ;AACd,SAAG,QAAQ,OAAO,GAAG;AACrB,SAAG,WAAW,KAAK;AACnB,aAAO;AAAA,QACL,SAAS,CAAC,MAAM,GAAG,QAAQ,CAAC;AAAA,QAC5B,SAAS,CAAC,GAAG,MAAM,GAAG,QAAQ,GAAG,CAAC;AAAA,QAClC,YAAY,CAAC,MAAM,GAAG,WAAW,CAAC;AAAA,MACpC;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,IAAI,gBAAgB;AAC7B;AAGO,IAAM,eAAe;AAAA,EAC1B,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,mBAAmB;AACrB;AAaO,SAAS,oBACd,QACA,WACA,cAA6B,MAC7B,6BAA4C,MACe;AAC3D,QAAM,UAA4B,SAAS,CAAC,EAAE,KAAK,oBAAoB,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC;AAC7F,QAAM,WAA6B,CAAC;AACpC,MAAI,UAAW,UAAS,KAAK,EAAE,KAAK,cAAc,OAAO,CAAC,SAAS,EAAE,CAAC;AACtE,MAAI,OAAQ,UAAS,KAAK,EAAE,KAAK,WAAW,OAAO,CAAC,MAAM,EAAE,CAAC;AAC7D,MAAI,YAAa,UAAS,KAAK,EAAE,KAAK,gBAAgB,OAAO,CAAC,WAAW,EAAE,CAAC;AAC5E,MAAI,2BAA4B,UAAS,KAAK,EAAE,KAAK,0CAA0C,OAAO,CAAC,0BAA0B,EAAE,CAAC;AACpI,SAAO,EAAE,SAAS,SAAS;AAC7B;AAcA,SAAS,cAAsB;AAC7B,QAAM,IAAK,OAAO,eAAe,cAAc,aAAa,CAAC;AAG7D,MAAI,EAAE,QAAQ,WAAY,QAAO,EAAE,OAAO,WAAW;AACrD,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,MAAI,EAAE,QAAQ,iBAAiB;AAC7B,MAAE,OAAO,gBAAgB,KAAK;AAAA,EAChC,OAAO;AACL,aAAS,IAAI,GAAG,IAAI,IAAI,IAAK,OAAM,CAAC,IAAI,KAAK,MAAM,KAAK,OAAO,IAAI,GAAG;AAAA,EACxE;AACA,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,MAAM,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC;AACpE,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,IAAI,MAAM,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,IAC/D,MAAM,GAAG,CAAC,EACV,KAAK,EAAE,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC;AACxE;AAEO,IAAM,kBAAN,MAAsB;AAAA,EAW3B,YAAY,SAAiC;AAH7C;AAAA,SAAQ,YAA2B;AACnC,SAAQ,qBAAqB;AAG3B,SAAK,UAAU,QAAQ;AACvB,SAAK,eAAe,QAAQ;AAC5B,SAAK,MAAM,QAAQ,QAAQ,MAAM,KAAK,IAAI;AAC1C,SAAK,OAAO,QAAQ,QAAQ;AAC5B,SAAK,UAAU,QAAQ,WAAW;AAAA,EACpC;AAAA,EAEA,oBAA6B;AAC3B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,mBAAmB,SAAwB;AACzC,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EAGA,YAA2B;AACzB,QAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,QAAI,KAAK,aAAc,QAAO,KAAK;AAEnC,UAAM,YAAY,KAAK,QAAQ,QAAQ,aAAa,MAAM;AAC1D,QAAI,UAAW,QAAO;AAEtB,UAAM,KAAK,KAAK,KAAK;AACrB,SAAK,QAAQ,QAAQ,aAAa,QAAQ,EAAE;AAC5C,SAAK,QAAQ,QAAQ,aAAa,WAAW,OAAO,KAAK,IAAI,CAAC,CAAC;AAC/D,SAAK,qBAAqB;AAC1B,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,YAAqB;AACnB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,eAA8B;AAC5B,QAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,aAAa,WAAgC;AAC3C,QAAI,CAAC,KAAK,QAAS;AACnB,SAAK,YAAY;AAAA,EACnB;AACF;;;AC1KA,IAAM,gBAAwC;AAAA,EAC5C,OAAO;AAAA,EACP,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AACP;AAGO,SAAS,kBAAkB,KAAiC;AACjE,QAAM,QAAQ,IAAI,MAAM,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC;AAC5C,QAAM,MAAM,MAAM,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,EAAE,YAAY;AAChE,SAAO,cAAc,GAAG;AAC1B;AAIA,SAAS,eAAe,QAAwB;AAC9C,SAAO,OAAO,QAAQ,qBAAqB,EAAE,EAAE,KAAK;AACtD;AAEA,SAAS,cAAc,KAAsB;AAG3C,MAAI,CAAC,oBAAoB,KAAK,GAAG,EAAG,QAAO;AAE3C,SAAO,CAAC,mBAAmB,KAAK,GAAG;AACrC;AAGO,SAAS,qBAAqB,QAAwB;AAC3D,QAAM,OAAO,eAAe,MAAM;AAClC,SAAO,IAAI,IAAI;AACjB;AAGO,SAAS,iBAAiB,QAAgB,KAAsB;AACrE,MAAI,CAAC,OAAO,CAAC,cAAc,GAAG,EAAG,QAAO;AACxC,QAAM,OAAO,eAAe,MAAM;AAClC,QAAM,MAAM,kBAAkB,GAAG;AACjC,QAAM,MAAM,MAAM,QAAQ,GAAG,cAAc,GAAG,OAAO,QAAQ,GAAG;AAChE,SAAO,2BAA2B,IAAI,SAAS,GAAG;AACpD;","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, _style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string): void;
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, _style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string): void;
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>html,body{margin:0;padding:0;background:transparent;font-family:-apple-system,system-ui,sans-serif}#mf{width:100%;box-sizing:border-box}#mf *{max-width:100%;box-sizing:border-box}</style>
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, _style, sessionId, miniService, analyticsFeedbackSessionId) {
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
- this.analytics?.track(name, params);
4558
- this.log("analytics \xB7 track:", name, params ?? {});
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.