@magicfeedback/popup-sdk 1.1.4 → 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":"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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 {
@@ -658,7 +667,7 @@ declare class ReactNativePopupRenderer implements PopupRenderer {
658
667
  private partialEmitted;
659
668
  constructor(options?: ReactNativeRendererOptions);
660
669
  init(): void;
661
- 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;
662
671
  hide(): void;
663
672
  /**
664
673
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
@@ -684,6 +693,7 @@ interface BuildSurveyHtmlOptions {
684
693
  profile?: IdentityAnswer[];
685
694
  metadata?: IdentityAnswer[];
686
695
  version?: string;
696
+ font?: PopupFont;
687
697
  }
688
698
  declare function buildSurveyHtml(opts: BuildSurveyHtmlOptions): string;
689
699
 
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 {
@@ -658,7 +667,7 @@ declare class ReactNativePopupRenderer implements PopupRenderer {
658
667
  private partialEmitted;
659
668
  constructor(options?: ReactNativeRendererOptions);
660
669
  init(): void;
661
- 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;
662
671
  hide(): void;
663
672
  /**
664
673
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
@@ -684,6 +693,7 @@ interface BuildSurveyHtmlOptions {
684
693
  profile?: IdentityAnswer[];
685
694
  metadata?: IdentityAnswer[];
686
695
  version?: string;
696
+ font?: PopupFont;
687
697
  }
688
698
  declare function buildSurveyHtml(opts: BuildSurveyHtmlOptions): string;
689
699
 
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
  }
@@ -3664,6 +3731,7 @@ function resolveEnvironment(nodeEnv) {
3664
3731
  init_tracking_manager();
3665
3732
 
3666
3733
  // src/ui/surveyHtml.ts
3734
+ init_font();
3667
3735
  function buildSurveyHtml(opts) {
3668
3736
  const env = opts.env === "development" ? "dev" : "prod";
3669
3737
  const version = opts.version ?? "2.2.4";
@@ -3672,10 +3740,12 @@ function buildSurveyHtml(opts) {
3672
3740
  const pid = JSON.stringify(opts.productId);
3673
3741
  const profileJson = JSON.stringify(opts.profile ?? []);
3674
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";
3675
3745
  return `<!DOCTYPE html><html><head>
3676
3746
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
3677
3747
  <meta name="color-scheme" content="light"/>
3678
- <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>
3679
3749
  </head><body>
3680
3750
  <div id="mf"></div>
3681
3751
  <script>
@@ -3726,13 +3796,13 @@ var ReactNativePopupRenderer = class {
3726
3796
  }
3727
3797
  init() {
3728
3798
  }
3729
- 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) {
3730
3800
  this.emitFn = emit;
3731
3801
  this.onCloseFn = onClose;
3732
3802
  this.currentSurveyId = surveyId;
3733
3803
  this.partialEmitted = false;
3734
3804
  const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);
3735
- const html = buildSurveyHtml({ surveyId, productId, env, profile, metadata });
3805
+ const html = buildSurveyHtml({ surveyId, productId, env, profile, metadata, font: style?.font });
3736
3806
  if (this.options.onShow) {
3737
3807
  this.options.onShow({ surveyId, productId, html });
3738
3808
  } else {