@magicfeedback/popup-sdk 1.16.0 → 1.17.0

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types/index.ts","../src/triggers/index.ts","../src/config/env.ts","../src/ui/surveyHtml.ts","../src/platform/react-native-renderer.ts","../src/ui/preload.ts","../src/platform/renderer.ts","../src/analytics/analytics-manager.ts","../src/analytics/feedback-payload.ts","../src/analytics/suspended-session.ts","../src/tracking/navigation-observer.ts","../src/analytics/device-info.ts","../src/analytics/geo-info.ts","../src/analytics/language.ts","../src/analytics/engagement-tracker.ts","../src/contact/contact-manager.ts","../src/analytics/crash-reporter.ts","../src/core/popup-state-store.ts","../src/analytics/messaging.ts","../src/core/deepdots-popups.ts","../src/react-native/setup.ts"],"sourcesContent":["/**\n * Configuration options for initializing the DeepdotsPopups SDK\n */\nimport type { KeyValueStorage } from '../tracking/tracking-manager';\nimport type { DeviceInfo } from '../analytics/device-info';\nimport type { AnalyticsKeys } from '../analytics/feedback-payload';\n\n/**\n * Custom logger to receive the SDK's debug output instead of the console.\n * Only `log` is required; `warn`/`error`/`info` fall back to `log` when omitted.\n * Lets the host pipe debug output to a file, Firebase, etc. `console` satisfies this shape.\n */\nexport interface DeepdotsLogger {\n log: (...args: unknown[]) => void;\n warn?: (...args: unknown[]) => void;\n error?: (...args: unknown[]) => void;\n info?: (...args: unknown[]) => void;\n}\n\n/**\n * A session of the analytics channel as the Deepdots API sees it. Every session becomes ONE\n * feedback, and `sessionId` is what the API stores as `sdkSessionId` on it: the value to look\n * that feedback up with (`GET /feedbacks?filter={\"where\":{\"sdkSessionId\":\"<sessionId>\"}}`).\n * A session that closes again keeps that one feedback: the API updates it.\n * - `open`: the API acknowledged the session's first batch; its events keep accumulating.\n * - `closed`: the API acknowledged the session's closing batch (`completed: true`); the\n * feedback is created from it within seconds.\n */\nexport interface FeedbackSession {\n sessionId: string;\n status: 'open' | 'closed';\n}\n\nexport interface DeepdotsInitParams {\n /** API key for authentication */\n apiKey?: string;\n /** Node environment: 'development' or 'production' */\n nodeEnv?: 'development' | 'production';\n /** Enable debug logging */\n debug?: boolean;\n /** Custom logger for debug output. When set (and `debug` is true), the SDK routes its logs here instead of `console`. Default: `console`. */\n logger?: DeepdotsLogger;\n /** Optional user id to send with popup events */\n userId?: string;\n /** Starts tracking enabled (default) or disabled (e.g. until consent is granted). Equivalent to calling `setTrackingEnabled` after init. */\n trackingEnabled?: boolean;\n /** Host app version (for analytics device info / Technology #11). */\n appVersion?: string;\n /** Injectable persistent storage (RN: adapter over AsyncStorage). If missing, uses localStorage/in-memory. */\n storage?: KeyValueStorage;\n /** Platform used in the analytics envelope. Default is 'web'; in RN pass 'android'/'ios'. */\n platform?: 'web' | 'android' | 'ios';\n /**\n * BCP-47 language tag (e.g. 'es-ES') used for the analytics context AND for popup\n * language segmentation (`segments.lang`). If omitted, the SDK auto-detects:\n * `navigator.language` on web, or the `Intl` locale as a fallback (works in\n * React Native with Hermes, where `navigator.language` is absent). If no source\n * resolves a language, popups with `segments.lang` cannot be filtered and are shown.\n */\n language?: string;\n /** Injectable device info (RN: from react-native-device-info). If missing, it is derived from the browser. */\n device?: DeviceInfo;\n /**\n * Analytics channel keys (`POST /sdk/feedback`). If provided, analytics is SENT\n * to the configured integration; if missing, it stays in dry-run mode: nothing is sent, and the payload is only\n * logged when `debug` is true.\n */\n analytics?: AnalyticsKeys;\n /**\n * Called when the analytics channel opens or closes a session (needs `analytics`), with the\n * id the Deepdots API stores as `sdkSessionId` on the feedback the session becomes: send it\n * to your backend to attach data to that feedback later. A session whose only batch is the\n * closing one reports just `closed`. `closed` does not come when the page unloads before the\n * closing request returns, for a session dropped after 30 min hidden, nor when the closing\n * request fails (the API closes those itself, later): their id already came with `open`. Key\n * by `sessionId`, not by order: the next session's `open` can arrive before the previous\n * one's `closed`. Same value as `getFeedbackSessionId()` while the session is open.\n */\n onFeedbackSession?: (session: FeedbackSession) => void;\n /**\n * IP geolocation (country/city) added to analytics. Default `true`. The lookup calls\n * third-party services (ipapi.co, then ipwho.is, then ipinfo.io as fallbacks, 3 s timeout\n * each) and only runs when `analytics` is configured, tracking is enabled and the 30-day\n * cache is missing or expired. Set `false` to never call them nor attach country/city.\n */\n geolocation?: boolean;\n /**\n * Internal user attributes known only by the host (language, age, plan, ...), sent\n * to the backend Contact for segmentation/targeting. Requires `userId`. Equivalent to calling\n * `setContactAttributes` after init (sends only when values changed from the last sync).\n */\n contactAttributes?: Record<string, string | number | boolean>;\n /**\n * Whether the SDK renders the popup \"modal\" (card + backdrop + positioning) around the survey.\n * Default `true`. Only affects React Native (the survey WebView HTML); the web DOM popup is\n * unaffected. With `false` the survey is delivered transparent and edge-to-edge so the host\n * mounts its own Modal/container and controls the styling — avoids the \"double modal\" when the\n * host already wraps the WebView. The survey stays fully functional (bridge, form, nav buttons,\n * close). Ver `ReactNativePopupRenderer`.\n */\n renderChrome?: boolean;\n /**\n * Barra de progreso (\"Question X of Y\" + barra) en la cabecera del popup. Si se omite,\n * manda el `showProgressBar` que la plataforma configura en el estilo del survey; `true` o\n * `false` lo fuerzan desde el host. Solo se pinta con más de una página, fuera de la\n * pantalla de inicio y antes de completar, igual que en la web de surveys.\n */\n showProgressBar?: boolean;\n /**\n * CSS propio del host, inyectado como último bloque de estilos del popup: gana en cascada\n * sobre el del SDK y sobre el de `@magicfeedback/native` sin tener que tocar ninguno. Es la\n * vía para reestilar el área de preguntas (enunciados, opciones, escalas) por integración,\n * en vez de cambiar los defaults que comparten todos los clientes.\n */\n surveyCss?: string;\n /**\n * Nonce de la Content-Security-Policy de la página, para hosts con una CSP estricta\n * (`style-src 'nonce-…'` sin `'unsafe-inline'`). El popup lo pone en cada `<style>` que\n * inyecta en el `<head>` (hoja del survey, spinner, responsive, `surveyCss`, `@font-face`);\n * sin él el navegador las bloquea y el survey sale sin estilos. Debe ser el MISMO nonce de\n * la cabecera CSP de esa respuesta (en Angular, el token `CSP_NONCE`). Solo web: en React\n * Native el survey vive en su propio WebView y no le afecta la CSP del host.\n */\n cspNonce?: string;\n\n}\nexport interface DeepdotsConfig {\n /** API key for authentication */\n apiKey?: string;\n /** Enable debug logging */\n debug?: boolean;\n /** Optional user id to send with popup events */\n userId?: string;\n}\n\n/**\n * Options for configuring survey triggers\n */\nexport interface TriggerConfig {\n /** Trigger type: 'time' (delay), 'scroll' (scroll percentage), 'exit' (route exit), 'click' (element id), 'event' (host event) */\n type: 'time' | 'scroll' | 'exit' | 'click' | 'event';\n /** Value for the trigger (milliseconds for time, percentage for scroll, seconds for exit, element id for click, event name for event) */\n value?: number | string;\n /** Survey ID to show when triggered */\n surveyId: string;\n /** Optional popup definition id (used to disambiguate repeated survey ids) */\n popupId?: string;\n}\n\n/**\n * Event types emitted by the SDK\n */\nexport type DeepdotsEventType =\n | 'popup_shown'\n | 'popup_clicked'\n | 'survey_completed';\n\n/**\n * Event payload structure\n */\nexport interface DeepdotsEvent {\n type: DeepdotsEventType;\n surveyId: string;\n timestamp: number;\n data?: Record<string, unknown>;\n}\n\n/**\n * Event listener callback\n */\nexport type EventListener = (event: DeepdotsEvent) => void;\n\n/**\n * Motivo del `deepdots_session_end` (último lote de la sesión, enviado con `completed:true`):\n * - `page_hide`: la página web se cierra o se navega fuera (`pagehide`).\n * - `background`: la app pasa a background (RN/nativo). Única señal disponible en móvil:\n * el kill de la app por el usuario o el SO NO produce ningún callback.\n * - `user_change`: `setUserId()` o un `init()` con otro `userId` (login/logout del host).\n * - `tracking_disabled`: `setTrackingEnabled(false)` (consentimiento revocado).\n * - `manual`: `endSession()` llamado por el host.\n * - `idle_timeout`: la pestaña vuelve a verse tras más de 30 min oculta (web). La sesión\n * anterior se cierra y se abre otra, como hace GA con su timeout de sesión.\n */\nexport type SessionEndReason =\n | 'page_hide'\n | 'background'\n | 'user_change'\n | 'tracking_disabled'\n | 'manual'\n | 'idle_timeout';\n\n/**\n * Motivo del `deepdots_session_start` (param `reason`): por qué EMPIEZA la sesión. Permite a\n * analytics separar una carga/arranque real de una vuelta desde background (\"Started here\").\n * Mismos valores que el SDK nativo donde el significado coincide:\n * - `cold_start`: `init()` (carga de la página / arranque de la app RN) sin sesión suspendida\n * que reanudar. Una recarga en menos de 1 min no emite `session_start` sino\n * `deepdots_session_resume`.\n * - `foreground`: `onForeground()` (RN, AppState 'active') tras un cierre (`onBackground()`\n * o `endSession()`). Con la sesión abierta no emite nada.\n * - `tracking_enabled`: `setTrackingEnabled(true)` (consentimiento concedido).\n * - `user_change`: `setUserId()` o un `init()` con otro `userId`, tras cerrar la del anterior.\n * - `idle_timeout`: la pestaña vuelve a verse tras más de 30 min oculta (web): la sesión\n * anterior se cerró (o se descartó) y empieza otra.\n * - `bfcache_restore`: la página vuelve de la bfcache (botón atrás, `pageshow` con\n * `persisted`) fuera de la ventana de reanudación (web).\n */\nexport type SessionStartReason =\n | 'cold_start'\n | 'foreground'\n | 'tracking_enabled'\n | 'user_change'\n | 'idle_timeout'\n | 'bfcache_restore';\n\n/** Trigger type used by remote popup definitions */\nexport type PopupTriggerType = 'time_on_page' | 'scroll' | 'exit' | 'click' | 'event';\n\nexport type PopupTriggerConditionStatus = 'SHOWED' | 'PARTIAL' | 'COMPLETED';\n\nexport const POPUP_TRIGGER_CONDITION_STATUSES: PopupTriggerConditionStatus[] = [\n 'SHOWED',\n 'PARTIAL',\n 'COMPLETED',\n];\n\n/** Extra condition used to activate the popup */\nexport interface PopupTriggerCondition {\n answered: PopupTriggerConditionStatus; // user progress status in the survey\n cooldownDays: number; // cooldown days before showing again\n}\n\n/** Trigger attached to the popup definition */\nexport interface PopupTrigger {\n type: PopupTriggerType;\n value: number | string; // seconds on page, scroll percentage, click id, or event name\n}\n\n/** Accept action (open survey) */\nexport interface PopupActionAccept {\n label: string;\n surveyId: string; // survey id to launch\n}\n\n/** Decline action */\nexport interface PopupActionDecline {\n label: string;\n cooldownDays?: number; // optional: do not show again until X days pass\n}\n\n/** Start Surveys Action */\nexport interface PopupActionStart {\n label: string;\n}\n\n/** Complete action (accept and auto-complete survey) */\n// Requires the survey to support auto-completion via parameters.\nexport interface PopupActionComplete {\n label: string;\n surveyId: string; // survey id to launch\n autoCompleteParams: Record<string, unknown>; // parameters used for auto-completion\n cooldownDays?: number;\n}\n\n/** Set of actions available in the popup */\nexport interface PopupActions {\n accept?: PopupActionAccept;\n decline?: PopupActionDecline;\n complete?: PopupActionComplete;\n start?: PopupActionStart;\n back?: PopupActionDecline;\n}\n\n/** Fuente personalizable del popup + survey (viene de la API por popup) */\nexport interface PopupFont {\n /** Nombre de la familia. Ej: \"Inter\" (nombre limpio, NO un stack). */\n family: string;\n /** Opcional: URL a un archivo de fuente (woff2/ttf/otf) para armar un @font-face. */\n url?: string;\n}\n\n/** Configurable popup styles */\nexport interface PopupStyle {\n theme: 'light' | 'dark';\n position: 'bottom' | 'bottom-right' | 'bottom-left' | 'top' | 'top-right' | 'top-left' | 'center';\n /** @deprecated Not used by the SDK; kept only for API compatibility */\n imageUrl?: string | null;\n /** Fuente personalizada. Si falta, se mantiene el comportamiento actual. */\n font?: PopupFont;\n}\n\n/** Segments/targeting rules for showing the popup */\nexport interface PopupSegments {\n lang?: string[]; // allowed languages\n path?: string[]; // app routes where it can be shown\n /**\n * App routes where it must NOT be shown. Wins over `path`: a route listed here\n * blocks the popup even if `path` matches it (so `path: ['/']` +\n * `excludedPaths: ['/cart']` = everywhere but the cart).\n * Same matching rules as `path`: an absolute URL matches the full href, an entry\n * starting with `/` matches any href containing it (this is what covers hash\n * routes like `/#/cart`), anything else matches the pathname exactly.\n */\n excludedPaths?: string[];\n [key: string]: unknown; // possible future extension\n}\n\n/** Full popup definition */\nexport interface PopupDefinition {\n id: string;\n title: string;\n message: string; // rendered safe HTML (recommended to sanitize upstream)\n triggers: PopupTrigger[];\n cooldown?: PopupTriggerCondition[];\n actions?: PopupActions;\n surveyId: string;\n productId: string;\n style?: PopupStyle;\n segments?: PopupSegments;\n}\n\nexport interface FormStyle\n{\n id: string;\n logo: string;\n title: string;\n companyName: string;\n backgroundColor: string;\n boxBackgroundColor: string;\n buttonOnTopColor: string;\n buttonPrimaryColor: string\n buttonSecondaryColor: string;\n loadingBarColor: string;\n favIcon: string;\n startMessage: string;\n successMessage: string;\n addLogoInMsg: boolean;\n privacyPolicyUrl: string;\n helpUrl: string;\n integrationId: string;\n redirectLink: string;\n redirectError: string;\n redirectTime: number;\n integrationThemeId: string;\n contentAlign: 'top' | 'center';\n logoSize: 'small' | 'medium' | 'large';\n logoPosition: 'left' | 'right' | 'center';\n showProgressBar: boolean;\n showProgressUnit: boolean;\n progressUnit: 'percentage' | 'fraction';\n}\n\n\nexport interface FormData {\n id: string;\n name: string;\n description: string;\n type: string;\n identity: string;\n status: string;\n createdAt: Date;\n updatedAt: Date;\n externalId?: string | null;\n companyId: string;\n productId: string;\n userId: string;\n /**\n * Idiomas configurados en la integración del survey. `@magicfeedback/native` localiza\n * preguntas y placeholders con el primero, y el popup traduce su chrome con el mismo.\n */\n lang?: string[];\n style: FormStyle\n}\n\nexport enum POPUPSESSIONSTATUS {\n NONE = \"NONE\",\n SHOWED = \"SHOWED\",\n PARTIAL = \"PARTIAL\",\n COMPLETED = \"COMPLETED\",\n DEPRECATED = \"DEPRECATED\",\n}\n","import type { TriggerConfig } from '../types';\nimport type { DeepdotsPopups } from '../core/deepdots-popups';\n\n/** `TriggerConfig.value` también acepta string (id/nombre para click/event); para time/scroll se necesita un número. */\nfunction toNumericValue(value: number | string | undefined, fallback: number): number {\n const n = Number(value);\n return Number.isFinite(n) ? n : fallback;\n}\n\nexport function setupTrigger(popups: DeepdotsPopups, trigger: TriggerConfig): void {\n switch (trigger.type) {\n case 'time':\n setupTimeTrigger(popups, trigger);\n break;\n case 'scroll':\n setupScrollTrigger(popups, trigger);\n break;\n case 'exit':\n setupExitTrigger(popups, trigger);\n break;\n case 'event':\n setupEventTrigger(popups, trigger);\n break;\n case 'click':\n setupClickTrigger(popups, trigger);\n break;\n }\n}\n\nfunction setupTimeTrigger(popups: DeepdotsPopups, trigger: TriggerConfig): void {\n const delay = toNumericValue(trigger.value, 5000);\n setTimeout(() => {\n popups.triggerSurvey(trigger.surveyId, trigger.popupId);\n }, delay);\n popups.debug(`Time trigger set for ${delay}ms`);\n}\n\nfunction setupScrollTrigger(popups: DeepdotsPopups, trigger: TriggerConfig): void {\n const threshold = toNumericValue(trigger.value, 50);\n const handler = () => {\n const scrollPercentage = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;\n if (scrollPercentage >= threshold) {\n popups.triggerSurvey(trigger.surveyId, trigger.popupId);\n window.removeEventListener('scroll', handler);\n }\n };\n window.addEventListener('scroll', handler);\n popups.debug(`Scroll trigger set for ${threshold}%`);\n}\n\nfunction setupExitTrigger(popups: DeepdotsPopups, trigger: TriggerConfig): void {\n if (typeof window === 'undefined') return;\n\n const delaySeconds = parseExitDelaySeconds(trigger.value);\n const historyRef = window.history;\n const originalPushState = historyRef.pushState.bind(historyRef);\n const originalReplaceState = historyRef.replaceState.bind(historyRef);\n\n let lastUrl = normalizeUrl(window.location.href);\n let lastQueuedKey = '';\n let lastQueuedAt = 0;\n\n const queueLeave = (fromRaw: string, toRaw: string) => {\n const from = normalizeUrl(fromRaw);\n const to = normalizeUrl(toRaw);\n if (!from || !to || from === to) return;\n\n const key = `${from}=>${to}`;\n const now = Date.now();\n if (key === lastQueuedKey && now - lastQueuedAt < 150) return;\n lastQueuedKey = key;\n lastQueuedAt = now;\n popups.queueExitPopup(trigger.surveyId, delaySeconds, from, trigger.popupId);\n };\n\n const handleNavigation = (to: string) => {\n const from = lastUrl;\n queueLeave(from, to);\n lastUrl = normalizeUrl(to);\n };\n\n const handleDocumentClick = (event: MouseEvent) => {\n if (event.defaultPrevented) return;\n if (event.button !== 0) return;\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;\n\n const anchor = (event.target as Element | null)?.closest?.('a');\n if (!(anchor instanceof HTMLAnchorElement)) return;\n\n const href = anchor.getAttribute('href');\n if (!href || href.startsWith('javascript:') || href.startsWith('mailto:') || href.startsWith('tel:')) {\n return;\n }\n const target = anchor.getAttribute('target');\n if (target && target !== '_self') return;\n\n let destination: URL;\n try {\n destination = new URL(href, window.location.href);\n } catch {\n return;\n }\n\n if (destination.origin !== window.location.origin) return;\n queueLeave(window.location.href, destination.href);\n };\n\n const handleHashChange = (event: HashChangeEvent) => {\n const to = event.newURL || window.location.href;\n const from = event.oldURL || lastUrl;\n queueLeave(from, to);\n lastUrl = normalizeUrl(to);\n };\n\n const handlePopState = () => {\n handleNavigation(window.location.href);\n };\n\n historyRef.pushState = function (data: any, unused: string, url?: string | URL | null) {\n const from = window.location.href;\n const result = originalPushState(data, unused, url as any);\n queueLeave(from, window.location.href);\n lastUrl = normalizeUrl(window.location.href);\n return result;\n };\n\n historyRef.replaceState = function (data: any, unused: string, url?: string | URL | null) {\n const from = window.location.href;\n const result = originalReplaceState(data, unused, url as any);\n queueLeave(from, window.location.href);\n lastUrl = normalizeUrl(window.location.href);\n return result;\n };\n\n document.addEventListener('click', handleDocumentClick, true);\n window.addEventListener('hashchange', handleHashChange);\n window.addEventListener('popstate', handlePopState);\n popups.debug('Exit trigger set (deferred to next route)', {\n surveyId: trigger.surveyId,\n delaySeconds,\n });\n}\n\nfunction setupEventTrigger(popups: DeepdotsPopups, trigger: TriggerConfig): void {\n // Event triggers are manually fired by host application using popups.triggerEvent(name).\n popups.debug('Event trigger registered', {\n popupId: trigger.popupId,\n surveyId: trigger.surveyId,\n eventName: String(trigger.value || ''),\n });\n}\n\nfunction parseExitDelaySeconds(value?: number | string): number {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return Math.max(0, value);\n }\n if (typeof value === 'string' && value.trim()) {\n const parsed = Number(value);\n if (Number.isFinite(parsed)) return Math.max(0, parsed);\n }\n return 0;\n}\n\nfunction normalizeUrl(value: string): string {\n if (!value) return '';\n const withoutIndex = value.replace(/\\/index\\.html(?=($|[?#]))/i, '');\n return withoutIndex.length > 1 && withoutIndex.endsWith('/') ? withoutIndex.slice(0, -1) : withoutIndex;\n}\n\nfunction setupClickTrigger(popups: DeepdotsPopups, trigger: TriggerConfig): void {\n const targetId = typeof trigger.value === 'string' ? trigger.value : '';\n if (!targetId) {\n popups.debug('Click trigger missing element id', trigger);\n return;\n }\n\n const attach = () => {\n const el = document.getElementById(targetId);\n if (!el) {\n popups.debug('Click trigger target not found', targetId);\n return false;\n }\n const handler = (e: MouseEvent) => {\n if (e.defaultPrevented) return;\n popups.triggerSurvey(trigger.surveyId, trigger.popupId);\n el.removeEventListener('click', handler);\n };\n el.addEventListener('click', handler);\n popups.debug(`Click trigger set for #${targetId}`);\n return true;\n };\n\n if (typeof document === 'undefined') return;\n if (attach()) return;\n window.addEventListener('DOMContentLoaded', () => attach(), { once: true });\n}\n","import {DeepdotsInitParams} from '../types';\n\n/** Variables específicas por entorno derivadas de nodeEnv */\nexport interface EnvironmentVariables {\n apiBaseUrl: string;\n}\n\nconst ENVIRONMENT_MAP: Record<\"development\" | \"production\", EnvironmentVariables> = {\n development: {\n apiBaseUrl: 'https://api-dev.deepdots.com',\n },\n production: {\n apiBaseUrl: 'https://api.deepdots.com',\n },\n};\n\n/** Resuelve las variables para el entorno indicado */\nexport function resolveEnvironment(nodeEnv: DeepdotsInitParams['nodeEnv']): EnvironmentVariables {\n return ENVIRONMENT_MAP[nodeEnv || 'production'];\n}\n\n","import type { IdentityAnswer } from '../tracking/tracking-manager';\nimport { buildFontFaceCss, buildFontFamilyValue } from './font';\nimport type { PopupActions, PopupFont } from '../types';\nimport { LABELS, RESOLVE_LOCALE_JS, RTL_LOCALES, directionFor, getLabels, resolveActionLabels } from '../i18n/labels';\nimport { REVEAL_TIMEOUT_MS } from './reveal';\nimport { PRIMARY_COLOR_JS } from './surveyPalette';\nimport { BUSY_JS } from './busy';\nimport magicfeedbackCss from '../assets/style.css';\n\n/**\n * Apertura diferida, versión que corre DENTRO del WebView: espejo ES5 de `revealWhenPainted`\n * (`reveal.ts`). El popup se monta invisible y se enseña cuando el survey está pintado y sus\n * imágenes han llegado, en vez de enseñar el spinner girando; `timeoutMs` es el techo para lo\n * que no llegue nunca. En RN esto pesa más que en web, porque a la espera del survey se le suma\n * el arranque del WebView y la descarga del bundle del CDN.\n *\n * Va como texto (y no como import) porque el WebView no comparte módulos con el bundle del SDK.\n * `surveyHtml.reveal.test.ts` lo evalúa para probar el comportamiento, no solo el string.\n */\nexport const REVEAL_JS = `\nfunction ddCreateReveal(popup, onReveal, timeoutMs){\n var revealed=false, timer=null;\n function reveal(){\n if(revealed){ return; }\n revealed=true;\n if(timer){ clearTimeout(timer); timer=null; }\n onReveal();\n }\n function revealWhenPainted(){\n if(revealed){ return; }\n var imgs=popup.querySelectorAll('img'), pending=[];\n for(var i=0;i<imgs.length;i++){ if(!imgs[i].complete){ pending.push(imgs[i]); } }\n if(!pending.length){ reveal(); return; }\n var left=pending.length;\n function onSettled(){ left--; if(left<=0){ reveal(); } }\n for(var j=0;j<pending.length;j++){\n pending[j].addEventListener('load', onSettled);\n pending[j].addEventListener('error', onSettled);\n }\n }\n timer=setTimeout(reveal, timeoutMs);\n return { reveal: reveal, whenPainted: revealWhenPainted };\n}`;\n\n/**\n * Construye un HTML autocontenido que renderiza el popup (chrome + survey de\n * `@magicfeedback/native`) dentro de un WebView (React Native). Es el equivalente RN de\n * `renderPopup.ts` (DOM real, solo navegador): mismo header con botón cerrar, spinner,\n * footer con back/start/send, gate \"Start survey\" y colores/logo desde `formData.style`.\n * Carga el bundle del survey desde CDN, crea el form con la identidad inyectada\n * (`profile`/`metadata`) y reenvía los eventos al host vía el puente de\n * react-native-webview: `window.ReactNativeWebView.postMessage`.\n *\n * Mensajes emitidos (JSON `{name, payload}`): `loaded`, `ready`, `before_submit`,\n * `after_submit`, `survey_completed`, `back`, `popup_close`, y `error:load` / `error:init` /\n * `error:exception` (con `{message}`) cuando el survey no ha podido cargar.\n */\nexport interface BuildSurveyHtmlOptions {\n surveyId: string;\n productId: string;\n env?: string; // 'production' | 'development'\n profile?: IdentityAnswer[];\n metadata?: IdentityAnswer[];\n version?: string; // versión de @magicfeedback/native\n font?: PopupFont; // fuente personalizada (family + url opcional)\n theme?: 'light' | 'dark';\n position?: 'bottom' | 'bottom-right' | 'bottom-left' | 'top' | 'top-right' | 'top-left' | 'center';\n actions?: PopupActions;\n /**\n * Renderiza el \"modal\" del popup (tarjeta con fondo/sombra/bordes + overlay + posicionamiento).\n * Default `true`. Con `false` el HTML es transparente y llena el contenedor: el host monta su\n * propio Modal/tarjeta y controla el estilo. El survey sigue siendo funcional (bridge, form,\n * botones y botón de cerrar). Pensado para React Native cuando el host gestiona el contenedor.\n */\n chrome?: boolean;\n /**\n * Título de la cabecera, a la izquierda del botón de cerrar. Es el `title` de la definición\n * del popup (`GET /sdk/{publicKey}/popups`), así que cambia de un popup a otro. Vacío es un\n * valor legítimo: la cabecera se queda solo con la X. NO cae al título del survey.\n */\n title?: string;\n /**\n * Barra de progreso (\"Question X of Y\" + barra) bajo la cabecera. `undefined` respeta el\n * `showProgressBar` que configure la plataforma en el estilo del survey; `true`/`false` lo\n * fuerzan desde el host. Solo se pinta con más de una página y fuera de la pantalla de inicio.\n */\n showProgressBar?: boolean;\n /**\n * CSS del host, inyectado como último bloque de estilos: gana en cascada sobre el del SDK\n * y sobre el de `@magicfeedback/native` sin tener que tocar ninguno de los dos. Es la vía\n * para reestilar el área de preguntas (enunciados, opciones, escalas) por integración.\n */\n surveyCss?: string;\n /**\n * Idioma con el que arranca el chrome (botones, progreso, aria-labels, errores). Es el\n * idioma resuelto en `init()`. Dentro del WebView lo pisa el idioma del survey\n * (`formData.lang[0]`), y ambos los pisa `actions.*.label` de la API.\n */\n language?: string;\n}\n\ninterface PositionRule {\n justifyContent: string;\n alignItems: string;\n padding: string;\n background: string;\n}\n\nconst POSITION_MAP: Record<string, PositionRule> = {\n center: { justifyContent: 'center', alignItems: 'center', padding: '0', background: 'rgba(0,0,0,0.5)' },\n bottom: { justifyContent: 'center', alignItems: 'flex-end', padding: '16px', background: 'transparent' },\n 'bottom-right': { justifyContent: 'flex-end', alignItems: 'flex-end', padding: '16px', background: 'transparent' },\n 'bottom-left': { justifyContent: 'flex-start', alignItems: 'flex-end', padding: '16px', background: 'transparent' },\n top: { justifyContent: 'center', alignItems: 'flex-start', padding: '16px', background: 'transparent' },\n 'top-right': { justifyContent: 'flex-end', alignItems: 'flex-start', padding: '16px', background: 'transparent' },\n 'top-left': { justifyContent: 'flex-start', alignItems: 'flex-start', padding: '16px', background: 'transparent' },\n};\n\n/**\n * Serializa un valor para incrustarlo dentro de un `<script>`. `JSON.stringify` no escapa `<`,\n * así que un `</script>` dentro de un texto de la API (título, etiqueta de botón, metadata)\n * cerraría el bloque y el resto se interpretaría como HTML. Escapa también los separadores de\n * línea U+2028/U+2029, que son JSON válido pero rompen un literal de JavaScript.\n */\nfunction jsonForScript(value: unknown): string {\n return JSON.stringify(value ?? null)\n .replace(/</g, '\\\\u003C')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n}\n\n/** Devuelve el label de la API solo si trae contenido; en blanco cuenta como \"no configurado\". */\nfunction trimmedOrNull(value: string | undefined): string | null {\n const trimmed = typeof value === 'string' ? value.trim() : '';\n return trimmed ? value as string : null;\n}\n\nexport function buildSurveyHtml(opts: BuildSurveyHtmlOptions): string {\n const env = opts.env === 'development' ? 'dev' : 'prod';\n // Misma versión que la dep npm del bundle web, para que las dos rutas pinten igual.\n const version = opts.version ?? '2.2.33';\n const cdn = `https://cdn.jsdelivr.net/npm/@magicfeedback/native@${version}/dist/magicfeedback-sdk.browser.js`;\n const sid = jsonForScript(opts.surveyId);\n const pid = jsonForScript(opts.productId);\n const profileJson = jsonForScript(opts.profile ?? []);\n const metaJson = jsonForScript(opts.metadata ?? []);\n const fontFaceCss = opts.font ? buildFontFaceCss(opts.font.family, opts.font.url) : '';\n const fontFamilyValue = opts.font ? buildFontFamilyValue(opts.font.family) : '-apple-system,system-ui,sans-serif';\n const popupClass = opts.font?.family ? 'deepdots-popup deepdots-has-font' : 'deepdots-popup';\n\n const isDark = opts.theme === 'dark';\n const popupBg = isDark ? '#1e1e1e' : '#fff';\n const colorScheme = isDark ? 'dark' : 'light';\n const textPrimary = isDark ? '#f0f0f0' : '#111';\n const textMuted = isDark ? '#9ca3af' : '#6b7280';\n const trackBg = isDark ? '#3f3f46' : '#e5e7eb';\n const pos = POSITION_MAP[opts.position ?? 'center'] ?? POSITION_MAP.center;\n\n // chrome:false → el host envuelve el survey en su propio Modal/tarjeta. El HTML se vuelve\n // transparente y llena el contenedor (sin fondo/sombra/borde/overlay/posicionamiento propios);\n // así se evita el \"doble modal\" cuando el host ya monta el suyo. Survey funcional intacto.\n const chrome = opts.chrome !== false;\n const bodyBg = chrome ? pos.background : 'transparent';\n const bodyPadding = chrome ? pos.padding : '0';\n const bodyDisplay = chrome ? 'flex' : 'block';\n const cardBg = chrome ? popupBg : 'transparent';\n const cardShadow = chrome ? '0 4px 6px rgba(0,0,0,0.1)' : 'none';\n const cardRadius = chrome ? '8px' : '0';\n const cardMaxWidth = chrome ? '600px' : 'none';\n const cardWidth = chrome ? '90%' : '100%';\n const cardMinHeight = chrome ? '200px' : '0';\n // Con chrome el alto lo marca el contenido (hasta 90vh); sin chrome llena el contenedor del\n // host. En ambos casos hace falta un alto acotado para que el footer pueda quedar fijo abajo.\n const cardHeight = chrome ? 'auto' : '100%';\n const cardMaxHeight = chrome ? '90vh' : '100%';\n // En móvil la tarjeta se estrecha y se separa de los bordes de la ventana... salvo con\n // chrome:false, donde el contenedor es del host: ahí llena el ancho y NO añade los insets\n // de safe-area (el host ya los aplica; sumarlos otra vez dejaba un hueco muerto bajo el footer).\n const mobileCard = chrome\n ? `.deepdots-popup{width:calc(100% - 24px);max-width:calc(100% - 24px);border-radius:12px;padding:calc(16px + env(safe-area-inset-top)) 16px calc(16px + env(safe-area-inset-bottom)) 16px}`\n : `.deepdots-popup{width:100%;max-width:100%;border-radius:0;padding:16px}`;\n\n // El título viaja como literal JSON y se aplica con textContent: viene de la API y no debe\n // interpolarse en el HTML (mismo criterio anti-inyección que `font.family`).\n const titleJson = jsonForScript(opts.title ?? '');\n // CSS del host: va el último para ganar en cascada. Se corta en `</style>` porque el bloque\n // se cierra ahí y todo lo que siguiera pasaría a interpretarse como HTML.\n const customCss = (opts.surveyCss ?? '').replace(/<\\/\\s*style/gi, '<\\\\/style');\n // `null` = decide la plataforma (style.showProgressBar); true/false = lo fuerza el host.\n const progressPref = opts.showProgressBar === undefined ? 'null' : String(opts.showProgressBar);\n\n // Textos del chrome. Arrancan con el idioma del init; dentro del WebView se re-resuelven con\n // el idioma del survey (`formData.lang[0]`), que es el que la plataforma configura en la\n // integración y con el que `@magicfeedback/native` localiza preguntas y placeholders.\n const initLabels = getLabels(opts.language);\n const initActionLabels = resolveActionLabels(opts.actions, opts.language);\n const backLabel = jsonForScript(initActionLabels.back);\n const startLabel = jsonForScript(initActionLabels.start);\n const completeLabel = jsonForScript(initActionLabels.complete);\n const submitLabel = jsonForScript(initActionLabels.accept);\n const cancelLabel = jsonForScript(initActionLabels.decline);\n // La tabla entera viaja con el HTML: el idioma del survey no se conoce hasta que carga.\n const labelsTableJson = jsonForScript(LABELS);\n // Los labels de la API van aparte para que sigan ganando después de re-resolver.\n const actionOverridesJson = jsonForScript({\n accept: trimmedOrNull(opts.actions?.accept?.label),\n start: trimmedOrNull(opts.actions?.start?.label),\n complete: trimmedOrNull(opts.actions?.complete?.label),\n back: trimmedOrNull(opts.actions?.back?.label),\n decline: trimmedOrNull(opts.actions?.decline?.label),\n });\n const initLangJson = jsonForScript(opts.language ?? '');\n // Dirección del chrome. El survey se voltea solo desde 2.2.22 (`dir=\"rtl\"` en su contenedor);\n // el header/footer/progreso son nuestros y tienen que acompañarlo. Siempre explícito.\n const initDirection = directionFor(opts.language);\n const rtlLocalesJson = jsonForScript(RTL_LOCALES);\n\n return `<!DOCTYPE html><html><head>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta name=\"color-scheme\" content=\"${colorScheme}\"/>\n<style>\n${magicfeedbackCss}\n${fontFaceCss}\nhtml,body{margin:0;padding:0;height:100%;font-family:${fontFamilyValue}}\n/* Apertura diferida: ni la tarjeta ni el velo se pintan hasta que el survey está listo\n (\\`dd-ready\\`). Si el fondo entrara antes, en RN se vería el velo oscuro varios cientos de ms\n antes que el contenido, que es la misma mala sensación que el spinner. */\nbody{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${pos.alignItems};background:transparent;padding:${bodyPadding};box-sizing:border-box}\nbody.dd-ready{background:${bodyBg}}\n#dd-popup{visibility:hidden}\nbody.dd-ready #dd-popup{visibility:visible}\n.deepdots-popup{position:relative;display:flex;flex-direction:column;justify-content:flex-start;background:${cardBg};color-scheme:${colorScheme};border-radius:${cardRadius};padding:24px;box-shadow:${cardShadow};max-width:${cardMaxWidth};width:${cardWidth};min-height:${cardMinHeight};height:${cardHeight};max-height:${cardMaxHeight};box-sizing:border-box}\n.deepdots-popup-header{display:flex;justify-content:space-between;align-items:center;gap:12px;width:100%;flex:0 0 auto}\n/* Neutraliza la regla \\`.deepdots-popup h2\\` del CSS del survey (uppercase, centrado y\n margin-bottom 40px), pensada para los enunciados y no para el título de la cabecera. */\n#dd-title{margin:0;font-family:var(--deepdots-font,'Montserrat',inherit);font-size:17px;font-weight:600;line-height:1.3;color:${textPrimary};text-transform:none;text-align:start;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n#dd-close{background:transparent;border:none;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:8px;cursor:pointer;color:${textPrimary};padding:4px;flex:0 0 auto;margin-inline-start:auto}\n/* Progreso alineado con la cabecera: sin padding horizontal propio, lo marca la tarjeta.\n El vertical es el mismo arriba y abajo, y del mismo valor que el padding de la tarjeta. */\n.deepdots-progress{display:none;flex-direction:column;gap:8px;width:100%;flex:0 0 auto;padding:16px 0;box-sizing:border-box}\n.deepdots-progress-head{display:flex;flex-direction:row;justify-content:space-between;align-items:center;width:100%;gap:8px}\n#dd-progress-label{font-size:13px;line-height:1.2}\n/* \"Question 1\" en negrita y \"of 3\" en regular gris, como el mockup. */\n#dd-progress-current{font-weight:700;color:${textPrimary}}\n#dd-progress-total{font-weight:400;color:${textMuted}}\n#dd-progress-followup{display:none;font-size:12px;font-weight:600;color:#fff;background:rgba(59,130,246,0.44);border-radius:999px;padding:2px 10px}\n.deepdots-progress-track{width:100%;height:4px;border-radius:999px;background:${trackBg};overflow:hidden}\n#dd-progress-bar{height:100%;width:0%;background:#22C55E;border-radius:999px;transition:width 450ms ease}\n/* El popup es una columna: solo el bloque de preguntas hace scroll, para que el footer\n con las acciones quede siempre visible en el borde inferior (antes vivía dentro del\n área scrollable y había que bajar hasta el final para ver \"Send\"). */\n/* Sin sangrado extra: el contenido se alinea con el título y con la barra de progreso (antes\n sumaba 20px a los lados sobre el padding de la tarjeta y quedaba más adentro que la cabecera). */\n/* Sin barra de progreso, este padding-top es el que separa la cabecera de la pregunta. Con\n barra, el JS lo pone a 0 y el hueco lo aporta el padding inferior del bloque de progreso,\n de modo que el espacio por arriba y por abajo de la barra es idéntico. */\n.deepdots-popup-container-content{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;padding:16px 0 0 0;overflow:hidden;box-sizing:border-box}\n/* El enunciado trae margin-top:20px del CSS de surveys, que sumaba al hueco de la barra y\n descompensaba la simetría. Solo se anula en el primero: entre preguntas sigue separando. */\n.deepdots-popup .magicfeedback-questions .magicfeedback-div:first-child .magicfeedback-label{margin-top:0}\n.deepdots-popup-main{display:flex;flex-direction:column;width:100%;flex:1 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden;position:relative}\n#dd-form-wrapper{width:100%;flex:1 1 auto}\n#mf{width:100%;box-sizing:border-box;visibility:hidden}\n.deepdots-popup-main *{max-width:100%;box-sizing:border-box}\n/* El margen lateral lo pone la tarjeta: el CSS del survey lo añade otra vez en el contenedor, en\n el formulario y en el bloque de cada pregunta, y eso deja el enunciado y las opciones más\n adentro que el logo, el título y la barra de progreso. Solo el lateral; el vertical separa las\n preguntas. Mismas reglas que en el popup DOM (renderPopup.ts). */\n.deepdots-popup .magicfeedback-container,\n.deepdots-popup .magicfeedback-form,\n.deepdots-popup .magicfeedback-div{padding-left:0;padding-right:0}\n#dd-logo{max-height:40px;max-width:100%;object-fit:contain;display:block;margin:12px 0 0 0}\n.deepdots-error-hint{display:none;margin:12px 0 0 0;padding:10px 12px;border-radius:6px;background:#FEF3C7;color:#92400E;border:1px solid #FCD34D;font-size:13px}\n/* Pantalla final: el HTML del editor de la plataforma (imagen + texto), centrado. */\n.deepdots-load-error{display:none;width:100%;text-align:center;padding:24px 0;font-size:15px;color:${textPrimary}}\n.deepdots-success{display:none;width:100%;text-align:center;padding:24px 0;color:${textPrimary}}\n.deepdots-success img{max-width:100%;height:auto;margin:0 auto 16px auto;display:block}\n.deepdots-success p{margin:0;font-size:16px;font-weight:600;line-height:1.4}\n.deepdots-popup-footer{display:flex;flex-direction:row-reverse;justify-content:space-between;align-items:center;gap:4px;margin-top:auto;width:100%;padding-top:12px}\n.dd-nav-btn{display:none;border:none;min-height:44px;padding:12px 24px;border-radius:999px;cursor:pointer;font-size:15px;font-weight:600;align-items:center;justify-content:center;text-align:center}\n/* Secundario como botón de texto: sin borde ni fondo, para que el primario sea la única CTA. */\n#dd-back{background:transparent;color:${textMuted};border:none}\n#dd-start,#dd-complete,#dd-submit,#dd-cancel{background:#1E293B;color:#fff;border:none}\n#dd-submit:disabled,#dd-start:disabled,#dd-back:disabled,#dd-complete:disabled{opacity:0.6;cursor:not-allowed}\n.mf-spinner{display:none;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}\n.mf-spinner-circle{width:28px;height:28px;border:3px solid #e0e6ed;border-top-color:#1E293B;border-radius:50%;animation:ddspin 0.9s linear infinite}\n@keyframes ddspin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}\n@media (max-width:640px){\n ${mobileCard}\n /* Apilados: la acción principal (Send/Start/Complete) arriba y Back debajo. El orden del\n DOM ya es ese, así que \\`column\\` (no \\`column-reverse\\`) es lo que lo respeta. */\n .deepdots-popup-footer{flex-direction:column;gap:4px}\n .dd-nav-btn{width:100%}\n .magicfeedback-checkbox-container,.magicfeedback-radio-container{margin:4px 0;padding:4px 8px}\n .magicfeedback-checkbox label,.magicfeedback-radio-container label{font-size:15px}\n .deepdots-popup input[type=\"radio\"]{margin:6px 0 6px 8px}\n .deepdots-popup input[type=\"checkbox\"]{width:16px;height:16px}\n}\n${customCss}\n</style>\n</head><body>\n<div class=\"${popupClass}\" id=\"dd-popup\" dir=\"${initDirection}\">\n <div class=\"deepdots-popup-header\">\n <h2 id=\"dd-title\" class=\"deepdots-popup-title\" hidden></h2>\n <button type=\"button\" id=\"dd-close\" aria-label=\"${initLabels.closeAria}\">\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M5 5L19 19M5 19L19 5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"butt\"/>\n </svg>\n </button>\n </div>\n <div class=\"deepdots-progress\" id=\"dd-progress\">\n <div class=\"deepdots-progress-head\">\n <span id=\"dd-progress-label\"><span id=\"dd-progress-current\"></span><span id=\"dd-progress-total\"></span></span>\n <span id=\"dd-progress-followup\">${initLabels.followUp}</span>\n </div>\n <div class=\"deepdots-progress-track\"><div id=\"dd-progress-bar\"></div></div>\n </div>\n <div class=\"deepdots-popup-container-content\" id=\"dd-content\">\n <div class=\"deepdots-popup-main\" id=\"dd-main\">\n <div id=\"dd-form-wrapper\">\n <div class=\"mf-spinner\" id=\"dd-spinner\" role=\"status\" aria-label=\"${initLabels.loadingAria}\"><div class=\"mf-spinner-circle\"></div></div>\n <div id=\"mf\"></div>\n </div>\n <div class=\"deepdots-load-error\" id=\"dd-load-error\" role=\"alert\"></div>\n <div class=\"deepdots-error-hint\" id=\"dd-error\" role=\"alert\" aria-live=\"polite\"></div>\n </div>\n <div class=\"deepdots-popup-footer\" id=\"dd-footer\">\n <button type=\"button\" class=\"dd-nav-btn\" id=\"dd-submit\"></button>\n <button type=\"button\" class=\"dd-nav-btn\" id=\"dd-back\"></button>\n <button type=\"button\" class=\"dd-nav-btn\" id=\"dd-complete\"></button>\n <button type=\"button\" class=\"dd-nav-btn\" id=\"dd-start\"></button>\n <button type=\"button\" class=\"dd-nav-btn\" id=\"dd-cancel\"></button>\n </div>\n </div>\n</div>\n<script>\n(function(){\n function emit(s){ try{ if(window.ReactNativeWebView){ window.ReactNativeWebView.postMessage(s); } }catch(e){} }\n function emitJSON(n,p){ try{ emit(JSON.stringify({name:n,payload:p||{}})); }catch(e){} }\n window.DeepdotsClose = function(){ emitJSON('popup_close'); };\n var chromeOn=${chrome ? 'true' : 'false'};\n\n var popup=document.getElementById('dd-popup');\n var main=document.getElementById('dd-main');\n var content=document.getElementById('dd-content');\n var footer=document.getElementById('dd-footer');\n var spinner=document.getElementById('dd-spinner');\n var formHost=document.getElementById('mf');\n var formWrapper=document.getElementById('dd-form-wrapper');\n var errorHint=document.getElementById('dd-error');\n var backBtn=document.getElementById('dd-back');\n var startBtn=document.getElementById('dd-start');\n var completeBtn=document.getElementById('dd-complete');\n var submitBtn=document.getElementById('dd-submit');\n var cancelBtn=document.getElementById('dd-cancel');\n var loadErrorEl=document.getElementById('dd-load-error');\n var titleEl=document.getElementById('dd-title');\n var progressEl=document.getElementById('dd-progress');\n var progressLabel=document.getElementById('dd-progress-label');\n var progressCurrent=document.getElementById('dd-progress-current');\n var progressTotal=document.getElementById('dd-progress-total');\n var progressFollowUp=document.getElementById('dd-progress-followup');\n var progressBar=document.getElementById('dd-progress-bar');\n var closeEl=document.getElementById('dd-close');\n\n // --- i18n del chrome -------------------------------------------------------------------\n // La tabla completa viaja embebida porque el idioma del survey (\\`formData.lang[0]\\`) solo se\n // conoce ya dentro del WebView. \\`ddResolveLocale\\` es el espejo del \\`resolveLocale\\` de TS\n // (paridad cubierta por test).\n var DD_LABELS=${labelsTableJson};\n var DD_ACTION_OVERRIDES=${actionOverridesJson};\n ${RESOLVE_LOCALE_JS}\n var DD_LABELS_ACTIVE=DD_LABELS[ddResolveLocale(${initLangJson})];\n var DD_RTL=${rtlLocalesJson};\n function ddIsRtl(lang){ return DD_RTL.indexOf(ddResolveLocale(lang))!==-1; }\n function ddApplyLabels(lang){\n var next=DD_LABELS[ddResolveLocale(lang)];\n if(!next) return;\n DD_LABELS_ACTIVE=next;\n // El survey ya se ha volteado a sí mismo: el chrome tiene que acompañarlo.\n popup.setAttribute('dir', ddIsRtl(lang) ? 'rtl' : 'ltr');\n backBtn.textContent=DD_ACTION_OVERRIDES.back||next.back;\n startBtn.textContent=DD_ACTION_OVERRIDES.start||next.start;\n completeBtn.textContent=DD_ACTION_OVERRIDES.complete||next.complete;\n submitBtn.textContent=DD_ACTION_OVERRIDES.accept||next.accept;\n cancelBtn.textContent=DD_ACTION_OVERRIDES.decline||next.decline;\n loadErrorEl.textContent=next.errorLoad;\n if(closeEl){ closeEl.setAttribute('aria-label',next.closeAria); }\n if(spinner){ spinner.setAttribute('aria-label',next.loadingAria); }\n progressFollowUp.textContent=next.followUp;\n }\n\n function setTitle(t){\n if(!t) return;\n titleEl.textContent=t;\n titleEl.hidden=false;\n }\n setTitle(${titleJson});\n\n // Estado de la barra de progreso. \\`progressPref\\` null = manda la plataforma (style).\n var progressPref=${progressPref};\n var progressEnabled=progressPref===true;\n var progressShowUnit=true;\n var progressUnit='fraction';\n var onStartPage=false;\n\n function updateProgress(p){\n var total=p&&typeof p.total==='number'?p.total:0;\n var progress=p&&typeof p.progress==='number'?p.progress:0;\n // Solo tiene sentido con más de una página, fuera de la pantalla de inicio y sin completar.\n if(!progressEnabled||onStartPage||p&&p.completed||total<=1){\n progressEl.style.display='none';\n // Sin barra, el hueco cabecera→pregunta lo pone el contenido.\n content.style.paddingTop='16px';\n return;\n }\n progressEl.style.display='flex';\n // Con barra, el hueco inferior ya lo pone el bloque de progreso: no duplicarlo.\n content.style.paddingTop='0';\n // Espejo de LineProgressQuestion (MagicSurvey): la barra usa el valor real (las follow-up\n // suman +0.5 y avanzan media casilla) y la etiqueta redondea hacia abajo, porque una\n // follow-up es un paso DENTRO de la misma pregunta, no la siguiente.\n var current=Math.min(total,Math.max(1,progress+1));\n var pct=Math.min(100,Math.max(0,(current/total)*100));\n progressBar.style.width=pct+'%';\n var label=Math.min(total,Math.max(1,Math.floor(progress)+1));\n progressLabel.style.display=progressShowUnit?'block':'none';\n if(progressUnit==='percentage'){\n progressCurrent.textContent=Math.round(pct)+'%';\n progressTotal.textContent='';\n } else {\n progressCurrent.textContent=DD_LABELS_ACTIVE.question+' '+label;\n progressTotal.textContent=' '+DD_LABELS_ACTIVE.of+' '+total;\n }\n progressFollowUp.style.display=p&&p.followup?'inline-block':'none';\n }\n\n backBtn.textContent=${backLabel};\n startBtn.textContent=${startLabel};\n completeBtn.textContent=${completeLabel};\n submitBtn.textContent=${submitLabel};\n cancelBtn.textContent=${cancelLabel};\n\n closeEl.onclick=function(){ emitJSON('popup_close'); };\n backBtn.onclick=function(){ if(window.DeepdotsForm && window.DeepdotsForm.back){ window.DeepdotsForm.back(); } };\n startBtn.onclick=function(){\n if(window.DeepdotsForm && window.DeepdotsForm.startForm){\n window.DeepdotsForm.startForm();\n onStartPage=false;\n updateProgress({ progress:window.DeepdotsForm.progress, total:window.DeepdotsForm.total, completed:false, followup:false });\n }\n };\n completeBtn.onclick=function(){ emitJSON('popup_close'); };\n // Única acción del survey que no carga: cierra igual que la X.\n cancelBtn.onclick=function(){ emitJSON('popup_close'); };\n var surveyCompletedEmitted=false;\n submitBtn.onclick=function(){\n if(surveyCompletedEmitted) return;\n if(window.DeepdotsForm && window.DeepdotsForm.send){ window.DeepdotsForm.send(); }\n };\n\n // Mensaje final configurado en la plataforma (\\`style.successMessage\\`). Se guarda al cargar\n // y se pinta al completar. Va por innerHTML porque es HTML del editor de la plataforma\n // (imagen + texto), igual que ya hace \\`renderStartMessage\\` con el mensaje de inicio.\n var successMessageHtml='';\n function showSuccessScreen(){\n progressEl.style.display='none';\n var wrapper=document.getElementById('dd-form-wrapper');\n if(wrapper){ wrapper.style.display='none'; }\n var done=document.getElementById('dd-success');\n if(!done){\n done=document.createElement('div');\n done.id='dd-success';\n done.className='deepdots-success';\n main.insertBefore(done, errorHint);\n }\n done.innerHTML=successMessageHtml || '<p>Thank you for your feedback!</p>';\n done.style.display='block';\n }\n\n // Profundidad de navegación DENTRO del survey: +1 por página avanzada, -1 al volver.\n // Sustituye a la condición \\`total>1 && progress>0 && progress<total\\`, que escondía el\n // Back siempre que la siguiente pantalla era una follow-up dinámica: las follow-up no\n // entran en el grafo (suman +0.5 al progress y no tocan el total), así que un survey de\n // una pregunta con follow-up tiene total=1 y nunca cumplía \\`total>1\\`.\n var pageDepth=0;\n\n function updateNavButtons(){\n updateButtons(pageDepth>0?'in_progress_next':'in_progress_first');\n }\n\n function updateButtons(state){\n backBtn.style.display='none';\n startBtn.style.display='none';\n submitBtn.style.display='none';\n completeBtn.style.display='none';\n cancelBtn.style.display='none';\n backBtn.style.width='';\n startBtn.style.width='';\n submitBtn.style.width='';\n completeBtn.style.width='';\n if(state==='start'){\n startBtn.style.display='inline-flex';\n startBtn.style.width='100%';\n } else if(state==='in_progress_first'){\n submitBtn.style.display='inline-flex';\n } else if(state==='in_progress_next'){\n backBtn.style.display='inline-flex';\n submitBtn.style.display='inline-flex';\n } else if(state==='completed'){\n completeBtn.style.display='inline-flex';\n completeBtn.style.width='100%';\n } else if(state==='load_error'){\n cancelBtn.style.display='inline-flex';\n }\n }\n\n${PRIMARY_COLOR_JS}\n${BUSY_JS}\n${REVEAL_JS}\n // Foco en la primera pregunta si es de escribir. Al navegar lo pone @magicfeedback/native\n // (autofocus:'navigation'); la apertura la decide el popup, que es quien sabe cuándo se revela.\n // Necesita el popup revelado y el survey cargado, que llegan en cualquier orden, y solo con\n // ratón o trackpad: en táctil sacaría el teclado sin que el usuario haya tocado nada.\n var ddRevealed=false, ddSurveyLoaded=false, ddOpenFocusDone=false;\n function ddAutofocusOnOpen(){\n if(ddOpenFocusDone || !ddRevealed || !ddSurveyLoaded){ return; }\n ddOpenFocusDone=true;\n var fine=typeof window.matchMedia==='function' && window.matchMedia('(pointer: fine)').matches;\n var f=window.DeepdotsForm;\n if(fine && f && typeof f.focusFirstQuestion==='function'){ f.focusFirstQuestion(); }\n }\n // El popup se enseña cuando el survey está pintado; \\`ready\\` deja al host abrir su propio\n // Modal en ese momento en vez de montar un WebView en blanco.\n var ddReveal=ddCreateReveal(popup, function(){\n document.body.classList.add('dd-ready');\n emitJSON('ready');\n ddRevealed=true;\n ddAutofocusOnOpen();\n }, ${REVEAL_TIMEOUT_MS});\n\n function setLoading(isLoading){\n spinner.style.display=isLoading?'flex':'none';\n // El spinner no tapa el survey (28px en posicion absoluta), asi que sin esto el usuario\n // puede seguir cambiando de opcion mientras se envia la pagina, y ese cambio ya no viaja\n // con ella. Se bloquea el wrapper, que es el elemento estable: el div de dentro lo\n // sustituye @magicfeedback/native por su propio contenedor al generar el survey.\n ddSetSurveyBusy(formWrapper, isLoading);\n if(!isLoading){\n formHost.style.visibility='visible';\n // Fin de la carga, sea por survey cargado o por error. Entre páginas es no-op: la\n // revelación solo actúa la primera vez.\n ddReveal.whenPainted();\n }\n footer.style.display=isLoading?'none':'flex';\n backBtn.disabled=isLoading; startBtn.disabled=isLoading; completeBtn.disabled=isLoading; submitBtn.disabled=isLoading;\n if(isLoading){ updateButtons(null); }\n }\n setLoading(true);\n\n // Survey que no ha cargado (406 de /info, sin red, bundle del CDN caído). Antes se emitía\n // \\`loaded\\` y se pintaba un Send sobre un survey vacío. Espejo de KMP: aviso traducido, la X\n // sigue, un único Cancel, y se revela ya (manda \\`ready\\`): no hay nada más que esperar.\n // No es interacción del usuario: el renderer no lo convierte en popup_clicked.\n var ddLoadFailed=false;\n function ddShowLoadError(name, reason){\n if(ddLoadFailed){ return; }\n ddLoadFailed=true;\n var why=(reason && reason.message) ? reason.message : String(reason);\n emitJSON(name, { message: why });\n progressEl.style.display='none';\n formWrapper.style.display='none';\n errorHint.style.display='none';\n loadErrorEl.textContent=DD_LABELS_ACTIVE.errorLoad;\n loadErrorEl.style.display='block';\n setLoading(false);\n updateButtons('load_error');\n ddReveal.reveal();\n }\n\n var initialized=false;\n function initMF(){\n if(initialized || !window.magicfeedback) return initialized;\n initialized=true;\n try{\n window.magicfeedback.init({ debug:false, env:'${env}' });\n var form = window.magicfeedback.form(${sid}, ${pid}, ${profileJson}, ${metaJson});\n window.DeepdotsForm = form;\n var stylesInjected=false;\n form.generate('mf', {\n addButton:false,\n getMetaData:true,\n // La pantalla final la pinta el popup: renderSuccess de @magicfeedback/native usa\n // textContent, así que el mensaje de la plataforma (HTML con imagen) no se vería, y su\n // fallback es un literal genérico que ignora style.successMessage.\n addSuccessScreen:false,\n // Surveys multi-idioma: native pide a la API este idioma (el del init o el del\n // dispositivo); si el survey no lo tiene, la API sirve el idioma por defecto.\n lang:${initLangJson}||undefined,\n // Foco en la primera pregunta de escribir tras Start, Siguiente o Atrás. Las versiones\n // de native sin esta opción la ignoran.\n autofocus:'navigation',\n onLoadedEvent:function(args){\n if(args && args.error){ ddShowLoadError('error:load', args.error); return; }\n // Idioma en el que native muestra el survey: manda sobre el del init para que el\n // chrome no se quede en inglés delante de un survey traducido. Native < 2.3 no lo\n // envía: entonces el primero de formData.lang (el idioma por defecto).\n var shown=args && typeof args.lang==='string' && args.lang.trim() ? args.lang : null;\n var langs=shown ? [shown] : (args && args.formData ? args.formData.lang : null);\n if(langs && langs.length){\n for(var li=0;li<langs.length;li++){\n if(typeof langs[li]==='string' && langs[li].trim()){ ddApplyLabels(langs[li]); break; }\n }\n }\n var s=args && args.formData ? args.formData.style : null;\n if(s && !stylesInjected){\n stylesInjected=true;\n if(s.successMessage){ successMessageHtml=s.successMessage; }\n // La barra de progreso la decide el host (init) y, si no se pronuncia, la plataforma.\n if(progressPref===null){ progressEnabled=s.showProgressBar===true; }\n if(s.showProgressUnit!==undefined){ progressShowUnit=s.showProgressUnit!==false; }\n if(s.progressUnit){ progressUnit=s.progressUnit; }\n if(s.loadingBarColor){ progressBar.style.background=s.loadingBarColor; progressFollowUp.style.background=s.loadingBarColor; }\n if(chromeOn && s.boxBackgroundColor){ popup.style.background=s.boxBackgroundColor; }\n if(s.contentAlign){ main.style.justifyContent = s.contentAlign==='center' ? 'center' : 'flex-start'; }\n // El survey pinta sus controles con --mf-primary, que sale de primaryColor; si la\n // integración solo configura el del botón, el popup mezclaría el color de la marca\n // con el gris azulado por defecto del paquete.\n ddApplySurveyPrimaryColor(popup, s);\n if(s.buttonPrimaryColor){\n submitBtn.style.background=s.buttonPrimaryColor; submitBtn.style.border='none'; submitBtn.style.color='#fff';\n startBtn.style.background=s.buttonPrimaryColor; startBtn.style.border='none'; startBtn.style.color='#fff';\n completeBtn.style.background=s.buttonPrimaryColor; completeBtn.style.border='none'; completeBtn.style.color='#fff';\n }\n if(s.buttonSecondaryColor){\n // Botón de texto: solo tiñe la etiqueta, sin recuperar fondo ni borde.\n backBtn.style.background='transparent'; backBtn.style.color=s.buttonSecondaryColor; backBtn.style.border='none';\n }\n if(s.logo && !document.getElementById('dd-logo')){\n var logoImg=document.createElement('img');\n logoImg.id='dd-logo'; logoImg.src=s.logo; logoImg.alt='Logo';\n if(s.logoSize==='small'){ logoImg.style.maxHeight='30px'; }\n else if(s.logoSize==='medium'){ logoImg.style.maxHeight='50px'; }\n else if(s.logoSize==='large'){ logoImg.style.maxHeight='70px'; }\n // Márgenes lógicos: con un survey RTL el popup va con dir=\"rtl\" y el logo\n // tiene que acompañar al chrome. En LTR el resultado es el mismo de antes.\n if(s.logoPosition==='left'){ logoImg.style.margin='12px 0 0 0'; logoImg.style.setProperty('margin-inline-start','0'); logoImg.style.setProperty('margin-inline-end','16px'); }\n else if(s.logoPosition==='right'){ logoImg.style.margin='12px 0 0 0'; logoImg.style.setProperty('margin-inline-start','auto'); logoImg.style.setProperty('margin-inline-end','0'); }\n else if(s.logoPosition==='center'){ logoImg.style.margin='12px auto 0 auto'; }\n // Encima de la barra de progreso, como hermano del header (espejo de ui/logo.ts en\n // web). Fuera de #dd-main, así el logo no se va con el scroll del formulario.\n popup.insertBefore(logoImg, progressEl);\n }\n if(s.startMessage && s.startMessage!==''){ onStartPage=true; updateButtons('start'); }\n else { updateButtons('in_progress_first'); }\n } else {\n updateButtons('in_progress_first');\n }\n // El total solo se conoce una vez montado el form; sin él no hay barra que pintar.\n updateProgress({ progress:form.progress, total:form.total, completed:false, followup:false });\n emitJSON('loaded');\n setLoading(false);\n // La primera carga es la apertura; las siguientes (tras Start) las enfoca native.\n if(!ddSurveyLoaded){ ddSurveyLoaded=true; ddAutofocusOnOpen(); }\n },\n beforeSubmitEvent:function(){\n setLoading(true);\n emitJSON('before_submit');\n },\n afterSubmitEvent:function(p){\n var error=p && p.error ? (typeof p.error==='string' ? p.error : String(p.error)) : '';\n if(error){\n setLoading(false);\n if(error.toLowerCase().indexOf('no response')!==-1){\n // La página no ha cambiado: el estado de navegación se queda como estaba.\n errorHint.textContent=DD_LABELS_ACTIVE.errorRequired;\n errorHint.style.display='block';\n updateNavButtons();\n return;\n }\n errorHint.textContent=DD_LABELS_ACTIVE.errorSubmit;\n errorHint.style.display='block';\n return;\n }\n errorHint.style.display='none';\n setLoading(false);\n if(p && p.completed){\n surveyCompletedEmitted=true;\n showSuccessScreen();\n updateButtons('completed');\n updateProgress(p);\n emitJSON('survey_completed', p);\n return;\n }\n pageDepth++;\n updateNavButtons();\n updateProgress(p);\n emitJSON('after_submit', p);\n },\n onBackEvent:function(p){\n // Con error (\"No page found\") no hubo navegación: la profundidad no se toca.\n if(!(p&&p.error)){ pageDepth=Math.max(0,pageDepth-1); }\n updateNavButtons();\n // onBackEvent no trae \\`total\\`: se lee del form, que es su dueño.\n updateProgress({ progress:p&&p.progress, total:form.total, completed:false, followup:p&&p.followup });\n emitJSON('back', p);\n }\n }).catch(function(e){ ddShowLoadError('error:init', e); });\n }catch(e){ ddShowLoadError('error:exception', e); }\n return true;\n }\n var s=document.createElement('script'); s.src=${JSON.stringify(cdn)}; s.async=true;\n s.onload=function(){ initMF(); };\n s.onerror=function(){ ddShowLoadError('error:load', 'survey script failed to load'); };\n document.head.appendChild(s);\n var tries=0, t=setInterval(function(){ if(initMF()||++tries>40){ clearInterval(t); } },150);\n})();\n</script>\n</body></html>`;\n}\n","import type { DeepdotsEventType, PopupActions, PopupStyle } from '../types';\nimport type { PopupRenderer, PopupRenderOptions } from './renderer';\nimport { buildSurveyIdentity } from '../tracking/tracking-manager';\nimport { buildSurveyHtml } from '../ui/surveyHtml';\nimport { REVEAL_TIMEOUT_MS } from '../ui/reveal';\nimport { sdkWarn } from '../util/logger';\n\n/**\n * Margen que el aviso nativo de \"listo\" deja por detrás del techo que ya lleva el HTML. El\n * WebView se revela solo a `REVEAL_TIMEOUT_MS`; este respaldo solo debe entrar cuando el WebView\n * no llega ni a ejecutar el HTML (motor que no arranca, documento que no carga), que es el único\n * caso en el que el host se quedaría esperando para siempre.\n */\nexport const NATIVE_READY_GRACE_MS = 500;\n\ntype EmitFn = (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void;\n\n/** Payload que recibe el host para montar el WebView del survey. */\nexport interface ReactNativeSurveyPayload {\n surveyId: string;\n productId: string;\n /** HTML autocontenido para `<WebView source={{ html }}>`. */\n html: string;\n}\n\nexport interface ReactNativeRendererOptions {\n /** Se llama al mostrar un popup: monta el WebView con `payload.html`. */\n onShow?: (payload: ReactNativeSurveyPayload) => void;\n /** Se llama al cerrar el popup: desmonta el WebView. */\n onHide?: () => void;\n /**\n * Se llama cuando el survey ya está pintado dentro del WebView, o pasado el techo de espera.\n *\n * Sirve para no enseñar un WebView en blanco: monta el WebView en cuanto llegue `onShow` (fuera\n * de pantalla o transparente) y ábrelo al recibir `onReady`. El HTML se revela solo de todas\n * formas, así que el host que no lo use sigue funcionando igual.\n */\n onReady?: (payload: { surveyId: string }) => void;\n}\n\n/**\n * Renderer de React Native: el SDK no puede pintar componentes RN, así que entrega el\n * HTML del survey al host (que lo monta en `react-native-webview`) y traduce los\n * mensajes del WebView a eventos de popup del SDK (→ `POST /sdk/popups`, Messaging #18–22).\n *\n * Uso: monta el WebView con `onShow` y enséñalo con `onReady`, para que el usuario no vea el\n * WebView arrancando en blanco (motor + bundle del CDN + fetch del survey).\n * ```tsx\n * const renderer = new ReactNativePopupRenderer({\n * onShow: (p) => setSurvey({ ...p, ready: false }),\n * onReady: () => setSurvey((s) => (s ? { ...s, ready: true } : s)),\n * onHide: () => setSurvey(null),\n * });\n * sdk.setRenderer(renderer);\n * // …\n * // ⚠️ Con `<Modal visible={survey.ready}>` esto NO funciona: React Native no monta los hijos\n * // de un Modal cerrado, así que el WebView no empezaría a cargar y `ready` no llegaría nunca.\n * // El WebView tiene que estar montado y solo invisible.\n * {survey ? (\n * <View style={[StyleSheet.absoluteFill, { opacity: survey.ready ? 1 : 0 }]}\n * pointerEvents={survey.ready ? 'auto' : 'none'}>\n * <WebView source={{ html: survey.html }}\n * onMessage={(e) => renderer.handleMessage(e.nativeEvent.data)} />\n * </View>\n * ) : null}\n * ```\n *\n * Quien prefiera no complicarse puede ignorar `onReady` y abrir su Modal en `onShow`: el HTML se\n * revela solo igualmente, así que el usuario verá el contenedor del host unos cientos de ms antes\n * que la tarjeta, pero nunca el spinner.\n */\nexport class ReactNativePopupRenderer implements PopupRenderer {\n private emitFn: EmitFn | null = null;\n private onCloseFn: (() => void) | null = null;\n private currentSurveyId: string | null = null;\n private partialEmitted = false;\n private readyEmitted = false;\n private readyTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(private options: ReactNativeRendererOptions = {}) {}\n\n init(): void {\n /* nada que preparar */\n }\n\n show(\n surveyId: string,\n productId: string,\n actions: PopupActions | undefined,\n emit: EmitFn,\n onClose: () => void,\n env: string = 'production',\n userId?: string,\n style?: PopupStyle,\n sessionId?: string,\n miniService?: string,\n analyticsFeedbackSessionId?: string,\n renderChrome?: boolean,\n options?: PopupRenderOptions,\n ): void {\n this.emitFn = emit;\n this.onCloseFn = onClose;\n this.currentSurveyId = surveyId;\n this.partialEmitted = false;\n this.readyEmitted = false;\n this.armReadyFallback(surveyId);\n\n const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);\n const html = buildSurveyHtml({\n surveyId,\n productId,\n env,\n profile,\n metadata,\n font: style?.font,\n theme: style?.theme,\n position: style?.position,\n actions,\n // renderChrome:false (init) → HTML sin tarjeta/overlay: el host lo envuelve en su Modal.\n chrome: renderChrome,\n title: options?.title,\n showProgressBar: options?.showProgressBar,\n surveyCss: options?.surveyCss,\n language: options?.language,\n });\n if (this.options.onShow) {\n this.options.onShow({ surveyId, productId, html });\n } else {\n sdkWarn(\n '[Deepdots] ReactNativePopupRenderer has no onShow: pass { onShow } to new ReactNativePopupRenderer({...}) to mount the survey WebView.',\n );\n }\n }\n\n hide(): void {\n this.clearReadyFallback();\n this.options.onHide?.();\n this.currentSurveyId = null;\n }\n\n /** Respaldo del aviso de listo por si el WebView no llega a ejecutar el HTML. */\n private armReadyFallback(surveyId: string): void {\n this.clearReadyFallback();\n if (!this.options.onReady) return;\n this.readyTimer = setTimeout(() => {\n this.readyTimer = null;\n this.emitReady(surveyId);\n }, REVEAL_TIMEOUT_MS + NATIVE_READY_GRACE_MS);\n }\n\n private clearReadyFallback(): void {\n if (this.readyTimer !== null) {\n clearTimeout(this.readyTimer);\n this.readyTimer = null;\n }\n }\n\n /** Idempotente: el WebView puede avisar a la vez que vence el respaldo. */\n private emitReady(surveyId: string): void {\n if (this.readyEmitted) return;\n this.readyEmitted = true;\n this.clearReadyFallback();\n this.options.onReady?.({ surveyId });\n }\n\n /**\n * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a\n * eventos del SDK: primera interacción → `popup_clicked` (PARTIAL), completado →\n * `survey_completed` (COMPLETED) y cierra el popup.\n */\n handleMessage(raw: string): void {\n const surveyId = this.currentSurveyId;\n if (!surveyId || !this.emitFn) return;\n\n let name: string | undefined;\n try {\n name = (JSON.parse(raw) as { name?: string }).name;\n } catch {\n name = raw; // mensajes simples (\"error:load\")\n }\n\n switch (name) {\n case 'ready':\n // El survey ya está pintado dentro del WebView. NO es interacción del usuario: no debe\n // marcar el popup como PARTIAL.\n this.emitReady(surveyId);\n break;\n case 'loaded':\n // Solo avisa de que el survey ha cargado: tampoco es interacción. Antes contaba como\n // la primera, y cada popup que se abría quedaba como PARTIAL en backend.\n break;\n case 'before_submit':\n case 'after_submit':\n case 'back':\n if (!this.partialEmitted) {\n this.partialEmitted = true;\n this.emitFn('popup_clicked', surveyId, { action: 'partial' });\n }\n break;\n case 'survey_completed':\n // Solo reporta el estado COMPLETED. NO cierra: el WebView acaba de pintar la pantalla\n // final del survey y cerrar aquí la hacía invisible. El cierre llega después con\n // `popup_close`, cuando el usuario pulsa el botón de completar.\n this.emitFn('survey_completed', surveyId);\n break;\n case 'popup_close':\n this.onCloseFn?.();\n break;\n default:\n // `error:load` / `error:init` / `error:exception`: el survey no ha cargado y el WebView\n // ya enseña el aviso con su Cancel. El HTML manda `ready` al revelarlo; esto cubre un\n // WebView que solo avise del error, para que el host que difiere el montaje lo enseñe.\n // Tampoco es interacción del usuario: nada de popup_clicked.\n if (typeof name === 'string' && name.startsWith('error:')) this.emitReady(surveyId);\n break;\n }\n }\n}\n\n/** Detección simple de entorno React Native. */\nexport function isReactNativeEnv(): boolean {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return typeof navigator !== 'undefined' && (navigator as any).product === 'ReactNative';\n}\n\n/** Factoría usada por `createDefaultRenderer` cuando se detecta RN (el host debería pasar onShow). */\nexport function createReactNativeRenderer(): PopupRenderer {\n return new ReactNativePopupRenderer();\n}\n","/**\n * Precalentado del chunk de `renderPopup`.\n *\n * `renderPopup` se carga por dynamic import para que importar el SDK sea seguro en React Native\n * y en SSR, pero eso deja su descarga (DOM del popup + `@magicfeedback/native` + el CSS\n * vendorizado, ~238 KB sin comprimir) dentro del camino crítico: empieza cuando el trigger ya ha\n * disparado y el usuario está mirando. Traerlo en idle en cuanto se sabe que hay algún popup que\n * puede abrirse deja el import de `show()` resuelto al instante.\n *\n * Es oportunista por diseño: si falla, no rompe nada porque `show()` hace su propio import.\n */\n\ntype Loader = () => Promise<unknown>;\n\nconst defaultLoader: Loader = () => import('./renderPopup');\n\n/** Precalentado en curso o ya resuelto. Null tras un fallo, para permitir reintento. */\nlet pending: Promise<void> | null = null;\n\n/**\n * Descarga el chunk una sola vez por sesión. Nunca rechaza: un precalentado que falla es un\n * no-evento, la carga de verdad la hará `show()`.\n */\nexport function preloadRenderPopup(loader: Loader = defaultLoader): Promise<void> {\n if (pending) return pending;\n try {\n // El loader se invoca ya, no en un microtask: el import debe empezar en este tick.\n pending = loader().then(() => undefined).catch(() => { pending = null; });\n } catch {\n pending = null;\n return Promise.resolve();\n }\n return pending;\n}\n\n/**\n * Agenda el precalentado para cuando el navegador esté ocioso, para no competir con el render\n * de la página del host. `requestIdleCallback` no existe en Safari, ahí basta con salir del\n * tick actual.\n */\nexport function schedulePreloadRenderPopup(loader?: Loader): void {\n const run = () => { void preloadRenderPopup(loader); };\n const idle = (globalThis as { requestIdleCallback?: (cb: () => void, opts?: { timeout: number }) => unknown }).requestIdleCallback;\n if (typeof idle === 'function') {\n idle(run, { timeout: 2000 });\n return;\n }\n setTimeout(run, 0);\n}\n","import {DeepdotsEventType, PopupActions, PopupStyle} from '../types';\nimport { isReactNativeEnv, createReactNativeRenderer } from './react-native-renderer';\nimport { schedulePreloadRenderPopup } from '../ui/preload';\n// renderPopup (DOM + @magicfeedback/native) se carga PEREZOSAMENTE solo al mostrar\n// un popup en navegador, para que importar el SDK sea seguro en React Native/SSR.\n\n/**\n * Ajustes de presentación que no vienen del estilo del popup. Van agrupados para no seguir\n * alargando la lista posicional de `show`, que ya arrastra doce parámetros.\n */\nexport interface PopupRenderOptions {\n /**\n * Título de la cabecera: el `title` de la definición del popup, tal cual llega de la API.\n * Varía por popup y vacío es un valor válido (la cabecera se queda solo con la X).\n */\n title?: string;\n /** Barra de progreso: `undefined` deja decidir a la plataforma, `true`/`false` la fuerzan. */\n showProgressBar?: boolean;\n /** CSS del host, inyectado el último: gana sobre el del SDK y el de `@magicfeedback/native`. */\n surveyCss?: string;\n /**\n * Idioma con el que arranca el chrome del popup (botones, progreso, aria-labels, errores).\n * Es el idioma resuelto en `init()` (host > navigator > Intl). Lo pisa el idioma del survey\n * (`formData.lang[0]`) en cuanto este carga, y ambos los pisa `actions.*.label` de la API.\n */\n language?: string;\n /**\n * Nonce de la Content-Security-Policy del host (`DeepdotsInitParams.cspNonce`). Se pone en\n * cada `<style>` que inyecta el popup DOM; sin él, una CSP con `style-src 'nonce-…'` bloquea\n * la hoja del survey. No aplica a React Native (el survey vive en su propio WebView).\n */\n cspNonce?: string;\n}\n\nexport interface PopupRenderer {\n /** Preparar recursos si aplica */\n init?(): void;\n /**\n * Traer por adelantado lo que la apertura necesitará, en idle. Opcional: solo lo implementan\n * los renderers cuyo coste de apertura se paga delante del usuario.\n */\n preload?(): void;\n /** Mostrar popup */\n show(\n surveyId: string,\n productId: string,\n actions: PopupActions | undefined,\n emit: (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void,\n onClose: () => void,\n env?: string,\n userId?: string,\n style?: PopupStyle,\n sessionId?: string,\n miniService?: string,\n analyticsFeedbackSessionId?: string,\n renderChrome?: boolean,\n options?: PopupRenderOptions,\n ): void;\n /** Ocultar popup */\n hide(): void;\n}\n\n/** Renderer que no hace nada (SSR / entornos sin DOM) */\nexport class NoopPopupRenderer implements PopupRenderer {\n show(): void { /* no-op */ }\n hide(): void { /* no-op */ }\n}\n\n/** Renderer para navegadores usando la implementación actual basada en renderPopup */\nexport class BrowserPopupRenderer implements PopupRenderer {\n private container: HTMLElement | null = null;\n private visible = false;\n\n init(): void {\n if (typeof document === 'undefined') return;\n if (!this.container) {\n this.container = document.createElement('div');\n this.container.id = 'deepdots-popup-container';\n this.container.style.cssText = `\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: none;\n z-index: 999999;\n background: rgba(0, 0, 0, 0.5);\n justify-content: center;\n align-items: center;\n `;\n }\n if (!document.body.contains(this.container)) {\n document.body.appendChild(this.container);\n }\n }\n\n /**\n * Descarga el chunk de `renderPopup` antes de que ningún trigger dispare, para que abrir el\n * popup no empiece por esperar ~238 KB de JS.\n */\n preload(): void {\n schedulePreloadRenderPopup();\n }\n\n show(\n surveyId: string,\n productId: string,\n actions: PopupActions | undefined,\n emit: (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void,\n onClose: () => void,\n env: string = 'production',\n userId?: string,\n style?: PopupStyle,\n sessionId?: string,\n miniService?: string,\n analyticsFeedbackSessionId?: string,\n _renderChrome?: boolean, // solo aplica a RN (WebView); el popup DOM web siempre lleva su chrome\n options?: PopupRenderOptions,\n ): void {\n if (this.visible) return;\n if (!this.container || !document.body.contains(this.container)) this.init();\n if (!this.container) return; // aún sin DOM\n this.visible = true;\n const container = this.container;\n void import('../ui/renderPopup').then(({ renderPopup }) => {\n renderPopup(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService, analyticsFeedbackSessionId, options);\n });\n }\n\n hide(): void {\n this.visible = false;\n if (this.container) {\n this.container.style.display = 'none';\n // `renderPopup` abre el contenedor invisible y lo revela al cargar el survey; si se cierra\n // antes de esa revelación, el estado oculto no puede quedarse pegado al contenedor.\n this.container.style.visibility = '';\n this.container.style.pointerEvents = '';\n this.container.innerHTML = '';\n }\n }\n}\n\n/** Factoría para obtener renderer por defecto */\nexport function createDefaultRenderer(): PopupRenderer {\n if (typeof document === 'undefined') {\n // Entorno sin DOM: probar RN primero\n if (isReactNativeEnv()) return createReactNativeRenderer();\n return new NoopPopupRenderer();\n }\n // Si estamos en React Native y hay WebView con document emulado, aún queremos RN renderer\n if (isReactNativeEnv()) return createReactNativeRenderer();\n return new BrowserPopupRenderer();\n}\n","/**\n * Capa de Analytics (eventos GA-style) — canal SEPARADO del feedback de survey.\n *\n * El feedback de survey se envía con `magicfeedback.send()`. La analítica de\n * navegación/comportamiento (page views, mini-services, tasks/funnels, meaningful\n * interactions, findability…) va por un ENDPOINT PROPIO, vinculada por `user_id`.\n *\n * De momento NO se hace la llamada: el `sink` por defecto hace `console.log` del\n * payload exacto que se enviaría (dry-run), para poder verificarlo. Cuando el\n * endpoint exista, se sustituye el sink por un POST.\n */\n\nimport type { DeviceInfo } from './device-info';\n\nexport interface AnalyticsEvent {\n name: string;\n timestamp: number;\n params?: Record<string, unknown>;\n}\n\nexport interface AnalyticsEnvelope {\n /** Clave pública del cliente (auth). */\n publicKey?: string;\n /** Id que vincula la analítica al usuario (el backend cose por aquí). */\n userId: string | null;\n /** Session id del backend si ya se conoce (puede ir null). */\n sessionId: string | null;\n context: {\n platform: string; // 'web' | 'android' | 'ios' (RN inyecta la suya)\n language?: string;\n /** Info de dispositivo (Technology #11–13). */\n device?: DeviceInfo;\n /** User attributes del cliente, usados para breakdowns (registration_status, pass_type, …). */\n attributes: Record<string, string>;\n /** Métricas del host: valores medibles persistentes → van en `feedback.metrics` del body. */\n metrics?: Record<string, string>;\n };\n events: AnalyticsEvent[];\n}\n\n/** Contexto del flush, para que el sink pueda adaptar el transporte. */\nexport interface AnalyticsFlushMeta {\n /**\n * `true` cuando el flush ocurre porque la página/app se está cerrando. El sink debe usar\n * un transporte que sobreviva al unload (`fetch` con `keepalive`) y no puede esperar\n * respuestas.\n */\n final?: boolean;\n /**\n * `true` cuando este es el ÚLTIMO lote de la sesión (cierre de página, app a background,\n * cambio de usuario, tracking desactivado). El sink lo envía con `completed:true` y olvida\n * el `sessionId`, para que el lote siguiente abra un registro nuevo.\n */\n sessionEnd?: boolean;\n /**\n * `true` para olvidar la sesión SIN enviar nada (el payload llega vacío): el backend ya la\n * cerró por inactividad. El sink descarta su `sessionId` para que el lote siguiente abra un\n * registro nuevo, igual que tras `sessionEnd`, pero sin POST.\n */\n resetSession?: boolean;\n}\n\n/**\n * Envío del lote. Si devuelve una promesa que **rechaza**, el manager re-encola los\n * eventos para reintentarlos en el siguiente flush (fallo transitorio: red o 5xx).\n * Resolver = lote entregado o descartado definitivamente (no se reintenta).\n */\nexport type AnalyticsSink = (payload: AnalyticsEnvelope, meta?: AnalyticsFlushMeta) => void | Promise<void>;\n\n/** Identidad resuelta por el tracking, inyectada al construir el payload. */\nexport interface AnalyticsIdentity {\n userId: string | null;\n sessionId: string | null;\n}\n\n/**\n * Sink de dry-run: NO envía nada, solo vuelca lo que se enviaría por el `log` dado\n * (default `console.log`, o el logger inyectado por el host en init()).\n */\nexport function createDryRunSink(log: (...args: unknown[]) => void = console.log): AnalyticsSink {\n return (payload, meta) => {\n if (meta?.resetSession) return;\n log(\n '[DeepdotsAnalytics] (dry-run · NOT sent · no init.analytics) POST /sdk/feedback →',\n JSON.stringify(payload, null, 2),\n );\n };\n}\n\n/** Sink por defecto: NO envía nada, solo pinta por consola lo que se enviaría. */\nexport const dryRunSink: AnalyticsSink = createDryRunSink();\n\nexport interface AnalyticsManagerOptions {\n sink?: AnalyticsSink;\n now?: () => number;\n publicKey?: string;\n language?: string;\n device?: DeviceInfo;\n /** Plataforma del envelope (default 'web'; RN inyecta 'android'/'ios'). */\n platform?: string;\n /** Nº máximo de eventos en buffer antes de solicitar un flush automático (default 20). */\n maxBatchSize?: number;\n /**\n * Techo de eventos retenidos en memoria contando los re-encolados por fallo de envío\n * (default 200). Al superarlo se descartan los más antiguos.\n */\n maxBufferedEvents?: number;\n /** Callback invocado cuando el buffer alcanza `maxBatchSize`. El caller hace el flush real. */\n onFlushNeeded?: () => void;\n}\n\nexport class AnalyticsManager {\n private sink: AnalyticsSink;\n private now: () => number;\n private publicKey?: string;\n private language?: string;\n private device?: DeviceInfo;\n private platform: string;\n\n private events: AnalyticsEvent[] = [];\n private attributes: Record<string, string> = {};\n /** Métricas del host (valores medibles persistentes). Sobrescriben por key; se reenvían en cada flush. */\n private metrics: Record<string, string> = {};\n /** Mini-services activos: nombre → timestamp de entrada. El orden de inserción marca el \"más reciente\". */\n private activeMiniServices = new Map<string, number>();\n private maxBatchSize: number;\n private maxBufferedEvents: number;\n private onFlushNeeded?: () => void;\n\n constructor(options: AnalyticsManagerOptions = {}) {\n this.sink = options.sink ?? dryRunSink;\n this.now = options.now ?? (() => Date.now());\n this.publicKey = options.publicKey;\n this.language = options.language;\n this.device = options.device;\n this.platform = options.platform ?? 'web';\n this.maxBatchSize = options.maxBatchSize ?? 20;\n this.maxBufferedEvents = options.maxBufferedEvents ?? 200;\n this.onFlushNeeded = options.onFlushNeeded;\n }\n\n /** Actualiza campos de device en runtime (p.ej. country/city tras resolver geo). */\n updateDevice(partial: Partial<DeviceInfo>): void {\n this.device = { ...this.device, ...partial } as DeviceInfo;\n }\n\n /** Mezcla user attributes (se coercionan a string). Mutable en runtime. */\n setUserAttributes(attrs: Record<string, string | number | boolean>): void {\n for (const [k, v] of Object.entries(attrs)) {\n if (!k) continue;\n this.attributes[k] = String(v);\n }\n }\n\n /**\n * Registra/actualiza una métrica (valor medible). Persistente: se reenvía en cada flush.\n * Repetir la misma key SOBRESCRIBE; el valor se coerciona a string; ignora key vacía.\n */\n setMetric(key: string, value: string | number | boolean): void {\n if (!key) return;\n this.metrics[key] = String(value);\n }\n\n /**\n * Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`.\n * Admite varios activos a la vez (concurrencia); el \"actual\" para etiquetar es el más reciente.\n * Reentrar con un nombre ya activo refresca su orden y su tiempo de entrada.\n */\n enterMiniService(name: string, entryPointType?: string): void {\n this.activeMiniServices.delete(name); // reinsertar = pasa a ser el más reciente\n this.activeMiniServices.set(name, this.now());\n this.track('deepdots_mini_service_enter', { entry_point_type: entryPointType ?? null });\n }\n\n /**\n * Cierra el mini-service `name` emitiendo `mini_service_exit` con su duración (#27).\n * No-op si ese nombre no está activo. Cierre coherente con concurrencia.\n */\n exitMiniService(name: string): void {\n const enteredAt = this.activeMiniServices.get(name);\n if (enteredAt === undefined) return;\n const durationSeconds = Math.max(0, Math.round((this.now() - enteredAt) / 1000));\n this.activeMiniServices.delete(name); // dejar de etiquetar con este antes de emitir\n this.track('deepdots_mini_service_exit', { mini_service: name, duration_seconds: durationSeconds });\n }\n\n /** Cierra TODOS los mini-services activos (orden LIFO). Para el cierre por lifecycle (background/cierre). */\n exitAllMiniServices(): void {\n for (const name of Array.from(this.activeMiniServices.keys()).reverse()) {\n this.exitMiniService(name);\n }\n }\n\n /**\n * Olvida la sesión en curso SIN emitir nada: vacía el buffer y los mini-services activos\n * (sin `mini_service_exit`). Para una sesión que el backend ya cerró por inactividad: lo\n * que quedara pendiente pertenecía a ella y no debe llegar con la sesión nueva.\n */\n discardSession(identity: AnalyticsIdentity): void {\n this.events = [];\n this.activeMiniServices.clear();\n try {\n void this.sink({ ...this.buildPayload(identity), events: [] }, { resetSession: true });\n } catch {\n /* el sink no debe romper el ciclo de vida */\n }\n }\n\n /**\n * Olvida lo que pertenecía al usuario anterior (user attributes + métricas) al cambiar de\n * usuario. NO toca el buffer de eventos: el cierre de sesión ya lo vació con la identidad\n * vieja, y lo que llegue después es del usuario nuevo.\n */\n resetUserScope(): void {\n this.attributes = {};\n this.metrics = {};\n }\n\n /** Registra un evento de analítica (modelo GA: nombre + params). */\n track(name: string, params?: Record<string, unknown>): void {\n const current = this.getMiniService();\n const merged: Record<string, unknown> = {\n ...(current ? { mini_service: current } : {}),\n ...(params ?? {}),\n };\n this.events.push({\n name,\n timestamp: this.now(),\n params: Object.keys(merged).length ? merged : undefined,\n });\n this.enforceBufferCap();\n if (this.events.length >= this.maxBatchSize) {\n this.onFlushNeeded?.();\n }\n }\n\n /** Mini-service actual (el más reciente aún activo) para etiquetar eventos + metadata del survey (#33). */\n getMiniService(): string | null {\n let last: string | null = null;\n for (const name of this.activeMiniServices.keys()) last = name; // última clave = más reciente\n return last;\n }\n\n /** Nº de eventos pendientes de flush. */\n pending(): number {\n return this.events.length;\n }\n\n /** Construye el envelope que se enviaría al endpoint de analytics. */\n buildPayload(identity: AnalyticsIdentity): AnalyticsEnvelope {\n return {\n publicKey: this.publicKey,\n userId: identity.userId,\n sessionId: identity.sessionId,\n context: {\n platform: this.platform,\n language: this.language,\n device: this.device,\n attributes: { ...this.attributes },\n metrics: { ...this.metrics },\n },\n events: [...this.events],\n };\n }\n\n /**\n * Envía (vía sink) el lote acumulado y vacía el buffer. No-op si no hay eventos.\n *\n * Si el sink falla de forma transitoria (promesa rechazada o excepción síncrona) el lote\n * se **re-encola** al principio del buffer y se reintenta en el siguiente flush: un error\n * de red o un 5xx no debe hacer desaparecer los eventos.\n */\n flush(identity: AnalyticsIdentity, meta?: AnalyticsFlushMeta): AnalyticsEnvelope | null {\n if (this.events.length === 0) return null;\n const batch = this.events;\n const payload = this.buildPayload(identity);\n this.events = [];\n\n let result: void | Promise<void>;\n try {\n // sin meta se invoca con un solo argumento (compatibilidad con sinks del host)\n result = meta ? this.sink(payload, meta) : this.sink(payload);\n } catch {\n this.requeue(batch);\n return payload;\n }\n if (result && typeof (result as Promise<void>).then === 'function') {\n (result as Promise<void>).catch(() => this.requeue(batch));\n }\n return payload;\n }\n\n /**\n * Devuelve un lote fallido al buffer, delante de lo que haya llegado mientras se enviaba\n * (orden cronológico). Con el buffer lleno se descartan los eventos más antiguos.\n */\n private requeue(batch: AnalyticsEvent[]): void {\n this.events = [...batch, ...this.events];\n this.enforceBufferCap();\n }\n\n /** Techo del buffer: si el envío lleva rato fallando, se sacrifica lo más antiguo. */\n private enforceBufferCap(): void {\n const overflow = this.events.length - this.maxBufferedEvents;\n if (overflow > 0) this.events.splice(0, overflow);\n }\n}\n","/**\n * Mapeo del envelope de analytics → body de `POST /sdk/feedback`.\n *\n * La analítica se envía como un Feedback del modelo del Surveys SDK, agrupado por una\n * INTEGRACIÓN creada en la plataforma. Se manda en streaming con `completed:false`; el\n * backend cose por `sessionId` + `user_id`. El ÚLTIMO lote de una sesión (cierre de página,\n * app a background, cambio de usuario) va con `completed:true`: cierra el registro y el\n * siguiente lote omite el `sessionId` viejo para que el backend abra uno nuevo.\n * `feedback.finished` se queda siempre en `false` (no es la señal de cierre acordada).\n *\n * Encoding:\n * - todo va en `feedback.metadata`: contexto (user_id, session_id, platform…) + eventos\n * - cada entrada usa `value: string[]` (array de un elemento)\n * - eventos: {key: nombre_evento, value: [JSON(timestamp + params)]}\n * - identidad (user_id) → `profile` como `external-user-id`\n * - métricas del host (setMetric) → `feedback.metrics` (mismo shape que metadata, sin prefijo)\n * - `answers` vacío\n * - `text` vacío\n */\n\nimport type { AnalyticsEnvelope, AnalyticsSink } from './analytics-manager';\n\nexport interface AnalyticsKeys {\n publicKey: string;\n integration: string;\n}\n\nexport interface FeedbackKV {\n key: string;\n value: string[];\n}\n\nexport interface AnalyticsFeedbackBody {\n feedback: {\n text: string;\n answers: FeedbackKV[];\n metrics: FeedbackKV[];\n metadata: FeedbackKV[];\n profile: FeedbackKV[];\n finished: boolean;\n };\n publicKey: string;\n integration: string;\n completed: boolean;\n finished?: boolean;\n /** sessionId devuelto por el primer POST; permite que el backend agrupe todos los eventos en un solo registro. */\n sessionId?: string;\n}\n\n/** Inserta un par key/value en la lista solo si el valor está definido y no es vacío. */\nfunction pushKV(list: FeedbackKV[], key: string, value: unknown): void {\n if (value === undefined || value === null || value === '') return;\n list.push({ key, value: [String(value)] });\n}\n\nexport interface BuildBodyOptions {\n /**\n * `true` en el último lote de la sesión → `completed:true` (cierra el registro en backend).\n * El body SÍ lleva el `sessionId` actual (es el registro que se cierra); es el lote\n * SIGUIENTE el que lo omite.\n */\n sessionEnd?: boolean;\n}\n\nexport function buildAnalyticsFeedbackBody(\n envelope: AnalyticsEnvelope,\n keys: AnalyticsKeys,\n feedbackSessionId?: string,\n options?: BuildBodyOptions,\n): AnalyticsFeedbackBody {\n const { context } = envelope;\n\n const profile: FeedbackKV[] = [];\n pushKV(profile, 'external-user-id', envelope.userId);\n\n // Todo va en metadata: contexto del sistema (prefijo deepdots_) + atributos de usuario + eventos\n const metadata: FeedbackKV[] = [];\n pushKV(metadata, 'deepdots_user_id', envelope.userId);\n pushKV(metadata, 'deepdots_session_id', envelope.sessionId);\n pushKV(metadata, 'deepdots_platform', context.platform);\n pushKV(metadata, 'deepdots_language', context.language);\n if (context.device) {\n const d = context.device;\n pushKV(metadata, 'deepdots_device_type', d.device_type);\n pushKV(metadata, 'deepdots_os_version', d.os_version);\n pushKV(metadata, 'deepdots_device_model', d.device_model);\n pushKV(metadata, 'deepdots_app_version', d.app_version);\n pushKV(metadata, 'deepdots_user_agent', d.user_agent);\n pushKV(metadata, 'deepdots_timezone', d.timezone);\n pushKV(metadata, 'deepdots_referrer', d.referrer);\n pushKV(metadata, 'deepdots_viewport_size', d.viewport_size);\n pushKV(metadata, 'deepdots_screen_resolution', d.screen_resolution);\n pushKV(metadata, 'deepdots_pixel_ratio', d.pixel_ratio);\n pushKV(metadata, 'deepdots_entry_type', d.entry_type);\n pushKV(metadata, 'deepdots_page_load_ms', d.page_load_ms);\n pushKV(metadata, 'deepdots_connection_type', d.connection_type);\n pushKV(metadata, 'deepdots_country', d.country);\n pushKV(metadata, 'deepdots_city', d.city);\n }\n for (const [k, v] of Object.entries(context.attributes ?? {})) {\n pushKV(metadata, k, v);\n }\n for (const e of envelope.events) {\n metadata.push({\n key: e.name,\n value: [JSON.stringify({ timestamp: e.timestamp, ...(e.params ?? {}) })],\n });\n }\n\n // Métricas del host → campo dedicado `feedback.metrics` (mismo shape que metadata, sin prefijo).\n const metrics: FeedbackKV[] = [];\n for (const [k, v] of Object.entries(context.metrics ?? {})) {\n pushKV(metrics, k, v);\n }\n\n return {\n feedback: {\n text: '',\n answers: [],\n metrics,\n metadata,\n profile,\n finished: false,\n },\n publicKey: keys.publicKey,\n integration: keys.integration,\n // Único marcador de cierre acordado con backend: el último lote de la sesión.\n completed: options?.sessionEnd === true,\n ...(feedbackSessionId ? { sessionId: feedbackSessionId } : {}),\n };\n}\n\nexport interface FeedbackSinkOptions {\n /** Base URL del backend (api(.|-dev.)deepdots.com) ya resuelta por el entorno. */\n baseUrl: string;\n keys: AnalyticsKeys;\n /** Logger opcional (gated por debug). */\n log?: (...args: unknown[]) => void;\n /** fetch inyectable (tests / RN). */\n fetchImpl?: typeof fetch;\n /**\n * @deprecated Se ignora. El flush final salía por `sendBeacon`, que va SIEMPRE con\n * credenciales; contra la API (`Access-Control-Allow-Origin: *` + `Allow-Credentials:\n * true`) el preflight fallaba y el lote de cierre, el único con `completed:true`, no\n * llegaba nunca. Ahora sale por `fetch` con `keepalive`. Se mantiene en el tipo para no\n * romper a quien lo pase.\n */\n sendBeaconImpl?: (url: string, body: Blob | string) => boolean;\n /**\n * Registro en el que seguir desde el primer lote: el de una sesión suspendida por un\n * `pagehide` y reanudada tras recargar la página (ver `suspended-session.ts`).\n */\n initialSessionId?: string;\n /** Callback invocado cuando el backend devuelve un nuevo sessionId (primer POST). */\n onSessionId?: (id: string) => void;\n /** Callback invocado al cerrar la sesión (`completed:true`): el sessionId cacheado se olvida. */\n onSessionReset?: () => void;\n /**\n * Callback invocado cuando el backend acepta el lote de cierre, con el sessionId del\n * registro cerrado: el que llevaba el lote o, en una sesión de un solo lote, el que\n * devuelve la respuesta. Ese registro se convierte en Feedback en segundos.\n */\n onSessionClosed?: (id: string) => void;\n}\n\n/** Límite práctico de `fetch({keepalive:true})` (~64KB en Chrome). */\nconst KEEPALIVE_MAX_BYTES = 60_000;\n\n/** Un fallo transitorio merece reintento; un 4xx (payload/claves/Contact) no. */\nfunction isRetryableStatus(status: number): boolean {\n return status >= 500 || status === 408 || status === 429;\n}\n\nasync function safeBodyText(res: Response): Promise<string> {\n try {\n return (await res.text()).slice(0, 500);\n } catch {\n return '';\n }\n}\n\n/**\n * Sink real: transforma el envelope y hace `POST {baseUrl}/sdk/feedback`.\n *\n * Stateful: cachea el `sessionId` que devuelve el backend en la primera respuesta y lo\n * envía en los siguientes POSTs para que el backend agrupe todos los eventos en un único\n * registro de feedback.\n *\n * Garantías de entrega:\n * - mientras no se conozca el `sessionId`, los lotes se **serializan** (esperan la primera\n * respuesta): dos POSTs a la vez sin `sessionId` crearían dos registros y partirían los datos;\n * - `keepalive` para que el navegador no cancele el POST al navegar fuera ni al cerrar la\n * página, también en el flush final (sin `sendBeacon`: ver `sendBeaconImpl`);\n * - sin credenciales, que es lo único que pasa el CORS de la API (`Allow-Origin: *`);\n * - la promesa **rechaza** en fallo transitorio (red, 5xx, 408, 429) para que el\n * `AnalyticsManager` re-encole el lote; un 4xx se loguea y se descarta.\n */\nexport function createFeedbackSink(options: FeedbackSinkOptions): AnalyticsSink {\n let feedbackSessionId: string | undefined = options.initialSessionId || undefined;\n /** Primer POST (aún sin sessionId) en vuelo: los siguientes lotes lo esperan. */\n let firstPostInFlight: Promise<void> | null = null;\n /**\n * Sube cada vez que se cierra u olvida una sesión. Una respuesta que llega con otra\n * generación es de una sesión ya cerrada (p. ej. el primer POST seguía en vuelo en el\n * `page_hide`) y no puede volver a cachear su sessionId ni avisarlo como abierto.\n */\n let generation = 0;\n\n const url = `${options.baseUrl}/sdk/feedback`;\n\n const post = async (body: AnalyticsFeedbackBody, final: boolean, sessionEnd = false): Promise<void> => {\n const postGeneration = generation;\n const json = JSON.stringify(body);\n const small = json.length <= KEEPALIVE_MAX_BYTES;\n\n // Sin `sendBeacon` a propósito, también en el cierre de página (ver `sendBeaconImpl`):\n // `keepalive` sobrevive igual al unload y, sin credenciales, pasa el preflight de la API.\n if (final) options.log?.('[DeepdotsAnalytics] final flush via fetch keepalive');\n\n const f = options.fetchImpl ?? (typeof fetch !== 'undefined' ? fetch : undefined);\n if (!f) {\n options.log?.('[DeepdotsAnalytics] no fetch available; payload not sent', body);\n return;\n }\n\n const res = await f(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: json,\n // La API autentica por `publicKey` en el body y responde `Allow-Origin: *`: una\n // petición con credenciales fallaría el CORS. Explícito para no depender del default.\n credentials: 'omit',\n // keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.\n ...(small ? { keepalive: true } : {}),\n });\n\n if (!res.ok) {\n const detail = await safeBodyText(res);\n if (isRetryableStatus(res.status)) {\n // Rechazar → el manager re-encola el lote y se reintenta en el siguiente flush.\n throw new Error(`POST /sdk/feedback ${res.status} (retryable): ${detail}`);\n }\n options.log?.(\n `[DeepdotsAnalytics] POST /sdk/feedback rejected with ${res.status}; batch DISCARDED:`,\n detail,\n );\n return;\n }\n\n // El POST de cierre devuelve el sessionId del registro que acabamos de cerrar:\n // NO se re-cachea (si no, el lote siguiente volvería a apuntar al registro cerrado),\n // pero se avisa por onSessionClosed, porque ese registro ya va a ser un Feedback.\n if (sessionEnd) {\n let closedId = body.sessionId;\n try {\n const data = (await res.json()) as { sessionId?: string };\n closedId = data?.sessionId ?? closedId;\n } catch {\n /* respuesta sin JSON: vale el sessionId que llevaba el lote */\n }\n try {\n if (closedId) options.onSessionClosed?.(closedId);\n } catch {\n // Un fallo del callback no puede convertir el lote entregado en uno fallido:\n // el manager lo re-encolaría y el cierre se enviaría dos veces.\n }\n return;\n }\n\n try {\n const data = (await res.json()) as { sessionId?: string };\n if (postGeneration !== generation) {\n options.log?.('[DeepdotsAnalytics] sessionId of a closed session ignored:', data?.sessionId);\n return;\n }\n if (data?.sessionId && data.sessionId !== feedbackSessionId) {\n feedbackSessionId = data.sessionId;\n options.log?.('[DeepdotsAnalytics] feedbackSessionId cached:', feedbackSessionId);\n options.onSessionId?.(feedbackSessionId);\n }\n } catch {\n /* respuesta sin JSON — ignorar */\n }\n };\n\n return async (envelope: AnalyticsEnvelope, meta) => {\n if (meta?.resetSession) {\n // Sesión cerrada por el backend: no se envía nada, solo se olvida el registro.\n feedbackSessionId = undefined;\n firstPostInFlight = null;\n generation++;\n options.onSessionReset?.();\n return;\n }\n const final = meta?.final === true;\n const sessionEnd = meta?.sessionEnd === true;\n\n // Sin sessionId todavía: esperar al primer POST para heredarlo y no partir el registro.\n // En el flush final no se espera (la página se está muriendo): mejor un registro\n // partido que perder el lote — el backend cose por user_id.\n if (!feedbackSessionId && firstPostInFlight && !final) {\n await firstPostInFlight;\n }\n\n const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId, { sessionEnd });\n options.log?.('[DeepdotsAnalytics] POST /sdk/feedback →', body);\n\n const sent = post(body, final, sessionEnd);\n if (sessionEnd) {\n // Registro cerrado con `completed:true`: olvidar el sessionId para que el siguiente\n // lote lo OMITA y el backend abra uno nuevo (sesión nueva, o usuario nuevo).\n feedbackSessionId = undefined;\n firstPostInFlight = null;\n generation++;\n options.onSessionReset?.();\n } else if (!feedbackSessionId) {\n // nunca rechaza: es solo una barrera de orden, el fallo lo propaga `sent`\n firstPostInFlight = sent.then(\n () => undefined,\n () => undefined,\n );\n }\n await sent;\n };\n}\n","/**\n * Sesión de analytics suspendida por un `pagehide` (web).\n *\n * El navegador lanza `pagehide` igual al recargar (Cmd+R / F5) o navegar con carga completa\n * que al cerrar la pestaña, y la página no puede distinguirlo. Cerrar la sesión ahí partía\n * cada recarga en dos registros (dos feedbacks, dos mensajes en Slack). Ahora el `pagehide`\n * SUSPENDE la sesión: manda lo pendiente sin `completed` y deja aquí su `sessionId`.\n *\n * - Recarga / navegación en la misma pestaña: `init()` lo encuentra y SIGUE en ese registro.\n * - Cierre de la pestaña: `sessionStorage` muere con ella. El backend (Run_Jobs\n * `incomplete-surveys`) cierra la sesión cuando su último lote es un\n * `deepdots_session_suspend` y pasan unos minutos sin otro (`SUSPEND_GRACE_MINUTES`).\n *\n * `sessionStorage` y no `localStorage`: es por pestaña y sobrevive a la recarga, así que\n * otra pestaña nunca hereda la sesión de esta. Todo es tolerante a fallos (storage bloqueado,\n * JSON corrupto): ante cualquier error no hay sesión que reanudar y se abre una nueva.\n */\n\nimport type { KeyValueStorage } from '../tracking/tracking-manager';\n\nexport const SUSPENDED_SESSION_KEY = 'deepdots.analytics.suspended';\n\nexport interface SuspendedSession {\n /** Registro de analytics (`sessionId` de POST /sdk/feedback) que queda abierto. */\n sessionId: string;\n /** Integración de analytics: otra integración en el mismo origen no la hereda. */\n integration: string;\n /** `deepdots_user_id`: un cambio de usuario entre cargas no la hereda. */\n userId: string | null;\n /** Cuándo se suspendió (ms epoch). */\n at: number;\n}\n\n/** `window.sessionStorage` si existe y es accesible; null en RN, SSR o con storage bloqueado. */\nexport function getSessionStorage(): KeyValueStorage | null {\n try {\n return typeof window !== 'undefined' && window.sessionStorage ? window.sessionStorage : null;\n } catch {\n return null;\n }\n}\n\n/** Guarda la sesión suspendida. `false` si no se pudo: entonces hay que cerrarla como antes. */\nexport function writeSuspendedSession(storage: KeyValueStorage, session: SuspendedSession): boolean {\n try {\n storage.setItem(SUSPENDED_SESSION_KEY, JSON.stringify(session));\n return true;\n } catch {\n return false;\n }\n}\n\n/** Borra la sesión suspendida. Tolerante a fallos. */\nexport function clearSuspendedSession(storage: KeyValueStorage): void {\n try {\n storage.removeItem(SUSPENDED_SESSION_KEY);\n } catch {\n /* storage no disponible: se ignora */\n }\n}\n\n/**\n * Lee y CONSUME la sesión suspendida: el `sessionId` si es de la misma integración y usuario\n * y se suspendió hace como mucho `windowMs`; null en otro caso. La entrada se borra siempre,\n * para que una carga posterior no reanude una sesión que ya no le toca.\n */\nexport function takeSuspendedSession(\n storage: KeyValueStorage,\n match: { integration: string; userId: string | null },\n now: number,\n windowMs: number,\n): SuspendedSession | null {\n let raw: string | null = null;\n try {\n raw = storage.getItem(SUSPENDED_SESSION_KEY);\n } catch {\n return null;\n }\n if (!raw) return null;\n clearSuspendedSession(storage);\n try {\n const s = JSON.parse(raw) as Partial<SuspendedSession>;\n if (typeof s.sessionId !== 'string' || !s.sessionId) return null;\n if (typeof s.at !== 'number' || now - s.at < 0 || now - s.at > windowMs) return null;\n if (s.integration !== match.integration) return null;\n if ((s.userId ?? null) !== match.userId) return null;\n return { sessionId: s.sessionId, integration: s.integration, userId: s.userId ?? null, at: s.at };\n } catch {\n return null;\n }\n}\n","/**\n * Observador de navegación (Fase 2). Detecta cambios de pantalla y emite \"visitas\"\n * completadas (screen + entrada/salida/duración) cuando se abandona una pantalla.\n *\n * En Web se engancha a la History API (pushState/replaceState/popstate/hashchange).\n * La lógica de timing y normalización es pura y testeable sin DOM (vía `begin`/`visit`/`stop`\n * y un reloj inyectable).\n *\n * El consumidor (DeepdotsPopups) reenvía cada visita como un evento `page_view` por el\n * canal de analytics; el grafo se reconstruye en backend por orden + timestamp.\n */\n\nexport interface ScreenVisit {\n /** Pantalla normalizada (IDs → `:id`): agrupa las visitas a la misma pantalla. */\n screen: string;\n /**\n * La ruta real, sin query: `/insights/requests/2f1c…` donde `screen` dice\n * `/insights/requests/:id`. Es lo que permite enlazar la visita al elemento concreto.\n */\n path: string;\n entry: number;\n exit: number;\n durationSeconds: number;\n}\n\n/**\n * La pantalla en la que está el usuario AHORA, aún sin `page_view` (sale al abandonarla).\n * Mismos `screen`/`path` que llevará su visita y `seconds` medidos igual que su\n * `durationSeconds` (tiempo de reloj desde que se entró), pero hasta este momento.\n */\nexport interface CurrentScreen {\n screen: string;\n path: string;\n seconds: number;\n}\n\nexport type VisitListener = (visit: ScreenVisit) => void;\n\nconst UUID_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-/i;\n\n/** La ruta de una URL: path (+hash route), sin query. Sin normalizar los IDs. */\nexport function screenPath(href: string): string {\n let pathAndHash: string;\n try {\n const u = new URL(href, 'http://deepdots.local');\n pathAndHash = u.pathname + (u.hash || '');\n } catch {\n pathAndHash = href;\n }\n // quitar query (incluida la que pueda venir dentro del hash route)\n return pathAndHash.replace(/\\?[^#]*/g, '') || '/';\n}\n\n/** Normaliza una URL a un nombre de pantalla: path (+hash route), sin query, IDs → :id. */\nexport function normalizeScreen(href: string): string {\n const pathAndHash = screenPath(href);\n const collapsed = pathAndHash\n .split('/')\n .map((seg) => (/^[0-9]+$/.test(seg) || UUID_PREFIX.test(seg) ? ':id' : seg))\n .join('/');\n return collapsed || '/';\n}\n\nexport interface NavigationObserverOptions {\n now?: () => number;\n}\n\nexport class NavigationObserver {\n private now: () => number;\n private listeners = new Set<VisitListener>();\n private currentScreen: string | null = null;\n private currentPath: string | null = null;\n private entryAt = 0;\n private installed = false;\n private domHandlers: Array<() => void> = [];\n\n constructor(options: NavigationObserverOptions = {}) {\n this.now = options.now ?? (() => Date.now());\n }\n\n onVisit(listener: VisitListener): void {\n this.listeners.add(listener);\n }\n\n /** Fija la pantalla inicial (sin emitir visita; su duración se cierra al salir). */\n begin(href: string): void {\n this.currentScreen = normalizeScreen(href);\n this.currentPath = screenPath(href);\n this.entryAt = this.now();\n }\n\n /** Navega a una pantalla nueva: cierra la anterior (emite visita) y abre la nueva. */\n visit(href: string): void {\n const nextPath = screenPath(href);\n // Misma ruta (solo cambia la query) → no es una visita nueva. Se compara la ruta y no\n // la pantalla normalizada: pasar de una research a otra (`/insights/requests/A` → `/B`)\n // sí es otra visita, aunque las dos sean `/insights/requests/:id`.\n if (this.currentScreen != null && nextPath === this.currentPath) return;\n this.closeCurrent();\n this.currentScreen = normalizeScreen(href);\n this.currentPath = nextPath;\n this.entryAt = this.now();\n }\n\n /** Cierra la pantalla actual emitiendo su visita (p. ej. al cerrar la app/pestaña). */\n stop(): void {\n this.closeCurrent();\n this.currentScreen = null;\n this.currentPath = null;\n }\n\n /**\n * Olvida la pantalla actual SIN emitir su visita. Para una sesión que el backend ya dio\n * por cerrada: su visita no tiene dónde ir, y mandarla con la sesión nueva la contaría\n * donde no ocurrió. Después, `restart()` vuelve a empezar en la pantalla actual.\n */\n discard(): void {\n this.currentScreen = null;\n this.currentPath = null;\n }\n\n /**\n * Retoma el seguimiento en la pantalla actual tras un `stop()`, cuando se abre una sesión\n * nueva sin navegar (cambio de usuario, vuelta de la bfcache). No-op si nunca se instaló\n * (navegación manual por `setScreen`, RN) o si ya hay pantalla en curso.\n */\n restart(): void {\n if (!this.installed || this.currentScreen != null || typeof window === 'undefined') return;\n this.begin(window.location.href);\n }\n\n /**\n * La pantalla en curso, sin cerrarla ni emitir nada; null si no hay (antes de `begin`, tras\n * `stop`/`discard`). Para que el `user_engagement` lleve la pantalla: si el cierre de la\n * sesión no llega nunca (pestaña dormida y descartada, app matada), la última pantalla no\n * tiene `page_view` y solo queda esto.\n */\n current(): CurrentScreen | null {\n if (this.currentScreen == null) return null;\n return {\n screen: this.currentScreen,\n path: this.currentPath ?? this.currentScreen,\n seconds: this.secondsSinceEntry(this.now()),\n };\n }\n\n private secondsSinceEntry(at: number): number {\n return Math.max(0, Math.round((at - this.entryAt) / 1000));\n }\n\n private closeCurrent(): void {\n if (this.currentScreen == null) return;\n const exit = this.now();\n const visit: ScreenVisit = {\n screen: this.currentScreen,\n path: this.currentPath ?? this.currentScreen,\n entry: this.entryAt,\n exit,\n durationSeconds: this.secondsSinceEntry(exit),\n };\n this.listeners.forEach((l) => {\n try {\n l(visit);\n } catch {\n /* listener no debe romper la navegación */\n }\n });\n }\n\n /** Instala los hooks de History en el navegador y registra la pantalla actual. */\n install(): void {\n if (this.installed || typeof window === 'undefined' || typeof history === 'undefined') return;\n this.installed = true;\n\n const handle = () => this.visit(window.location.href);\n\n const originalPush = history.pushState.bind(history);\n const originalReplace = history.replaceState.bind(history);\n history.pushState = (data: unknown, unused: string, url?: string | URL | null) => {\n const r = originalPush(data as never, unused, url as never);\n handle();\n return r;\n };\n history.replaceState = (data: unknown, unused: string, url?: string | URL | null) => {\n const r = originalReplace(data as never, unused, url as never);\n handle();\n return r;\n };\n window.addEventListener('popstate', handle);\n window.addEventListener('hashchange', handle);\n\n this.domHandlers.push(() => {\n history.pushState = originalPush;\n history.replaceState = originalReplace;\n window.removeEventListener('popstate', handle);\n window.removeEventListener('hashchange', handle);\n });\n\n this.begin(window.location.href);\n }\n\n uninstall(): void {\n this.domHandlers.forEach((off) => off());\n this.domHandlers = [];\n this.installed = false;\n }\n}\n","/**\n * Device info para el context de analytics (Technology #11–13).\n *\n * En Web enviamos `device_type` + `user_agent` crudo y dejamos que el backend\n * derive OS/navegador del UA (como hace GA). Los campos adicionales (timezone,\n * referrer, viewport, etc.) se recogen de las APIs del navegador sin permisos.\n * `country`/`city` se rellenan de forma asíncrona (ver geo-info.ts).\n * En KMP las APIs nativas dan os_version/device_model reales.\n */\n\nexport interface DeviceInfo {\n device_type: 'mobile' | 'tablet' | 'desktop';\n os_version?: string;\n device_model?: string;\n app_version?: string;\n user_agent?: string;\n timezone?: string;\n referrer?: string;\n viewport_size?: string;\n screen_resolution?: string;\n pixel_ratio?: number;\n entry_type?: string;\n page_load_ms?: number;\n connection_type?: string;\n country?: string;\n city?: string;\n}\n\nexport function parseDeviceType(ua: string): 'mobile' | 'tablet' | 'desktop' {\n const s = (ua || '').toLowerCase();\n if (/ipad|tablet|playbook|silk/.test(s) || (/android/.test(s) && !/mobile/.test(s))) {\n return 'tablet';\n }\n if (/mobi|iphone|ipod|windows phone/.test(s) || (/android/.test(s) && /mobile/.test(s))) {\n return 'mobile';\n }\n return 'desktop';\n}\n\nfunction getEntryType(): string | undefined {\n if (typeof performance === 'undefined') return undefined;\n const entries = performance.getEntriesByType('navigation');\n if (!entries.length) return undefined;\n return (entries[0] as PerformanceNavigationTiming).type;\n}\n\nfunction getPageLoadMs(): number | undefined {\n if (typeof performance === 'undefined') return undefined;\n const entries = performance.getEntriesByType('navigation');\n if (!entries.length) return undefined;\n const load = (entries[0] as PerformanceNavigationTiming).loadEventEnd;\n return load > 0 ? Math.round(load) : undefined;\n}\n\nfunction getConnectionType(): string | undefined {\n if (typeof navigator === 'undefined') return undefined;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (navigator as any).connection?.effectiveType ?? undefined;\n}\n\nexport function collectDeviceInfo(appVersion?: string): DeviceInfo {\n const ua = typeof navigator !== 'undefined' ? navigator.userAgent : '';\n return {\n device_type: parseDeviceType(ua),\n user_agent: ua || undefined,\n app_version: appVersion,\n timezone: typeof Intl !== 'undefined' ? Intl.DateTimeFormat().resolvedOptions().timeZone : undefined,\n referrer: typeof document !== 'undefined' && document.referrer ? document.referrer : undefined,\n viewport_size:\n typeof window !== 'undefined' ? `${window.innerWidth}x${window.innerHeight}` : undefined,\n screen_resolution:\n typeof screen !== 'undefined' ? `${screen.width}x${screen.height}` : undefined,\n pixel_ratio: typeof window !== 'undefined' ? window.devicePixelRatio : undefined,\n entry_type: getEntryType(),\n page_load_ms: getPageLoadMs(),\n connection_type: getConnectionType(),\n };\n}\n","/**\n * Geolocalización por IP (Option B: client-side) robusta.\n *\n * Robustez:\n * - CACHE persistente en `KeyValueStorage` con TTL: en arranques posteriores el país/ciudad\n * está disponible de inmediato (sin llamada, sin gap de timing) y ahorra cuota del proveedor.\n * - CADENA de proveedores con fallback: si uno está bloqueado (ad-blocker), rate-limited (429)\n * o caído, se prueba el siguiente.\n * - TIMEOUT por proveedor vía `AbortController` para no colgarse en uno lento.\n *\n * Todo es fire-and-forget y tolerante a fallos: ante cualquier error devuelve `null`.\n */\n\nimport type { KeyValueStorage } from '../tracking/tracking-manager';\n\nexport interface GeoInfo {\n country?: string;\n city?: string;\n}\n\n/** Clave de cache en storage. */\nexport const GEO_STORAGE_KEY = 'deepdots.geo';\n/** El país/ciudad casi no cambian: cache de 30 días. */\nexport const GEO_TTL_MS = 30 * 24 * 60 * 60 * 1000;\n/** Timeout por proveedor. */\nexport const GEO_TIMEOUT_MS = 3000;\n\ninterface CachedGeo extends GeoInfo {\n ts: number;\n}\n\n/** Lee el geo cacheado si sigue fresco (no supera el TTL). Tolerante a JSON corrupto. */\nexport function readCachedGeo(storage: KeyValueStorage, now: number): GeoInfo | null {\n try {\n const raw = storage.getItem(GEO_STORAGE_KEY);\n if (!raw) return null;\n const cached = JSON.parse(raw) as CachedGeo;\n if (typeof cached.ts !== 'number' || now - cached.ts > GEO_TTL_MS) return null;\n const geo: GeoInfo = {};\n if (cached.country) geo.country = cached.country;\n if (cached.city) geo.city = cached.city;\n return geo.country || geo.city ? geo : null;\n } catch {\n return null;\n }\n}\n\n/** Persiste el geo con la marca de tiempo actual. Tolerante a fallos de storage. */\nexport function writeCachedGeo(storage: KeyValueStorage, geo: GeoInfo, now: number): void {\n try {\n const payload: CachedGeo = { ...geo, ts: now };\n storage.setItem(GEO_STORAGE_KEY, JSON.stringify(payload));\n } catch {\n /* storage lleno / no disponible: se ignora */\n }\n}\n\nexport interface GeoProvider {\n url: string;\n parse: (data: Record<string, unknown>) => GeoInfo | null;\n}\n\n/** Proveedores en orden de preferencia. Todos devuelven JSON sin auth. */\nexport const DEFAULT_GEO_PROVIDERS: GeoProvider[] = [\n {\n // https://ipapi.co/json/\n url: 'https://ipapi.co/json/',\n parse: (d) => geoFrom(d.country_code, d.city),\n },\n {\n // https://ipwho.is/ (devuelve success:false con 200 en errores)\n url: 'https://ipwho.is/',\n parse: (d) => (d.success === false ? null : geoFrom(d.country_code, d.city)),\n },\n {\n // https://ipinfo.io/json (country = código ISO)\n url: 'https://ipinfo.io/json',\n parse: (d) => geoFrom(d.country, d.city),\n },\n];\n\nfunction geoFrom(country: unknown, city: unknown): GeoInfo | null {\n const c = typeof country === 'string' && country ? country : undefined;\n const t = typeof city === 'string' && city ? city : undefined;\n if (!c && !t) return null;\n return { country: c, city: t };\n}\n\nasync function fetchOne(provider: GeoProvider, f: typeof fetch, timeoutMs: number): Promise<GeoInfo | null> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n const res = await f(provider.url, { signal: controller.signal });\n if (!res.ok) return null;\n const data = (await res.json()) as Record<string, unknown>;\n return provider.parse(data);\n } catch {\n return null;\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport interface FetchGeoOptions {\n fetchImpl?: typeof fetch;\n providers?: GeoProvider[];\n timeoutMs?: number;\n}\n\n/**\n * Intenta cada proveedor en orden (con timeout) y devuelve el primer geo válido.\n * `null` si ninguno responde. No lanza.\n */\nexport async function fetchGeo(options: FetchGeoOptions = {}): Promise<GeoInfo | null> {\n const f = options.fetchImpl ?? (typeof fetch !== 'undefined' ? fetch : undefined);\n if (!f) return null;\n const providers = options.providers ?? DEFAULT_GEO_PROVIDERS;\n const timeoutMs = options.timeoutMs ?? GEO_TIMEOUT_MS;\n for (const provider of providers) {\n const geo = await fetchOne(provider, f, timeoutMs);\n if (geo) return geo;\n }\n return null;\n}\n","/**\n * Resolución del idioma para el context de analytics.\n *\n * Prioridad: idioma explícito del host > `navigator.language` (web) > locale de `Intl`\n * (fallback automático que funciona en React Native con Hermes, donde `navigator.language`\n * no existe). Si nada resuelve, devuelve `undefined` y el campo se omite del metadata.\n */\n\n/** Forma mínima de `Intl` que necesitamos (inyectable para tests / entornos sin Intl). */\nexport interface IntlLike {\n DateTimeFormat: () => { resolvedOptions: () => { locale?: string } };\n}\n\nexport interface LanguageSources {\n /** Idioma forzado por el host (init.language). Máxima prioridad. */\n explicit?: string;\n /** `navigator` global (web). En RN suele no tener `language`. */\n navigator?: { language?: string };\n /** `Intl` global. Fallback cross-platform (web + RN/Hermes). */\n intl?: IntlLike;\n}\n\nfunction clean(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? trimmed : undefined;\n}\n\nexport function resolveLanguage(sources: LanguageSources): string | undefined {\n const explicit = clean(sources.explicit);\n if (explicit) return explicit;\n\n const navLang = clean(sources.navigator?.language);\n if (navLang) return navLang;\n\n try {\n const locale = clean(sources.intl?.DateTimeFormat().resolvedOptions().locale);\n if (locale) return locale;\n } catch {\n /* Intl no disponible o falló: se ignora */\n }\n\n return undefined;\n}\n","/**\n * Mide el tiempo ACTIVO (en primer plano), estilo GA4 (engagement time).\n * Acumula mientras está \"resumed\" (pestaña visible / app en foreground) y pausa en\n * background. `consume()` devuelve los ms activos desde la última lectura y reinicia,\n * dejando el timer corriendo. El consumidor emite un evento `user_engagement` con\n * `engagement_time_msec`; el backend lo suma por sesión → \"Average Time Spent per Session\" (#8).\n */\nexport class EngagementTracker {\n private now: () => number;\n private activeMs = 0;\n private lastResumeAt: number | null = null;\n\n constructor(now: () => number = () => Date.now()) {\n this.now = now;\n }\n\n /** Reanuda el conteo (pestaña visible / app en foreground). Idempotente. */\n resume(): void {\n if (this.lastResumeAt === null) this.lastResumeAt = this.now();\n }\n\n /** Pausa el conteo (pestaña oculta / app en background), acumulando el tramo activo. */\n pause(): void {\n if (this.lastResumeAt !== null) {\n this.activeMs += this.now() - this.lastResumeAt;\n this.lastResumeAt = null;\n }\n }\n\n /** Devuelve los ms activos acumulados desde la última lectura y reinicia (sin parar el timer). */\n consume(): number {\n if (this.lastResumeAt !== null) {\n const t = this.now();\n this.activeMs += t - this.lastResumeAt;\n this.lastResumeAt = t;\n }\n const ms = this.activeMs;\n this.activeMs = 0;\n return ms;\n }\n\n isActive(): boolean {\n return this.lastResumeAt !== null;\n }\n}\n","import type { KeyValueStorage } from '../tracking/tracking-manager';\nimport { STORAGE_KEYS } from '../tracking/tracking-manager';\n\n/** Valor de un atributo de contact (info interna del usuario que solo conoce el host). */\nexport type ContactAttributeValue = string | number | boolean;\nexport type ContactAttributes = Record<string, ContactAttributeValue>;\n\n/** Body de `POST /sdk/popups/contact`. */\nexport interface ContactBody {\n publicKey: string;\n userId: string;\n userAttributes: ContactAttributes;\n}\n\nexport interface ContactManagerOptions {\n storage: KeyValueStorage;\n publicKey: string;\n userId: string;\n /** Envío real (inyectable para tests): hace el `POST /sdk/popups/contact`. */\n post: (body: ContactBody) => Promise<void>;\n}\n\n/** Serialización estable (claves ordenadas) para el diff contra lo último enviado. */\nfunction stableSerialize(attrs: ContactAttributes): string {\n const sorted: ContactAttributes = {};\n for (const key of Object.keys(attrs).sort()) {\n sorted[key] = attrs[key];\n }\n return JSON.stringify(sorted);\n}\n\n/**\n * Gestiona los atributos de contact del usuario identificado (`userId` del init).\n * Solo envía cuando cambian respecto a lo último persistido en storage, para ahorrar\n * peticiones (el host puede llamar `setAttributes` en cada identificación sin miedo).\n */\nexport class ContactManager {\n private readonly storage: KeyValueStorage;\n private readonly publicKey: string;\n private readonly userId: string;\n private readonly post: (body: ContactBody) => Promise<void>;\n\n constructor(options: ContactManagerOptions) {\n this.storage = options.storage;\n this.publicKey = options.publicKey;\n this.userId = options.userId;\n this.post = options.post;\n }\n\n /**\n * Envía los atributos si cambiaron respecto a lo último enviado.\n * @returns `true` si se envió, `false` si no hubo cambios.\n */\n async setAttributes(attributes: ContactAttributes): Promise<boolean> {\n const serialized = stableSerialize(attributes);\n const last = this.storage.getItem(STORAGE_KEYS.contactAttributes);\n if (last === serialized) return false;\n\n await this.post({\n publicKey: this.publicKey,\n userId: this.userId,\n userAttributes: attributes,\n });\n this.storage.setItem(STORAGE_KEYS.contactAttributes, serialized);\n return true;\n }\n}\n","/**\n * Crash & error reporting (Stability #14–17) — captura de errores GESTIONADOS.\n *\n * - `reportError()`: API pública para el host (app viva) → emite `deepdots_app_crash` ya.\n * - handlers de `window` (errores no capturados): persisten a disco y se reenvían\n * en el siguiente arranque (el proceso puede morir antes del flush normal).\n *\n * El contexto de device/sesión se captura EN EL MOMENTO del crash y se guarda en el\n * record — no se lee del envelope en el replay (el replay puede ocurrir en otra\n * app_version/OS). Las dims OS/device en Web suelen ir vacías (se derivan del UA en backend).\n */\n\nimport type { KeyValueStorage } from '../tracking/tracking-manager';\n\nexport type CrashSeverity = 'fatal' | 'error' | 'warning';\n\nexport interface CrashRecord {\n crashedAt: number;\n crashType: string;\n message: string;\n stack: string;\n fatal: boolean;\n handled: boolean;\n severity: CrashSeverity;\n sessionId: string | null;\n appVersion?: string;\n osVersion?: string;\n deviceModel?: string;\n context?: Record<string, string>;\n}\n\nexport interface ReportErrorOptions {\n severity?: CrashSeverity;\n handled?: boolean;\n context?: Record<string, unknown>;\n}\n\nexport interface DeviceSnapshot {\n appVersion?: string;\n osVersion?: string;\n deviceModel?: string;\n}\n\n/** Forma mínima de `global.ErrorUtils` de React Native. */\nexport interface ReactNativeErrorUtils {\n getGlobalHandler?(): ((error: unknown, isFatal?: boolean) => void) | undefined;\n setGlobalHandler(handler: (error: unknown, isFatal?: boolean) => void): void;\n}\n\nexport interface CrashReporterOptions {\n storage: KeyValueStorage;\n /** Emite un evento deepdots_app_crash AHORA (app viva). */\n emit: (params: Record<string, unknown>) => void;\n /** Snapshot de device en el momento del crash. */\n device: () => DeviceSnapshot;\n /** session_id en el momento del crash. */\n sessionId: () => string | null;\n now?: () => number;\n /** Kill-switch de consentimiento. */\n enabled?: () => boolean;\n}\n\nconst STACK_MAX = 8000;\nconst QUEUE_KEY = 'deepdots.crash.queue';\nconst MAX_QUEUED = 20;\n\n/** Convierte un CrashRecord en los params del evento `deepdots_app_crash` (omite undefined). */\nexport function crashRecordToParams(r: CrashRecord): Record<string, unknown> {\n const params: Record<string, unknown> = {\n crashed_at: r.crashedAt,\n crash_type: r.crashType,\n message: r.message,\n stack: r.stack,\n fatal: r.fatal,\n handled: r.handled,\n severity: r.severity,\n };\n if (r.sessionId) params.crashed_session_id = r.sessionId;\n if (r.appVersion) params.crashed_app_version = r.appVersion;\n if (r.osVersion) params.crashed_os_version = r.osVersion;\n if (r.deviceModel) params.crashed_device_model = r.deviceModel;\n if (r.context) {\n for (const [k, v] of Object.entries(r.context)) params[`ctx_${k}`] = v;\n }\n return params;\n}\n\nexport class CrashReporter {\n private options: CrashReporterOptions;\n private now: () => number;\n\n constructor(options: CrashReporterOptions) {\n this.options = options;\n this.now = options.now ?? (() => Date.now());\n }\n\n private isEnabled(): boolean {\n return this.options.enabled ? this.options.enabled() : true;\n }\n\n /** Construye un CrashRecord capturando contexto en el momento del crash. */\n private buildRecord(\n error: unknown,\n severity: CrashSeverity,\n handled: boolean,\n fatal: boolean,\n context?: Record<string, unknown>,\n ): CrashRecord {\n const err = error as { name?: string; message?: string; stack?: string };\n const isErr = error instanceof Error;\n const dev = this.options.device();\n const ctx = context\n ? Object.fromEntries(Object.entries(context).map(([k, v]) => [k, String(v)]))\n : undefined;\n return {\n crashedAt: this.now(),\n crashType: (isErr && err.name) || 'Error',\n message: isErr ? String(err.message ?? '') : String(error),\n stack: (isErr && typeof err.stack === 'string' ? err.stack : '').slice(0, STACK_MAX),\n fatal,\n handled,\n severity,\n sessionId: this.options.sessionId(),\n appVersion: dev.appVersion,\n osVersion: dev.osVersion,\n deviceModel: dev.deviceModel,\n context: ctx,\n };\n }\n\n /** API pública del host: reporta un error (app viva) → emite el evento ya. */\n reportError(error: unknown, options: ReportErrorOptions = {}): void {\n if (!this.isEnabled()) return;\n const severity = options.severity ?? 'error';\n const handled = options.handled ?? true;\n const fatal = severity === 'fatal';\n const record = this.buildRecord(error, severity, handled, fatal, options.context);\n this.options.emit(crashRecordToParams(record));\n }\n\n /** Captura un error no manejado (fatal) → persiste a disco para replay en el siguiente arranque. */\n captureUnhandled(error: unknown): void {\n this.persist(this.buildRecord(error, 'fatal', false, true));\n }\n\n /** Persiste un crash no capturado a disco para reenviarlo en el siguiente arranque. */\n private persist(record: CrashRecord): void {\n let queue: CrashRecord[] = [];\n try {\n const raw = this.options.storage.getItem(QUEUE_KEY);\n if (raw) queue = JSON.parse(raw) as CrashRecord[];\n if (!Array.isArray(queue)) queue = [];\n } catch {\n queue = [];\n }\n queue.push(record);\n if (queue.length > MAX_QUEUED) queue = queue.slice(queue.length - MAX_QUEUED);\n try {\n this.options.storage.setItem(QUEUE_KEY, JSON.stringify(queue));\n } catch {\n /* storage lleno / no disponible — se pierde el crash, no rompemos la app */\n }\n }\n\n /** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */\n drainPendingCrashes(): CrashRecord[] {\n let queue: CrashRecord[] = [];\n try {\n const raw = this.options.storage.getItem(QUEUE_KEY);\n if (raw) queue = JSON.parse(raw) as CrashRecord[];\n if (!Array.isArray(queue)) queue = [];\n } catch {\n queue = [];\n }\n if (queue.length) {\n try { this.options.storage.removeItem(QUEUE_KEY); } catch { /* noop */ }\n }\n return queue;\n }\n\n /** Instala los handlers globales de errores no capturados (no-op sin `window`). */\n install(): void {\n // En React Native `window` existe pero NO tiene addEventListener; comprobamos\n // la capacidad, no solo la existencia de `window`. En RN el host usa\n // installReactNative(ErrorUtils) para capturar los errores no manejados.\n if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') return;\n window.addEventListener('error', (e: ErrorEvent) => {\n if (!this.isEnabled()) return;\n this.captureUnhandled(e.error ?? e.message ?? 'error');\n });\n window.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => {\n if (!this.isEnabled()) return;\n this.captureUnhandled(e.reason ?? 'unhandledrejection');\n });\n }\n\n /** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */\n installReactNative(errorUtils: ReactNativeErrorUtils): void {\n const previous = errorUtils.getGlobalHandler?.();\n errorUtils.setGlobalHandler((error: unknown, isFatal?: boolean) => {\n if (this.isEnabled()) this.captureUnhandled(error);\n previous?.(error, isFatal);\n });\n }\n\n /** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */\n _persistForTest(error: unknown): void {\n this.captureUnhandled(error);\n }\n}\n","/**\n * Persistencia del estado que alimenta las reglas de redisplay (cooldown) de los popups.\n *\n * Hasta la 1.7.0 `lastShown`/`surveyProgress` vivían SOLO en memoria, así que cualquier\n * recarga de página reiniciaba el cooldown y el popup volvía a salir aunque su regla\n * dijese \"no antes de N días\". El SDK nativo (KMP) sí lo persistía (`getLastShown`/\n * `setLastShown` contra el `KeyValueStorage`); esto cierra esa divergencia en Web.\n *\n * ⚠️ El alcance es POR NAVEGADOR: otro dispositivo, otro perfil o un borrado de datos\n * vuelven a empezar. El cooldown por usuario de verdad exige que el backend filtre el\n * `GET /sdk/{publicKey}/popups` con el historial que ya recibe en `POST /sdk/popups`.\n */\nimport type { KeyValueStorage } from '../tracking/tracking-manager';\nimport { POPUP_TRIGGER_CONDITION_STATUSES } from '../types';\nimport type { PopupTriggerConditionStatus } from '../types';\n\n/** Clave única en el namespace `deepdots.*` (mismo que `STORAGE_KEYS`). */\nexport const POPUP_STATE_STORAGE_KEY = 'deepdots.popups.state';\n\n/**\n * Techo de entradas por mapa, con desalojo del más ANTIGUO (mismo trade-off explícito\n * que `MAX_TRACKED_MESSAGES` en `MessageGuard`). Una cuenta real tiene unos pocos popups;\n * el techo solo evita que el registro crezca sin límite si se rotan definiciones.\n * No se poda por antigüedad a propósito: un `cooldownDays` largo debe seguir valiendo.\n */\nexport const MAX_TRACKED_POPUP_ENTRIES = 200;\n\nexport interface PersistedProgressEntry {\n status: PopupTriggerConditionStatus;\n timestamp: number;\n}\n\nexport interface PersistedPopupState {\n /** popupId → timestamp de la última vez que se mostró. */\n lastShown: Record<string, number>;\n /** surveyId → último estado conocido (PARTIAL/COMPLETED) y cuándo. */\n progress: Record<string, PersistedProgressEntry>;\n}\n\nfunction emptyState(): PersistedPopupState {\n return { lastShown: {}, progress: {} };\n}\n\nfunction isValidTimestamp(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value);\n}\n\n/** Se queda con las `MAX_TRACKED_POPUP_ENTRIES` entradas de timestamp más reciente. */\nfunction capEntries<T>(entries: Array<[string, T]>, timestampOf: (value: T) => number): Array<[string, T]> {\n if (entries.length <= MAX_TRACKED_POPUP_ENTRIES) return entries;\n return [...entries]\n .sort((a, b) => timestampOf(b[1]) - timestampOf(a[1]))\n .slice(0, MAX_TRACKED_POPUP_ENTRIES);\n}\n\n/**\n * Lee el estado persistido. Tolerante a todo: sin clave, JSON corrupto, entradas con\n * forma inválida o un storage que lanza (Safari en privado, cookies bloqueadas) →\n * estado vacío o entradas buenas, nunca una excepción.\n */\nexport function readPopupState(storage: KeyValueStorage): PersistedPopupState {\n let raw: string | null = null;\n try {\n raw = storage.getItem(POPUP_STATE_STORAGE_KEY);\n } catch {\n return emptyState();\n }\n if (!raw) return emptyState();\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return emptyState();\n }\n if (typeof parsed !== 'object' || parsed === null) return emptyState();\n\n const source = parsed as Partial<Record<keyof PersistedPopupState, unknown>>;\n const state = emptyState();\n\n if (typeof source.lastShown === 'object' && source.lastShown !== null) {\n const valid = Object.entries(source.lastShown as Record<string, unknown>)\n .filter((entry): entry is [string, number] => isValidTimestamp(entry[1]));\n capEntries(valid, (timestamp) => timestamp).forEach(([popupId, timestamp]) => {\n state.lastShown[popupId] = timestamp;\n });\n }\n\n if (typeof source.progress === 'object' && source.progress !== null) {\n const valid = Object.entries(source.progress as Record<string, unknown>)\n .filter((entry): entry is [string, PersistedProgressEntry] => {\n const value = entry[1];\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Partial<PersistedProgressEntry>;\n return (\n POPUP_TRIGGER_CONDITION_STATUSES.includes(candidate.status as PopupTriggerConditionStatus)\n && isValidTimestamp(candidate.timestamp)\n );\n });\n capEntries(valid, (entry) => entry.timestamp).forEach(([surveyId, entry]) => {\n state.progress[surveyId] = { status: entry.status, timestamp: entry.timestamp };\n });\n }\n\n return state;\n}\n\n/** Escribe el estado (acotado). Un storage que lanza se ignora en silencio. */\nexport function writePopupState(storage: KeyValueStorage, state: PersistedPopupState): void {\n const capped: PersistedPopupState = emptyState();\n capEntries(Object.entries(state.lastShown), (timestamp) => timestamp).forEach(([popupId, timestamp]) => {\n capped.lastShown[popupId] = timestamp;\n });\n capEntries(Object.entries(state.progress), (entry) => entry.timestamp).forEach(([surveyId, entry]) => {\n capped.progress[surveyId] = entry;\n });\n\n try {\n storage.setItem(POPUP_STATE_STORAGE_KEY, JSON.stringify(capped));\n } catch {\n // Storage lleno o bloqueado por el navegador: el cooldown degrada a solo-memoria.\n }\n}\n\n/** Borra el registro (cambio de usuario: el historial era del anterior). */\nexport function clearPopupState(storage: KeyValueStorage): void {\n try {\n storage.removeItem(POPUP_STATE_STORAGE_KEY);\n } catch {\n // idem\n }\n}\n","/**\n * Messaging (#18–22): tracking de notificaciones del host (push / in-app).\n * Un único evento reservado `deepdots_message` con un campo `stage` discriminador;\n * el funnel se correlaciona por `message_id` y se agrupa por `message_title`.\n */\n\nexport type MessageStage = 'delivered' | 'clicked' | 'converted';\n\n/** Canales válidos. Un mensaje tiene UN canal: se valida en runtime porque el host puede no ser TS. */\nexport const MESSAGE_CHANNELS = ['push', 'in_app'] as const;\nexport type MessageChannel = (typeof MESSAGE_CHANNELS)[number];\n\nexport interface TrackMessageOptions {\n /** Identificador del mensaje/notificación — correlaciona el funnel delivered→clicked→converted. */\n id: string;\n /** Título del mensaje — dimensión de agrupación de #18–22. */\n title: string;\n /** Canal de entrega. */\n channel: MessageChannel;\n /** Nombre de la campaña (opcional). */\n campaign?: string;\n /** Valor de conversión (opcional, típico en `stage: 'converted'`). */\n value?: number;\n currency?: string;\n /** Parámetros libres adicionales. */\n params?: Record<string, unknown>;\n}\n\n/** Construye los params del evento `deepdots_message` (omite opcionales ausentes). */\nexport function buildMessageParams(stage: MessageStage, o: TrackMessageOptions): Record<string, unknown> {\n const p: Record<string, unknown> = {\n stage,\n message_id: o.id,\n message_title: o.title,\n channel: o.channel,\n };\n if (o.campaign) p.campaign = o.campaign;\n if (o.value !== undefined) p.value = o.value;\n if (o.currency) p.currency = o.currency;\n return { ...p, ...(o.params ?? {}) };\n}\n\n/** Techo de `message_id` vigilados por sesión; los más antiguos se evictan (memoria acotada). */\nexport const MAX_TRACKED_MESSAGES = 500;\n\nexport type MessageRejectionReason = 'invalid_channel' | 'duplicate_stage' | 'channel_conflict';\n\nexport type MessageGuardVerdict =\n | { emit: true }\n | { emit: false; reason: MessageRejectionReason; detail: string };\n\n/**\n * Protege el funnel de Messaging de las formas imposibles que el host puede producir por error\n * (visto en producción: CTR > 100% por falta de `delivered` + el mismo `message_id` reportado en\n * dos canales). Tres reglas, todas dentro de la sesión (se reinicia en cada `init()`):\n *\n * 1. `channel` fuera de `MESSAGE_CHANNELS` → se descarta el evento.\n * 2. Un `(message_id, stage)` ya emitido → se emite una sola vez.\n * 3. Un `message_id` ya visto en un canal → se descartan los eventos de otro canal.\n *\n * Los eventos rechazados NO mutan el estado: un rechazo nunca \"consume\" el stage bueno.\n * Lo que el SDK no puede arreglar es la ausencia de `delivered`: eso lo instrumenta el host.\n */\nexport class MessageGuard {\n /** message_id → canal fijado + stages ya emitidos. El orden de inserción marca la eviction. */\n private seen = new Map<string, { channel: MessageChannel; stages: Set<MessageStage> }>();\n\n constructor(private maxTrackedMessages: number = MAX_TRACKED_MESSAGES) {}\n\n evaluate(stage: MessageStage, options: TrackMessageOptions): MessageGuardVerdict {\n const channel = options.channel;\n if (!(MESSAGE_CHANNELS as readonly string[]).includes(channel)) {\n return {\n emit: false,\n reason: 'invalid_channel',\n detail: `invalid channel \"${channel}\" (expected ${MESSAGE_CHANNELS.join(' | ')})`,\n };\n }\n\n const entry = this.seen.get(options.id);\n if (!entry) {\n this.remember(options.id, channel, stage);\n return { emit: true };\n }\n if (entry.channel !== channel) {\n return {\n emit: false,\n reason: 'channel_conflict',\n detail: `message_id \"${options.id}\" was already reported on channel \"${entry.channel}\"; discarding \"${channel}\"`,\n };\n }\n if (entry.stages.has(stage)) {\n return {\n emit: false,\n reason: 'duplicate_stage',\n detail: `stage \"${stage}\" was already sent for message_id \"${options.id}\"`,\n };\n }\n entry.stages.add(stage);\n return { emit: true };\n }\n\n /** Olvida todo lo vigilado (nueva sesión). */\n reset(): void {\n this.seen.clear();\n }\n\n private remember(id: string, channel: MessageChannel, stage: MessageStage): void {\n this.seen.set(id, { channel, stages: new Set([stage]) });\n while (this.seen.size > this.maxTrackedMessages) {\n const oldest = this.seen.keys().next();\n if (oldest.done) break;\n this.seen.delete(oldest.value);\n }\n }\n}\n","import {\n DeepdotsConfig,\n TriggerConfig,\n DeepdotsEvent,\n DeepdotsEventType,\n EventListener,\n DeepdotsInitParams,\n DeepdotsLogger,\n FeedbackSession,\n PopupDefinition,\n PopupTriggerCondition,\n PopupActions,\n PopupTrigger,\n PopupTriggerConditionStatus,\n POPUP_TRIGGER_CONDITION_STATUSES, POPUPSESSIONSTATUS,\n PopupStyle,\n SessionEndReason,\n SessionStartReason,\n} from '../types';\n// renderPopup se carga de forma PEREZOSA (dynamic import) para no arrastrar\n// `@magicfeedback/native` (browser) ni el CSS al importar el SDK en entornos sin DOM (React Native).\nimport { setupTrigger } from '../triggers';\nimport { resolveEnvironment } from '../config/env';\nimport { PopupRenderer, createDefaultRenderer } from '../platform/renderer';\nimport { TrackingManager, createDefaultStorage, type KeyValueStorage } from '../tracking/tracking-manager';\nimport { AnalyticsManager, createDryRunSink, type AnalyticsEnvelope } from '../analytics/analytics-manager';\nimport { createFeedbackSink } from '../analytics/feedback-payload';\nimport {\n clearSuspendedSession,\n getSessionStorage,\n takeSuspendedSession,\n writeSuspendedSession,\n} from '../analytics/suspended-session';\nimport { NavigationObserver } from '../tracking/navigation-observer';\nimport { collectDeviceInfo } from '../analytics/device-info';\nimport { readCachedGeo, writeCachedGeo, fetchGeo } from '../analytics/geo-info';\nimport { resolveLanguage } from '../analytics/language';\nimport { EngagementTracker } from '../analytics/engagement-tracker';\nimport { ContactManager, type ContactAttributes } from '../contact/contact-manager';\nimport { CrashReporter, crashRecordToParams, type ReportErrorOptions, type ReactNativeErrorUtils } from '../analytics/crash-reporter';\nimport { clearPopupState, readPopupState, writePopupState } from './popup-state-store';\nimport { setLogger } from '../util/logger';\nimport { buildMessageParams, MessageGuard, type MessageStage, type TrackMessageOptions } from '../analytics/messaging';\n\nconst EXIT_QUEUE_STORAGE_KEY = '__deepdots_exit_popup_queue__';\nconst DAY_IN_MS = 24 * 60 * 60 * 1000;\nconst ANALYTICS_MAX_BATCH_SIZE = 20;\nconst ANALYTICS_FLUSH_INTERVAL_MS = 30_000;\n/**\n * Latido: con la pestaña visible (o la app en foreground), el flush periódico manda el\n * engagement acumulado al menos cada ANALYTICS_HEARTBEAT_MS. Sin él, alguien leyendo una\n * sola página no enviaba nada — el page_view sale al SALIR de la pantalla y el engagement\n * al ocultar/cerrar — y el backend daba la sesión por abandonada con solo su session_start.\n */\nconst ANALYTICS_HEARTBEAT_MS = 5 * 60_000;\n/** Una pestaña que vuelve a verse tras este tiempo oculta empieza sesión nueva (timeout de sesión, como GA). */\nconst ANALYTICS_SESSION_TIMEOUT_MS = 30 * 60_000;\n/**\n * Inactividad tras la que el backend cierra una sesión por su cuenta (Run_Jobs\n * `incomplete-surveys`, `IDLE_MINUTES`, 60 min; el barrido corre cada 20). Mantener en sincronía.\n */\nconst ANALYTICS_SERVER_IDLE_MS = 60 * 60_000;\n/**\n * Pasado este tiempo oculta, la sesión se da por cerrada en el backend: reenviarle el cierre\n * la volvería a montar entera en un segundo feedback, así que se descarta sin enviar nada.\n * Margen de 10 min bajo la ventana del backend: el último lote enviado puede ser algo\n * anterior al momento en que se ocultó la pestaña.\n */\nconst ANALYTICS_SESSION_DISCARD_MS = ANALYTICS_SERVER_IDLE_MS - 10 * 60_000;\n/**\n * Una página que vuelve a cargar en la misma pestaña (recarga, navegación con carga completa,\n * bfcache) dentro de este tiempo tras su `pagehide` SIGUE en la sesión suspendida, en vez de\n * abrir otra. Muy por debajo del margen con el que el backend cierra una sesión suspendida\n * (Run_Jobs `incomplete-surveys`, `SUSPEND_GRACE_MINUTES`, 3 min): reanudar una sesión que\n * el backend ya montó la partiría en dos feedbacks. Mantener en sincronía.\n */\nconst ANALYTICS_RESUME_WINDOW_MS = 60_000;\n/** Namespace reservado para los eventos que emite el propio SDK (page_view, message, mini_service…). */\nconst RESERVED_EVENT_PREFIX = 'deepdots_';\n/** Prefijo que se antepone a los eventos custom del host para poder identificarlos frente a los reservados. */\nconst CUSTOM_EVENT_PREFIX = 'deepdots_event_';\n/** Techo del buffer de disparos recibidos mientras se cargan las definiciones (descarta los más viejos). */\nexport const MAX_PENDING_TRIGGERS = 20;\n\n/** Disparo del host recibido antes de que lleguen las definiciones de popups. */\ntype PendingTrigger =\n | { kind: 'event'; name: string }\n | { kind: 'survey'; surveyId: string; popupId?: string };\n\ninterface DeferredExitPopup {\n id: string;\n surveyId: string;\n dueAt: number;\n sourceUrl: string;\n}\n\ninterface LegacyPopupTriggerCondition {\n answered?: boolean;\n cooldownDays?: number;\n}\n\ninterface PopupProgressState {\n status: PopupTriggerConditionStatus;\n timestamp: number;\n}\n\ntype NormalizedPopupDefinition = PopupDefinition & {\n legacyConditions?: LegacyPopupTriggerCondition[];\n};\n\n/**\n * Main class for managing survey popups\n */\nexport class DeepdotsPopups {\n private config: DeepdotsConfig | null = null;\n private listeners: Map<DeepdotsEventType, Set<EventListener>> = new Map();\n private triggers: TriggerConfig[] = [];\n private initialized = false;\n private renderer: PopupRenderer = createDefaultRenderer();\n /** Logger para el output de debug. Default `console`; el host puede inyectar el suyo en init(). */\n private logger: DeepdotsLogger = console;\n private popupDefinitions: NormalizedPopupDefinition[] = [];\n private popupsLoaded = false;\n /**\n * `true` mientras el `GET /sdk/{apiKey}/popups` de `init()` está en vuelo. En esa ventana los\n * disparos del host (`triggerEvent`, `triggerSurvey`) no tienen contra qué evaluarse, así que\n * se guardan en `pendingTriggers` y se reproducen al cargar. Click, scroll, time y exit no\n * hacen falta: son listeners que `autoLaunch` instala ya con las definiciones cargadas.\n */\n private awaitingPopups = false;\n private pendingTriggers: PendingTrigger[] = [];\n private pendingAutoLaunch = false;\n\n // Estado que alimenta las reglas de redisplay. Copia de trabajo en memoria; la fuente\n // duradera es el storage (`popup-state-store`), rehidratada en `init()` — sin eso, una\n // recarga reiniciaba el cooldown y el popup volvía a salir. `answeredSurveys` se deriva\n // de las entradas COMPLETED de `surveyProgress` (los dos los escribe `markSurveyAnswered`).\n private answeredSurveys: Set<string> = new Set();\n private surveyProgress: Map<string, PopupProgressState> = new Map();\n private lastShown: Map<string, number> = new Map(); // popupId -> timestamp mostrado\n private surveyToPopupId: Map<string, string> = new Map(); // surveyId -> popupId\n private deferredExitTimers: number[] = [];\n\n private baseUrl: string = '';\n private env: 'production' | 'development' = 'production';\n /** Si el SDK pinta el \"modal\" del popup (RN). Default true; false = el host lo envuelve. */\n private renderChrome = true;\n /** Barra de progreso. undefined = la decide la plataforma (style.showProgressBar). */\n private showProgressBar: boolean | undefined = undefined;\n /** CSS del host inyectado al final de los estilos del popup. */\n private surveyCss: string | undefined = undefined;\n private cspNonce: string | undefined = undefined;\n\n /** Identidad + sesión (Fase 1 tracking). Null hasta init(). */\n private tracking: TrackingManager | null = null;\n /** Capa de analytics (canal separado del feedback). Null hasta init(). */\n private analytics: AnalyticsManager | null = null;\n /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */\n private analyticsFeedbackSessionId: string | undefined = undefined;\n /** `init({ onFeedbackSession })`: avisa al host al abrir y cerrar cada registro. */\n private onFeedbackSession: DeepdotsInitParams['onFeedbackSession'] = undefined;\n /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */\n private analyticsFlushTimer: ReturnType<typeof setInterval> | undefined = undefined;\n /** Crash & error reporting (#14–17). Null hasta init(). */\n private crashReporter: CrashReporter | null = null;\n /** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */\n private messageGuard = new MessageGuard();\n /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */\n private navObserver: NavigationObserver | null = null;\n /** Tiempo activo (engagement time, #8). */\n private engagement: EngagementTracker | null = null;\n /** Atributos internos del usuario identificado → POST /sdk/popups/contact. Null si no hay userId. */\n private contact: ContactManager | null = null;\n /** Storage resuelto en init() (inyectado por el host o localStorage). Reusado al cambiar de usuario. */\n private storage: KeyValueStorage | null = null;\n /** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */\n private navStarted = false;\n /**\n * `true` entre `session_start` y `session_end`. Hace idempotente tanto la apertura como\n * el cierre (p. ej. `visibilitychange` + `pagehide` seguidos) y permite abrir una sesión\n * nueva al volver a foreground o al conceder el consentimiento más tarde.\n */\n private sessionOpen = false;\n /** Último `user_engagement` emitido (ms epoch): marca el latido. */\n private lastEngagementAt = 0;\n /** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */\n private hiddenAt: number | null = null;\n /**\n * Sesión suspendida por un `pagehide` (ms epoch y `deepdots_user_id`); null si no hay.\n * `openSession` la reanuda si sigue a tiempo (ver ANALYTICS_RESUME_WINDOW_MS).\n */\n private suspended: { at: number; userId: string | null } | null = null;\n /** Integración de analytics del init: la sesión suspendida solo la hereda la misma. */\n private analyticsIntegration: string | undefined = undefined;\n /**\n * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de\n * verdad para el context de analytics Y para la segmentación por idioma de los popups.\n */\n private language: string | undefined = undefined;\n /** `geolocation` del init: si es false no se usa ni la caché ni el lookup. */\n private geoEnabled = true;\n /** Solo se consulta a terceros si el país/ciudad se va a enviar (analytics configurado). */\n private geoWanted = false;\n /** Evita más de un lookup por instancia (el consentimiento puede ir y venir). */\n private geoLookupStarted = false;\n\n /** Initialize the SDK with configuration */\n init(config: DeepdotsInitParams): void {\n this.logger = config.logger ?? console;\n setLogger(this.logger); // los módulos sin `this` (renderPopup, renderers) enrutan por aquí\n if (this.initialized) {\n // Un init() con OTRO userId es un CAMBIO DE USUARIO (login/logout del host): cierra\n // la sesión anterior y cambia la identidad en caliente. Reinicializar de verdad\n // duplicaría listeners, timers y la carga de popups.\n const nextUserId = config.userId || undefined;\n if (nextUserId !== this.config?.userId) {\n this.setUserId(nextUserId);\n return;\n }\n this.log('SDK already initialized');\n return;\n }\n\n const env = resolveEnvironment(config.nodeEnv);\n\n this.baseUrl = env.apiBaseUrl;\n this.env = config.nodeEnv || 'production';\n this.renderChrome = config.renderChrome ?? true;\n this.showProgressBar = config.showProgressBar;\n this.surveyCss = config.surveyCss;\n this.cspNonce = config.cspNonce;\n this.onFeedbackSession = config.onFeedbackSession;\n\n this.config = {\n apiKey: config.apiKey || undefined,\n debug: config.debug || false,\n userId: config.userId || undefined,\n } as DeepdotsConfig;\n\n // user_id lo gestiona el SDK (persistente); el session_id lo provee el backend\n // (respuesta de POST /sdk/popups) y se cachea — el SDK no genera ni expira sesiones.\n const storage = config.storage ?? createDefaultStorage();\n this.storage = storage;\n this.loadPopupState(storage);\n this.tracking = new TrackingManager({\n storage,\n clientUserId: this.config.userId,\n enabled: config.trackingEnabled ?? true,\n });\n\n // Contact: info interna del usuario que solo conoce el host (segmentación/targeting).\n // Solo trackeamos usuarios IDENTIFICADOS → se crea únicamente si hay userId del init.\n if (this.config.userId) {\n const userId = this.config.userId;\n const publicKey = this.config.apiKey ?? '';\n this.contact = new ContactManager({\n storage,\n publicKey,\n userId,\n post: (body) => this.postContact(body),\n });\n // Atributos de contact pasados en init: envío fire-and-forget (solo si cambiaron).\n if (config.contactAttributes) {\n void this.setContactAttributes(config.contactAttributes);\n }\n }\n this.log('tracking · user_id:', this.tracking.getUserId(), '· new_user:', this.tracking.isNewUser(), '· enabled:', this.tracking.isTrackingEnabled());\n\n // Recarga en la misma pestaña: la sesión que suspendió el `pagehide` sigue abierta en el\n // backend. Se recoge aquí para que el sink siga en ese registro desde el primer lote.\n this.analyticsIntegration = config.analytics?.integration;\n const resumable = this.readResumableSession();\n if (resumable) {\n this.analyticsFeedbackSessionId = resumable.sessionId;\n this.suspended = { at: resumable.at, userId: resumable.userId };\n }\n\n // Analytics: se envía como Feedback a la integración (`POST /sdk/feedback`) si se\n // pasan claves en init.analytics; si no, queda en dry-run: no envía nada y el payload\n // solo se pinta con `debug`, para no ensuciar la consola de la web del host en producción.\n const analyticsSink = config.analytics\n ? createFeedbackSink({\n baseUrl: this.baseUrl,\n keys: config.analytics,\n log: (...a) => this.log(...a),\n initialSessionId: resumable?.sessionId,\n onSessionId: (id) => {\n this.analyticsFeedbackSessionId = id;\n this.notifyFeedbackSession(id, 'open');\n },\n onSessionClosed: (id) => this.notifyFeedbackSession(id, 'closed'),\n onSessionReset: () => { this.analyticsFeedbackSessionId = undefined; },\n })\n : createDryRunSink((...a) => { if (this.config?.debug) this.logger.log(...a); });\n const device = config.device ?? collectDeviceInfo(config.appVersion);\n this.language = resolveLanguage({\n explicit: config.language,\n navigator: typeof navigator !== 'undefined' ? navigator : undefined,\n intl: typeof Intl !== 'undefined' ? Intl : undefined,\n });\n this.messageGuard.reset();\n this.analytics = new AnalyticsManager({\n sink: analyticsSink,\n publicKey: config.analytics?.publicKey ?? this.config.apiKey,\n language: this.language,\n platform: config.platform ?? 'web',\n device,\n maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,\n onFlushNeeded: () => this.flushAnalytics(),\n });\n // Crash & error reporting (#14–17): captura errores no manejados (a disco, replay\n // en el siguiente arranque) y expone reportError() para el host (emite ya).\n this.crashReporter = new CrashReporter({\n storage,\n emit: (params) => this.track('deepdots_app_crash', params),\n device: () => ({ appVersion: device.app_version, osVersion: device.os_version, deviceModel: device.device_model }),\n sessionId: () => this.tracking?.getSessionId() ?? null,\n now: () => Date.now(),\n enabled: () => this.tracking?.isTrackingEnabled() ?? false,\n });\n if (this.tracking?.isTrackingEnabled()) {\n this.crashReporter.install();\n }\n // Geolocalización por IP: ver `resolveGeo()` para cuándo se consulta a terceros.\n this.geoEnabled = config.geolocation ?? true;\n this.geoWanted = this.geoEnabled && !!config.analytics;\n this.resolveGeo();\n // Fase 2: navegación → eventos page_view por el canal de analytics.\n this.navObserver = new NavigationObserver();\n this.navObserver.onVisit((v) =>\n this.track('deepdots_page_view', { screen: v.screen, path: v.path, duration_seconds: v.durationSeconds })\n );\n this.navObserver.install();\n // Engagement time (#8): cuenta tiempo activo en primer plano.\n // No arranca aquí: lo hace `openSession`, y solo con la pestaña visible.\n this.engagement = new EngagementTracker();\n this.setupAnalyticsFlush();\n // Marca de inicio de sesión (base para Crash-Free Users #14), o reanudación de la que\n // suspendió la carga anterior de esta pestaña.\n this.openSession('cold_start');\n if (resumable && this.analyticsFeedbackSessionId === resumable.sessionId) {\n this.notifyFeedbackSession(resumable.sessionId, 'open');\n }\n // Drena SIEMPRE la cola (descarta los pendientes si tracking está off, spec §7);\n // solo reenvía como evento cuando el tracking está activo.\n const pendingCrashes = this.crashReporter.drainPendingCrashes();\n if (this.tracking?.isTrackingEnabled()) {\n for (const rec of pendingCrashes) {\n this.track('deepdots_app_crash', crashRecordToParams(rec));\n }\n }\n\n this.initialized = true;\n this.log('SDK initialized', this.config);\n // El contenedor del popup (`#deepdots-popup-container`) lo crea el renderer, no el core.\n if (this.renderer.init) this.renderer.init();\n\n // Los popups SIEMPRE se reciben de la API (no se definen en init). Sin apiKey no hay\n // GET que esperar, así que tampoco se guardan disparos.\n this.awaitingPopups = Boolean(this.config.apiKey && this.baseUrl);\n this.fetchPopupsFromServer().then((defs) => {\n if (!defs) this.dropPendingTriggers();\n this.popupDefinitions = this.validatePopupDefinitions(defs ?? []);\n this.popupsLoaded = true;\n this.log('Popups loaded from API', this.popupDefinitions);\n // Con al menos un popup que pueda abrirse, el renderer se trae ya lo que costará la\n // apertura (en navegador, el chunk de renderPopup). Un host sin popups no descarga\n // nada.\n if (this.popupDefinitions.length) this.renderer.preload?.();\n this.configureTriggersFromDefinitions();\n this.processDeferredExitQueue();\n if (this.pendingAutoLaunch) {\n this.startTriggers();\n }\n this.replayPendingTriggers();\n });\n }\n\n /** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */\n getUserId(): string | null {\n return this.tracking?.getUserId() ?? null;\n }\n\n /** Session id de navegación actual. Null si tracking off. */\n getSessionId(): string | null {\n return this.tracking?.getSessionId() ?? null;\n }\n\n /**\n * Id del registro de analytics abierto: el `sessionId` que devolvió `POST /sdk/feedback`,\n * que la API guarda como `sdkSessionId` en el Feedback en que se convierte la sesión. No es\n * `getSessionId()`. Null hasta que el backend acepta el primer lote y de nuevo al cerrar la\n * sesión: para saber también qué registro se cerró, usa `init({ onFeedbackSession })`.\n */\n getFeedbackSessionId(): string | null {\n return this.analyticsFeedbackSessionId ?? null;\n }\n\n /** Avisa al host del registro abierto o cerrado; un fallo del host no corta el envío. */\n private notifyFeedbackSession(sessionId: string, status: FeedbackSession['status']): void {\n try {\n this.onFeedbackSession?.({ sessionId, status });\n } catch (error) {\n this.log('analytics · onFeedbackSession threw:', error);\n }\n }\n\n /** Activa/desactiva el tracking (identidad + sesión + analytics). Kill-switch del contrato §7bis. */\n setTrackingEnabled(enabled: boolean): void {\n // Al revocar el consentimiento se cierra la sesión ANTES de apagar el canal: si no, lo\n // acumulado se quedaría sin enviar y el registro abierto sin cerrar.\n if (!enabled) this.closeSession('tracking_disabled');\n this.tracking?.setTrackingEnabled(enabled);\n // Consentimiento concedido (ahora o más tarde que el init): abre sesión.\n if (enabled) this.openSession('tracking_enabled');\n // El lookup de geo se aplaza hasta que haya consentimiento.\n if (enabled) this.resolveGeo();\n this.log('tracking · setTrackingEnabled:', enabled, '· session_id:', this.tracking?.getSessionId() ?? null);\n }\n\n // ───────── Analytics (canal separado del feedback, vinculado por user_id) ─────────\n\n /** Registra un evento de analítica (modelo GA: nombre + params). No-op si tracking off. */\n track(name: string, params?: Record<string, unknown>): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n // Los eventos del SDK ya van en el namespace `deepdots_`; los custom del host se\n // prefijan con `deepdots_event_` para poder distinguirlos en analítica.\n const eventName = name.startsWith(RESERVED_EVENT_PREFIX) ? name : `${CUSTOM_EVENT_PREFIX}${name}`;\n this.analytics?.track(eventName, params);\n this.log('analytics · track:', eventName, params ?? {});\n }\n\n /** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */\n setUserAttributes(attributes: Record<string, string | number | boolean>): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n this.analytics?.setUserAttributes(attributes);\n }\n\n /**\n * Registra/actualiza una métrica (valor medible) → `feedback.metrics` del body.\n * Persistente y sobrescribe por key. No-op si tracking off.\n */\n setMetric(key: string, value: string | number | boolean): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n this.analytics?.setMetric(key, value);\n this.log('analytics · setMetric:', key, value);\n }\n\n /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */\n reportError(error: unknown, options?: ReportErrorOptions): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n this.crashReporter?.reportError(error, options);\n }\n\n /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */\n installReactNativeCrashHandler(errorUtils: ReactNativeErrorUtils): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n this.crashReporter?.installReactNative(errorUtils);\n }\n\n /**\n * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el\n * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del\n * init. Solo envía si los atributos cambiaron (diff en storage). No-op si tracking off o sin userId.\n * @returns `true` si se envió al backend, `false` si no hubo cambios o está deshabilitado.\n */\n async setContactAttributes(attributes: ContactAttributes): Promise<boolean> {\n if (!this.tracking?.isTrackingEnabled()) return false;\n if (!this.contact) return false;\n return this.contact.setAttributes(attributes);\n }\n\n /** Marca el inicio de un mini-service; etiqueta los eventos siguientes. No-op si tracking off. */\n enterMiniService(name: string, entryPointType?: string): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n this.analytics?.enterMiniService(name, entryPointType);\n this.log('analytics · enterMiniService:', name, entryPointType ?? '');\n }\n\n /** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */\n exitMiniService(name: string): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n this.analytics?.exitMiniService(name);\n this.log('analytics · exitMiniService:', name);\n }\n\n /** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */\n trackSearch(query: string, resultsCount: number, params?: Record<string, unknown>): void {\n this.track('deepdots_search', { query, results_count: resultsCount, has_results: resultsCount > 0, ...(params ?? {}) });\n }\n\n /** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */\n trackFindabilityFriction(frictionTopic: string, params?: Record<string, unknown>): void {\n this.track('deepdots_findability_friction', { friction_topic: frictionTopic, ...(params ?? {}) });\n }\n\n /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */\n trackFunnelStep(funnel: string, step: string, taskId: string, params?: Record<string, unknown>): void {\n this.track('deepdots_funnel_step', { funnel, step, task_id: taskId, ...(params ?? {}) });\n }\n\n /**\n * Product Effectiveness: una interacción significativa del usuario (`interaction_type` es la\n * dimensión de agrupación, p. ej. `get_help`, `homepage`, `contact_support`).\n *\n * Existe como evento RESERVADO para que la convención no la invente cada host: un `track()`\n * custom acaba como `deepdots_event_…` y no lo cuenta la analítica de effectiveness.\n * Un `interactionType` vacío se descarta con warning (no aporta dato agrupable).\n */\n trackMeaningfulInteraction(interactionType: string, params?: Record<string, unknown>): void {\n const type = typeof interactionType === 'string' ? interactionType.trim() : '';\n if (!type) {\n // Siempre visible (como en `trackMessage`): es un error de integración del host.\n console.warn('[DeepdotsPopups] trackMeaningfulInteraction discarded: empty interaction_type');\n return;\n }\n this.track('deepdots_meaningful_interaction', { interaction_type: type, ...(params ?? {}) });\n }\n\n /**\n * Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app).\n * No-op si tracking off. El evento se descarta (con warning) si el `channel` no es válido,\n * si ese `(message_id, stage)` ya se emitió en la sesión, o si el `message_id` ya se reportó\n * en otro canal — ver `MessageGuard`.\n */\n trackMessage(stage: MessageStage, options: TrackMessageOptions): void {\n const verdict = this.messageGuard.evaluate(stage, options);\n if (!verdict.emit) {\n console.warn(`[DeepdotsPopups] trackMessage discarded (${verdict.reason}): ${verdict.detail}`);\n return;\n }\n this.track('deepdots_message', buildMessageParams(stage, options));\n }\n\n /**\n * Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).\n * El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.\n */\n setScreen(name: string): void {\n if (!this.navObserver) return;\n if (!this.navStarted) {\n this.navObserver.begin(name);\n this.navStarted = true;\n } else {\n this.navObserver.visit(name);\n }\n }\n\n /** App a foreground (RN: AppState 'active'): reanuda el engagement time y abre sesión nueva. */\n onForeground(): void {\n this.engagement?.resume();\n this.openSession('foreground');\n }\n\n /**\n * App a background (RN: AppState 'background'): FIN DE SESIÓN. Cierra pantalla actual\n * (page_view), mini-services (mini_service_exit), emite engagement + `session_end` y\n * envía el último lote con `completed:true`.\n *\n * Es la única señal disponible en móvil: el kill de la app (swipe) NO da callback.\n */\n onBackground(): void {\n this.closeSession('background');\n this.engagement?.pause();\n }\n\n /**\n * Cierre EXPLÍCITO de la sesión por parte del host (logout, fin de flujo…): emite\n * `session_end` y envía el último lote con `completed:true`.\n */\n endSession(): void {\n this.closeSession('manual');\n }\n\n /**\n * Cambio de usuario (login / logout / cambio de cuenta). Cierra la sesión del usuario\n * anterior (`session_end` con `reason: 'user_change'` + `completed:true`), cambia la\n * identidad y abre una sesión nueva. Sin `userId` vuelve al id anónimo del SDK.\n *\n * Equivale a llamar a `init()` otra vez con otro `userId`.\n */\n setUserId(userId?: string): void {\n const next = userId || undefined;\n if (!this.initialized || !this.config || next === this.config.userId) return;\n\n this.closeSession('user_change');\n\n const enabled = this.tracking?.isTrackingEnabled() ?? true;\n const storage = this.storage ?? createDefaultStorage();\n this.config.userId = next;\n this.tracking = new TrackingManager({ storage, clientUserId: next, enabled });\n // El Contact solo existe para usuarios identificados.\n this.contact = next\n ? new ContactManager({\n storage,\n publicKey: this.config.apiKey ?? '',\n userId: next,\n post: (body) => this.postContact(body),\n })\n : null;\n // Atributos, métricas y protecciones de messaging pertenecían al usuario anterior.\n this.analytics?.resetUserScope();\n this.messageGuard.reset();\n // El historial de redisplay era del usuario anterior: el nuevo empieza de cero.\n this.lastShown.clear();\n this.surveyProgress.clear();\n this.answeredSurveys.clear();\n clearPopupState(storage);\n\n this.log('tracking · user_change · user_id:', this.tracking.getUserId());\n this.openSession('user_change');\n }\n\n /**\n * Cierra la sesión actual: vuelca todo lo que quedaba abierto y envía el último lote\n * marcado con `completed:true`, para que el backend cierre el registro. El `sessionId`\n * cacheado se olvida → el lote siguiente abre un registro nuevo.\n *\n * Idempotente: dos cierres seguidos (p. ej. `visibilitychange` + `pagehide`) no duplican\n * el `session_end`.\n */\n private closeSession(reason: SessionEndReason): void {\n if (!this.sessionOpen) return;\n if (!this.tracking?.isTrackingEnabled()) return;\n this.sessionOpen = false;\n\n this.navObserver?.stop(); // cierra la pantalla actual → page_view\n this.navStarted = false;\n this.analytics?.exitAllMiniServices(); // → mini_service_exit con duración\n this.flushEngagement(); // → user_engagement con el tiempo activo\n // Parado hasta la sesión siguiente. Si siguiera en marcha, lo que llega después del\n // cierre (en Chromium el `visibilitychange` va DESPUÉS de `pagehide`) emitía un\n // `user_engagement` sin sessionId que abría en la API un registro que nunca se cierra.\n this.engagement?.pause();\n this.track('deepdots_session_end', { reason });\n // `final` solo cuando el documento se está muriendo (el transporte no espera respuestas).\n this.flushAnalytics({ final: reason === 'page_hide', sessionEnd: true });\n // El session_id del canal de analytics y el de popups pertenecían a la sesión cerrada.\n this.analyticsFeedbackSessionId = undefined;\n this.tracking?.setSessionId(null);\n this.log('tracking · session_end:', reason);\n }\n\n /**\n * Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente.\n * El `reason` dice por qué empieza (ver `SessionStartReason`):\n * - `cold_start`: init() (carga de la página / arranque de la app RN).\n * - `foreground`: onForeground() tras un cierre (RN).\n * - `tracking_enabled`: setTrackingEnabled(true).\n * - `user_change`: setUserId() o init() con otro userId.\n * - `idle_timeout`: la pestaña vuelve a verse tras más de 30 min oculta.\n * - `bfcache_restore`: vuelta desde la bfcache fuera de la ventana de reanudación.\n * Si hay una sesión suspendida a tiempo emite `deepdots_session_resume` en su lugar, sin reason.\n */\n private openSession(reason: SessionStartReason): void {\n if (this.sessionOpen) return;\n if (!this.tracking?.isTrackingEnabled()) return;\n const suspendedMs = this.takeSuspendedSession();\n this.sessionOpen = true;\n this.lastEngagementAt = Date.now();\n if (suspendedMs === null) {\n this.track('deepdots_session_start', { reason });\n } else {\n // Misma sesión: sin `session_start`, que contaría una sesión de más.\n this.track('deepdots_session_resume', { suspended_ms: suspendedMs });\n this.log('tracking · session resumed after', suspendedMs, 'ms:', this.analyticsFeedbackSessionId);\n }\n // El cierre anterior dejó sin pantalla al observador: sin esto, la pantalla en la que\n // está el usuario al reabrir (cambio de usuario, vuelta de la bfcache) no contaba.\n this.navObserver?.restart();\n // Una pestaña abierta en segundo plano no ha sido vista: no cuenta hasta que se muestre.\n if (this.isDocumentVisible()) this.engagement?.resume();\n }\n\n /** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */\n private isDocumentVisible(): boolean {\n return typeof document === 'undefined' || document.visibilityState !== 'hidden';\n }\n\n /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */\n previewAnalytics(): AnalyticsEnvelope {\n return (\n this.analytics?.buildPayload(this.analyticsIdentity()) ?? {\n userId: null,\n sessionId: null,\n context: { platform: 'web', attributes: {} },\n events: [],\n }\n );\n }\n\n /**\n * Envía el lote acumulado de analytics y vacía el buffer.\n * `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque\n * el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.\n */\n flushAnalytics(options?: { final?: boolean; sessionEnd?: boolean }): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n this.analytics?.flush(this.analyticsIdentity(), options);\n }\n\n /**\n * Emite un evento `user_engagement` con el tiempo activo acumulado (#8) y la pantalla en\n * curso: `screen` y `path` (los mismos que llevará su `page_view`) y `screen_seconds` (lo\n * que lleva en ella, medido como el `duration_seconds` del `page_view`).\n *\n * El `page_view` sale al SALIR de la pantalla; si el cierre no llega nunca (pestaña dormida\n * y descartada al volver, pestaña o app matada en segundo plano), la última pantalla de la\n * sesión se perdía y una visita de una sola pantalla se quedaba sin ninguna. Con la pantalla\n * en el latido y en el engagement de ocultar la pestaña, el backend la reconstruye.\n *\n * En los cierres (`closeSession`, `suspendSession`) el observador ya emitió el `page_view`\n * y no tiene pantalla, así que ese engagement va sin `screen`: nunca se cuenta dos veces.\n * Tampoco se emite un `page_view` aquí, por lo mismo.\n */\n private flushEngagement(): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n const ms = this.engagement?.consume() ?? 0;\n if (ms > 0) {\n const current = this.navObserver?.current();\n this.track('deepdots_user_engagement', {\n engagement_time_msec: ms,\n ...(current\n ? { screen: current.screen, path: current.path, screen_seconds: current.seconds }\n : {}),\n });\n this.lastEngagementAt = Date.now();\n }\n }\n\n /** Tick del flush periódico: latido de engagement si toca, y envío de lo acumulado. */\n private onAnalyticsFlushTick(): void {\n if (\n this.sessionOpen &&\n this.isDocumentVisible() &&\n Date.now() - this.lastEngagementAt >= ANALYTICS_HEARTBEAT_MS\n ) {\n this.flushEngagement();\n }\n this.flushAnalytics();\n }\n\n /**\n * La pestaña vuelve a verse. Tras más de ANALYTICS_SESSION_TIMEOUT_MS oculta, la sesión\n * anterior termina y empieza otra: seguir con el mismo sessionId mezclaba dos visitas y,\n * si el backend ya la había cerrado por inactividad, la partía en dos feedbacks.\n * - Oculta menos de ANALYTICS_SESSION_DISCARD_MS: el backend aún la tiene abierta → se\n * cierra con normalidad (page_view de la pantalla, session_end `idle_timeout`, completed).\n * - Más: ya la cerró el backend → se descarta sin enviar nada.\n */\n private onDocumentVisible(): void {\n const hiddenFor = this.hiddenAt === null ? 0 : Date.now() - this.hiddenAt;\n this.hiddenAt = null;\n if (!this.sessionOpen) return; // sin sesión abierta no hay dónde contar ese tiempo\n if (hiddenFor < ANALYTICS_SESSION_TIMEOUT_MS) {\n this.engagement?.resume();\n return;\n }\n if (hiddenFor < ANALYTICS_SESSION_DISCARD_MS) {\n this.closeSession('idle_timeout');\n } else {\n this.discardSession();\n }\n this.openSession('idle_timeout');\n }\n\n /**\n * `pagehide` en web: SUSPENDE la sesión en vez de cerrarla, porque el navegador lo lanza\n * igual al recargar que al cerrar la pestaña. Vuelca lo pendiente (page_view de la\n * pantalla, mini-services, engagement) con un `deepdots_session_suspend` y SIN\n * `completed`, y deja el registro en `sessionStorage` (ver `suspended-session.ts`):\n * - si la página vuelve a cargar en esta pestaña, `init()` sigue en el mismo registro;\n * - si la pestaña se cerró, el backend lo cierra a los pocos minutos sin otro lote.\n *\n * Devuelve false cuando no se puede suspender — aún no se conoce el registro (el primer\n * POST sigue en vuelo) o no hay `sessionStorage` — y entonces el caller cierra como antes.\n */\n private suspendSession(): boolean {\n if (!this.sessionOpen || !this.tracking?.isTrackingEnabled()) return false;\n const sessionId = this.analyticsFeedbackSessionId;\n const integration = this.analyticsIntegration;\n const storage = getSessionStorage();\n if (!sessionId || !integration || !storage) return false;\n const at = Date.now();\n const userId = this.tracking.getUserId();\n if (!writeSuspendedSession(storage, { sessionId, integration, userId, at })) return false;\n\n this.sessionOpen = false;\n this.suspended = { at, userId };\n this.navObserver?.stop(); // cierra la pantalla actual → page_view\n this.navStarted = false;\n this.analytics?.exitAllMiniServices();\n this.flushEngagement();\n this.engagement?.pause(); // como en closeSession: nada de engagement tras el pagehide\n this.track('deepdots_session_suspend', { reason: 'page_hide' });\n this.flushAnalytics({ final: true });\n this.log('tracking · session suspended:', sessionId);\n return true;\n }\n\n /**\n * Recoge la sesión que dejó suspendida la carga anterior de esta pestaña (ver\n * `suspendSession`). Solo con analytics configurado y el tracking activo: sin ellos no hay\n * registro en el que seguir.\n */\n private readResumableSession() {\n const integration = this.analyticsIntegration;\n const storage = getSessionStorage();\n if (!integration || !storage || !this.tracking?.isTrackingEnabled()) return null;\n return takeSuspendedSession(\n storage,\n { integration, userId: this.tracking.getUserId() },\n Date.now(),\n ANALYTICS_RESUME_WINDOW_MS,\n );\n }\n\n /**\n * Consume la sesión suspendida al abrir: los ms que estuvo suspendida si se puede seguir en\n * ella (mismo usuario, dentro de ANALYTICS_RESUME_WINDOW_MS); null si no había o ya no toca.\n * En ese caso se olvida su registro sin enviar nada: lo cierra el backend, y la sesión que\n * se abre ahora va a uno nuevo.\n */\n private takeSuspendedSession(): number | null {\n const suspended = this.suspended;\n if (!suspended) return null;\n this.suspended = null;\n const storage = getSessionStorage();\n if (storage) clearSuspendedSession(storage);\n const suspendedMs = Math.max(0, Date.now() - suspended.at);\n if (\n this.analyticsFeedbackSessionId &&\n suspendedMs <= ANALYTICS_RESUME_WINDOW_MS &&\n suspended.userId === (this.tracking?.getUserId() ?? null)\n ) {\n return suspendedMs;\n }\n this.analytics?.discardSession(this.analyticsIdentity());\n this.analyticsFeedbackSessionId = undefined;\n this.tracking?.setSessionId(null);\n return null;\n }\n\n /** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */\n private discardSession(): void {\n this.sessionOpen = false;\n this.navObserver?.discard();\n this.analytics?.discardSession(this.analyticsIdentity());\n this.engagement?.consume(); // tiramos lo acumulado: era de la sesión descartada\n this.engagement?.pause();\n this.analyticsFeedbackSessionId = undefined;\n this.tracking?.setSessionId(null);\n this.log('tracking · session discarded after', ANALYTICS_SESSION_DISCARD_MS / 60_000, 'min hidden');\n }\n\n /**\n * Geolocalización por IP (país/ciudad para analytics). La caché persistente es local y se\n * aplica siempre que `geolocation` no esté desactivado. El lookup a terceros (ipapi.co →\n * ipwho.is → ipinfo.io, fallback con timeout) solo se hace si el dato se va a ENVIAR:\n * analytics configurado + tracking activo + caché ausente o caducada. Una vez por instancia.\n */\n private resolveGeo(): void {\n const storage = this.storage;\n if (!this.geoEnabled || !storage || !this.analytics) return;\n const cachedGeo = readCachedGeo(storage, Date.now());\n if (cachedGeo) {\n this.analytics.updateDevice(cachedGeo);\n return;\n }\n if (!this.geoWanted || this.geoLookupStarted) return;\n if (!this.tracking?.isTrackingEnabled()) return;\n this.geoLookupStarted = true;\n fetchGeo()\n .then((geo) => {\n if (geo) {\n this.analytics?.updateDevice(geo);\n writeCachedGeo(storage, geo, Date.now());\n }\n })\n .catch(() => {});\n }\n\n private analyticsIdentity() {\n return {\n userId: this.tracking?.getUserId() ?? null,\n sessionId: this.tracking?.getSessionId() ?? null,\n };\n }\n\n /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */\n private setupAnalyticsFlush(): void {\n // Flush periódico + latido en todos los entornos. En RN no había ninguno: una app en\n // foreground que no llegaba a `maxBatchSize` no enviaba nada hasta ir a background.\n this.startAnalyticsFlushTimer();\n if (typeof document === 'undefined' || typeof window === 'undefined') return;\n // al ocultar la página (recarga, navegación o cierre, indistinguibles aquí): se SUSPENDE\n // la sesión para que una recarga siga en ella (ver suspendSession). Si no se puede, FIN\n // DE SESIÓN como antes (page_view + mini_service_exit + engagement + session_end) y\n // último lote con completed:true.\n window.addEventListener('pagehide', () => {\n clearInterval(this.analyticsFlushTimer);\n if (!this.suspendSession()) this.closeSession('page_hide');\n });\n // Vuelta desde la bfcache (botón atrás): la página no se recarga, así que `init()` no\n // vuelve a correr. El `pagehide` suspendió (o cerró) la sesión y paró el flush; sin\n // reabrir ambos, la pestaña no mandaba nada más hasta una recarga. `openSession`\n // reanuda la suspendida si sigue a tiempo.\n window.addEventListener('pageshow', (event) => {\n if (!(event as PageTransitionEvent).persisted) return;\n this.startAnalyticsFlushTimer();\n this.openSession('bfcache_restore');\n });\n // al ocultar/mostrar la pestaña: pausar/reanudar engagement y enviar lo acumulado. El\n // engagement de ocultar lleva la pantalla en curso: si la pestaña no vuelve (la descarta\n // el navegador, se cierra dormida) es lo último que se sabe de dónde estaba el usuario.\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') {\n this.flushEngagement();\n this.engagement?.pause();\n this.flushAnalytics();\n this.hiddenAt = Date.now();\n } else if (document.visibilityState === 'visible') {\n this.onDocumentVisible();\n }\n });\n }\n\n private startAnalyticsFlushTimer(): void {\n clearInterval(this.analyticsFlushTimer);\n this.analyticsFlushTimer = setInterval(() => this.onAnalyticsFlushTick(), ANALYTICS_FLUSH_INTERVAL_MS);\n }\n\n /** Enable auto-launch functionality with configured triggers */\n autoLaunch(): void {\n if (!this.initialized) {\n throw new Error('SDK not initialized. Call init() first.');\n }\n if (!this.popupsLoaded) {\n this.pendingAutoLaunch = true;\n this.log('Auto-launch deferred until popups are loaded');\n return;\n }\n this.startTriggers();\n }\n\n /** Inicia los triggers configurados */\n private startTriggers(): void {\n this.log('Auto-launch enabled');\n this.triggers.forEach((trigger) => setupTrigger(this, trigger));\n }\n\n /** Configure triggers for auto-launching popups (manual) */\n configureTriggers(triggers: TriggerConfig[]): void {\n if (!this.initialized) {\n throw new Error('SDK not initialized. Call init() first.');\n }\n this.triggers = triggers;\n this.log('Triggers configured', triggers);\n }\n\n /** Deriva triggers desde las definiciones de popup */\n private configureTriggersFromDefinitions(): void {\n const derived: TriggerConfig[] = [];\n this.debug('Validated popup definitions', this.popupDefinitions);\n\n this.popupDefinitions.forEach((def) => {\n def.triggers.forEach((trigger) => {\n const type = this.mapPopupTriggerType(trigger.type);\n if (!type) {\n this.debug('Unsupported trigger type', trigger.type);\n return;\n }\n const value = type === 'time' && typeof trigger.value === 'number'\n ? trigger.value * 1000\n : trigger.value;\n derived.push({ type, value, surveyId: def.surveyId, popupId: def.id });\n });\n });\n\n if (derived.length) {\n this.configureTriggers(derived);\n }\n }\n\n /** Lógica para evaluar condiciones antes de mostrar una encuesta */\n triggerSurvey(surveyId: string, popupId?: string): void {\n if (this.bufferIfAwaitingPopups({ kind: 'survey', surveyId, popupId })) return;\n const def = this.findPopupDefinition(surveyId, popupId);\n if (!def) {\n this.debug('No popup definition for trigger', { surveyId, popupId });\n return;\n }\n if (!this.shouldShow(def)) {\n this.debug('Conditions prevented showing popup', def.id);\n return;\n }\n this.showDefinition(def);\n }\n\n /** Trigger popups configured with trigger.type = 'event' and matching trigger.value */\n public triggerEvent(eventName: string): void {\n if (!this.initialized) {\n throw new Error('SDK not initialized. Call init() first.');\n }\n const normalized = String(eventName || '').trim();\n if (!normalized) {\n this.debug('Ignoring empty event trigger name');\n return;\n }\n if (this.bufferIfAwaitingPopups({ kind: 'event', name: normalized })) return;\n this.evaluateEvent(normalized);\n }\n\n private evaluateEvent(normalized: string): void {\n const candidates = this.popupDefinitions.filter((def) => {\n return def.triggers.some((trigger) => {\n const triggerValue = String(trigger.value ?? '').trim();\n return trigger.type === 'event' && triggerValue === normalized;\n });\n });\n\n if (!candidates.length) {\n this.debug('No event popup definitions found', { eventName: normalized });\n return;\n }\n\n const matched = candidates.find((def) => this.shouldShow(def));\n if (!matched) {\n this.debug('Event popup blocked by conditions/segments', { eventName: normalized });\n return;\n }\n\n this.showDefinition(matched);\n }\n\n /**\n * Guarda `trigger` si las definiciones aún no han llegado. Devuelve `true` si lo guardó (el\n * llamador no debe evaluarlo ahora) y `false` si no hay carga en vuelo (se evalúa ya, como\n * siempre: tras cargar, tras un fallo o sin apiKey).\n */\n private bufferIfAwaitingPopups(trigger: PendingTrigger): boolean {\n if (!this.awaitingPopups) return false;\n if (this.pendingTriggers.length >= MAX_PENDING_TRIGGERS) {\n const dropped = this.pendingTriggers.shift();\n this.debug('Pending trigger buffer full; dropped oldest', dropped);\n }\n this.pendingTriggers.push(trigger);\n this.debug('Popups not loaded yet; trigger buffered', trigger);\n return true;\n }\n\n /** La carga falló: sin definiciones no hay nada que reproducir, se descarta todo. */\n private dropPendingTriggers(): void {\n const dropped = this.pendingTriggers.length;\n this.awaitingPopups = false;\n this.pendingTriggers = [];\n if (dropped > 0) this.debug('Popups failed to load; dropped pending triggers', dropped);\n }\n\n /** Reproduce en orden por la evaluación normal (segmentos, cooldowns y condiciones de AHORA). */\n private replayPendingTriggers(): void {\n this.awaitingPopups = false;\n const replay = this.pendingTriggers;\n this.pendingTriggers = [];\n if (!replay.length) return;\n this.debug('Replaying triggers received before popups loaded', replay.length);\n replay.forEach((trigger) => {\n if (trigger.kind === 'event') this.evaluateEvent(trigger.name);\n else this.triggerSurvey(trigger.surveyId, trigger.popupId);\n });\n }\n\n private showDefinition(def: PopupDefinition): void {\n this.log('Showing popup (definition)', def);\n this.surveyToPopupId.set(def.surveyId, def.id);\n this.lastShown.set(def.id, Date.now());\n this.persistPopupState();\n this.renderPopup(def.surveyId, def.productId, def.actions, def.style, def.title);\n this.emitEvent('popup_shown', def.surveyId, { popupId: def.id });\n }\n\n private shouldShow(def: NormalizedPopupDefinition, pathUrl?: string, skipPathCheck = false): boolean {\n // Otra pestaña puede haber enseñado o contestado este popup desde nuestro `init()`.\n if (this.storage) this.loadPopupState(this.storage);\n // La exclusión se evalúa SIEMPRE, también cuando `skipPathCheck` levanta la\n // restricción de \"dónde PUEDE mostrarse\" (popup de exit ya encolado): decir\n // \"no en /cart\" es una regla sobre la pantalla en la que se va a pintar.\n if (this.isPathExcluded(def, pathUrl)) {\n return false;\n }\n if (!skipPathCheck && !this.matchesSegmentsPath(def, pathUrl)) {\n return false;\n }\n if (!this.matchesSegmentsLang(def)) {\n return false;\n }\n\n const cooldowns = def.cooldown || [];\n if (cooldowns.length && !cooldowns.every((cooldown) => this.evaluateCooldown(def, cooldown))) {\n return false;\n }\n\n const legacyConditions = def.legacyConditions || [];\n if (!legacyConditions.length) return true;\n return legacyConditions.every((condition) => this.evaluateLegacyCondition(def, condition));\n }\n\n private matchesSegmentsLang(def: PopupDefinition): boolean {\n const langs = def.segments?.lang;\n if (!langs || langs.length === 0) return true;\n\n // Mismo idioma que reporta el context de analytics: explícito > navigator.language\n // > Intl. ⚠️ NO leer `navigator.language` aquí: en React Native `navigator` existe\n // pero no tiene `language`, y `undefined.toLowerCase()` reventaba la evaluación\n // COMPLETA del trigger (no solo este popup).\n const currentLang = (this.language ?? '').toLowerCase();\n\n if (!currentLang) {\n this.debug('No language available for lang comparison', { popupId: def.id, langs });\n return true;\n }\n\n // Acepta si el idioma actual empieza con alguno de los configurados\n // ej: \"en-US\" coincide con segment \"en\". Ignora entradas no-string del backend.\n const matches = langs.some(\n (lang) => typeof lang === 'string' && currentLang.startsWith(lang.toLowerCase()),\n );\n if (!matches) {\n this.debug('Lang mismatch for popup', { popupId: def.id, langs, currentLang });\n }\n return matches;\n }\n\n /** `segments.path`: rutas donde el popup PUEDE mostrarse. Sin lista, en todas. */\n private matchesSegmentsPath(def: PopupDefinition, pathUrl?: string): boolean {\n const paths = def.segments?.path;\n if (!Array.isArray(paths) || paths.length === 0) return true;\n\n const location = this.resolveCurrentLocation(pathUrl);\n if (!location) {\n this.debug('No window.location available for path comparison', { popupId: def.id, paths });\n return true;\n }\n\n const matches = paths.some((rawCandidate) => {\n const match = this.matchesPathCandidate(rawCandidate, location);\n this.debug('Path comparison', {\n popupId: def.id,\n candidate: rawCandidate,\n currentPath: location.path,\n currentHref: location.href,\n match,\n });\n return match;\n });\n\n if (!matches) {\n this.debug('No path match for popup', { popupId: def.id, paths, currentPath: location.path });\n }\n\n return matches;\n }\n\n /**\n * `segments.excludedPaths`: rutas donde el popup NO debe mostrarse. Gana sobre\n * `segments.path` (`path: ['/']` + `excludedPaths: ['/cart']` = en todas menos el\n * carrito) y se aplica también cuando no hay `path` (= en todas menos las excluidas).\n */\n private isPathExcluded(def: PopupDefinition, pathUrl?: string): boolean {\n const excluded = def.segments?.excludedPaths;\n if (!Array.isArray(excluded) || excluded.length === 0) return false;\n\n const location = this.resolveCurrentLocation(pathUrl);\n if (!location) {\n // Sin `window.location` (RN/SSR) no se puede evaluar: no bloqueamos, igual\n // que hace `matchesSegmentsPath` con la lista de inclusión.\n this.debug('No window.location available for excluded path comparison', { popupId: def.id, excluded });\n return false;\n }\n\n const hit = excluded.find((rawCandidate) => this.matchesPathCandidate(rawCandidate, location));\n if (hit !== undefined) {\n this.debug('Popup excluded by path', {\n popupId: def.id,\n candidate: hit,\n currentPath: location.path,\n currentHref: location.href,\n });\n return true;\n }\n return false;\n }\n\n /**\n * Compara UN candidato de `segments.path`/`segments.excludedPaths` con la ruta\n * actual. Mismas reglas para incluir y para excluir:\n * - URL absoluta → href completo exacto\n * - empieza por / → subcadena del href (así entran las rutas con hash: `/#/home`)\n * - resto → pathname exacto\n * Ignora entradas que no sean string: el backend puede colar un `null` en la lista\n * y no puede tumbar la evaluación del trigger entero (mismo fallo que hubo con\n * `segments.lang`).\n */\n private matchesPathCandidate(rawCandidate: unknown, location: { href: string; path: string }): boolean {\n if (typeof rawCandidate !== 'string') return false;\n const candidate = this.normalizeUrl(rawCandidate);\n if (!candidate) return false;\n if (candidate.startsWith('http://') || candidate.startsWith('https://')) {\n return location.href === candidate;\n }\n if (candidate.startsWith('/')) {\n return location.href.includes(candidate);\n }\n return location.path === candidate;\n }\n\n /** href + pathname normalizados de la ruta a evaluar, o null si no hay `window.location`. */\n private resolveCurrentLocation(pathUrl?: string): { href: string; path: string } | null {\n if (typeof window === 'undefined' || !window.location) return null;\n const href = this.normalizeUrl(pathUrl || window.location.href || '');\n const parsed = this.safeParseUrl(href);\n return { href, path: this.normalizeUrl(parsed?.pathname || window.location.pathname || '') };\n }\n\n private evaluateCooldown(def: PopupDefinition, condition: PopupTriggerCondition): boolean {\n switch (condition.answered) {\n case 'SHOWED':\n return this.hasCooldownElapsed(this.lastShown.get(def.id), condition.cooldownDays);\n case 'PARTIAL':\n case 'COMPLETED': {\n const progress = this.surveyProgress.get(def.surveyId);\n if (!progress || progress.status !== condition.answered) {\n return true;\n }\n return this.hasCooldownElapsed(progress.timestamp, condition.cooldownDays);\n }\n default:\n return true;\n }\n }\n\n private evaluateLegacyCondition(def: PopupDefinition, condition: LegacyPopupTriggerCondition): boolean {\n if (!condition.answered && this.answeredSurveys.has(def.surveyId)) {\n return false;\n }\n return this.hasCooldownElapsed(this.lastShown.get(def.id), condition.cooldownDays);\n }\n\n private hasCooldownElapsed(timestamp: number | undefined, cooldownDays?: number): boolean {\n if (!timestamp || !cooldownDays) {\n return true;\n }\n return Date.now() - timestamp >= cooldownDays * DAY_IN_MS;\n }\n\n /** Marcar encuesta como contestada externamente */\n public markSurveyAnswered(surveyId: string): void {\n this.answeredSurveys.add(surveyId);\n this.markSurveyProgress(surveyId, 'COMPLETED');\n }\n\n /** Queue an exit popup so it can render after navigation */\n public queueExitPopup(surveyId: string, delaySeconds: number, sourceUrl?: string, popupId?: string): void {\n const def = this.findPopupDefinition(surveyId, popupId);\n if (!def) {\n this.debug('No popup definition for exit trigger', { surveyId, popupId });\n return;\n }\n\n const originUrl = sourceUrl || (typeof window !== 'undefined' ? window.location.href : '');\n if (!originUrl) {\n this.debug('Exit popup skipped: missing source URL', { surveyId });\n return;\n }\n\n if (!this.shouldShow(def, originUrl)) {\n this.debug('Exit popup skipped by conditions/path', { popupId: def.id, sourceUrl: originUrl });\n return;\n }\n\n const safeDelayMs = Number.isFinite(delaySeconds) ? Math.max(0, delaySeconds * 1000) : 0;\n const deferred: DeferredExitPopup = {\n id: def.id,\n surveyId: def.surveyId,\n sourceUrl: this.normalizeUrl(originUrl),\n dueAt: Date.now() + safeDelayMs,\n };\n\n const queue = this.getDeferredExitQueue().filter((item) => !(item.id === deferred.id && item.sourceUrl === deferred.sourceUrl));\n queue.push(deferred);\n this.setDeferredExitQueue(queue);\n this.scheduleDeferredExitPopup(deferred);\n this.debug('Exit popup queued', deferred);\n }\n\n private processDeferredExitQueue(): void {\n const queue = this.getDeferredExitQueue();\n if (!queue.length) return;\n\n const pending: DeferredExitPopup[] = [];\n const now = Date.now();\n queue.forEach((item) => {\n const def = this.popupDefinitions.find((popup) => popup.id === item.id && popup.surveyId === item.surveyId);\n if (!def) {\n return;\n }\n if (item.dueAt <= now) {\n this.tryShowDeferredExitPopup(item);\n return;\n }\n pending.push(item);\n this.scheduleDeferredExitPopup(item);\n });\n this.setDeferredExitQueue(pending);\n }\n\n private scheduleDeferredExitPopup(item: DeferredExitPopup): void {\n if (typeof window === 'undefined') return;\n const delay = Math.max(0, item.dueAt - Date.now());\n const timer = window.setTimeout(() => this.tryShowDeferredExitPopup(item), delay);\n this.deferredExitTimers.push(timer);\n }\n\n private tryShowDeferredExitPopup(item: DeferredExitPopup): void {\n const def = this.popupDefinitions.find((popup) => popup.id === item.id && popup.surveyId === item.surveyId);\n if (!def) {\n this.removeDeferredExitPopup(item);\n return;\n }\n\n const currentUrl = typeof window !== 'undefined' ? this.normalizeUrl(window.location.href || '') : '';\n if (!currentUrl || currentUrl === this.normalizeUrl(item.sourceUrl)) {\n this.removeDeferredExitPopup(item);\n this.debug('Exit popup dropped because route did not change', item);\n return;\n }\n\n if (!this.shouldShow(def, undefined, true)) {\n this.removeDeferredExitPopup(item);\n this.debug('Exit popup skipped at render-time conditions', item);\n return;\n }\n\n this.showDefinition(def);\n this.removeDeferredExitPopup(item);\n }\n\n private findPopupDefinition(surveyId: string, popupId?: string): NormalizedPopupDefinition | undefined {\n if (popupId) {\n const byId = this.popupDefinitions.find((popup) => popup.id === popupId);\n if (byId) return byId;\n }\n return this.popupDefinitions.find((popup) => popup.surveyId === surveyId);\n }\n\n private removeDeferredExitPopup(item: DeferredExitPopup): void {\n const queue = this.getDeferredExitQueue().filter((entry) => !(entry.id === item.id && entry.sourceUrl === item.sourceUrl));\n this.setDeferredExitQueue(queue);\n }\n\n private getDeferredExitQueue(): DeferredExitPopup[] {\n if (typeof window === 'undefined') return [];\n try {\n const raw = window.sessionStorage.getItem(EXIT_QUEUE_STORAGE_KEY);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return parsed\n .filter((entry: unknown): entry is DeferredExitPopup => {\n if (typeof entry !== 'object' || entry === null) return false;\n const item = entry as Partial<DeferredExitPopup>;\n return typeof item.id === 'string'\n && typeof item.surveyId === 'string'\n && typeof item.sourceUrl === 'string'\n && typeof item.dueAt === 'number'\n && Number.isFinite(item.dueAt);\n })\n .map((item) => ({\n ...item,\n sourceUrl: this.normalizeUrl(item.sourceUrl),\n }));\n } catch {\n return [];\n }\n }\n\n private setDeferredExitQueue(queue: DeferredExitPopup[]): void {\n if (typeof window === 'undefined') return;\n try {\n if (!queue.length) {\n window.sessionStorage.removeItem(EXIT_QUEUE_STORAGE_KEY);\n return;\n }\n window.sessionStorage.setItem(EXIT_QUEUE_STORAGE_KEY, JSON.stringify(queue));\n } catch {\n // Ignore storage errors in host environments that block storage access.\n }\n }\n\n /** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */\n private async fetchPopupsFromServer(): Promise<unknown[] | null> {\n const apiKey = this.config?.apiKey;\n const baseUrl = this.baseUrl;\n // El backend aplica las reglas de redisplay AQUI: si este userId ya tiene un\n // SHOWED/PARTIAL/COMPLETED dentro del cooldown, no devuelve ese popup. Para que\n // cuadre, hay que preguntar por el MISMO id con el que se reporta el estado en\n // `POST /sdk/popups` — el del tracking (host si lo hay, si no el anonimo\n // persistente). Con `config.userId` a secas, la integracion que no pasa userId\n // (el caso por defecto) preguntaba sin filtro y el backend devolvia todo.\n const userId = this.tracking?.getUserId() ?? this.config?.userId;\n if (!apiKey || !baseUrl) {\n this.log('Missing apiKey or baseUrl. Skipping popups fetch.');\n return [];\n }\n const filter = userId ? { where: { userId } } : null;\n const query = filter ? `?filter=${encodeURIComponent(JSON.stringify(filter))}` : '';\n const endpoint = `${baseUrl}/sdk/${encodeURIComponent(apiKey)}/popups${query}`;\n try {\n const response = await fetch(endpoint);\n if (!response.ok) {\n this.log('Failed to fetch popups', response.status, response.statusText);\n return null;\n }\n const raw = await response.text();\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n this.log('Unexpected popups payload', parsed);\n return null;\n }\n this.debug('Fetched popups payload', parsed);\n return parsed;\n } catch (error) {\n this.log('Error fetching popups', error);\n return null;\n }\n }\n\n /** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */\n private async postContact(body: { publicKey: string; userId: string; userAttributes: ContactAttributes }): Promise<void> {\n const baseUrl = this.baseUrl;\n if (!baseUrl) return;\n const endpoint = `${baseUrl}/sdk/popups/contact`;\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n if (!response.ok) {\n this.log('Failed to post contact', response.status, response.statusText);\n return;\n }\n this.log('contact · sent attributes for user:', body.userId);\n } catch (error) {\n this.log('Error posting contact', error);\n }\n }\n\n /** Notifica eventos de popup a la API */\n private async postPopupEvent(status: POPUPSESSIONSTATUS, popupId: string, userId?: string): Promise<void> {\n const apiKey = this.config?.apiKey;\n const baseUrl = this.baseUrl;\n if (!apiKey || !baseUrl) {\n return;\n }\n const endpoint = `${baseUrl}/sdk/popups`;\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n publicKey: apiKey,\n status,\n popupId,\n userId: userId || undefined,\n }),\n });\n if (!response.ok) {\n this.log('Failed to post popup event', response.status, response.statusText);\n return;\n }\n // Contrato Fase 1 §6: el backend devuelve el sessionId; lo cacheamos para\n // inyectarlo en las respuestas de survey (lo cose por user_id).\n try {\n const data = await response.json();\n if (data && typeof data.sessionId === 'string') {\n this.tracking?.setSessionId(data.sessionId);\n this.log('tracking · session_id from backend:', data.sessionId);\n }\n } catch {\n // respuesta sin cuerpo JSON: nada que cachear\n }\n } catch (error) {\n this.log('Error posting popup event', error);\n }\n }\n\n /** Add an event listener */\n on(eventType: DeepdotsEventType, listener: EventListener): void {\n if (!this.listeners.has(eventType)) {\n this.listeners.set(eventType, new Set());\n }\n this.listeners.get(eventType)!.add(listener);\n }\n\n /** Remove an event listener */\n off(eventType: DeepdotsEventType, listener: EventListener): void {\n const listeners = this.listeners.get(eventType);\n if (listeners) {\n listeners.delete(listener);\n }\n }\n\n /** Render the popup UI */\n private renderPopup(surveyId: string, productId: string, actions?: PopupActions, style?: PopupStyle, title?: string): void {\n const userId = this.tracking?.getUserId() ?? this.config?.userId;\n const sessionId = this.tracking?.getSessionId() ?? undefined;\n const miniService = this.analytics?.getMiniService() ?? undefined;\n const analyticsFeedbackSessionId = this.analyticsFeedbackSessionId;\n this.log('tracking · survey identity →', { userId: userId ?? null, sessionId: sessionId ?? null, miniService: miniService ?? null, analyticsFeedbackSessionId: analyticsFeedbackSessionId ?? null });\n this.renderer.show(\n surveyId,\n productId,\n actions,\n (type, id, payload) => this.emitEvent(type, id, payload),\n () => this.hidePopup(),\n this.env,\n userId,\n style,\n sessionId,\n miniService,\n analyticsFeedbackSessionId,\n this.renderChrome,\n { title, showProgressBar: this.showProgressBar, surveyCss: this.surveyCss, language: this.language, cspNonce: this.cspNonce },\n );\n }\n\n /** Hide the popup */\n private hidePopup(): void {\n this.renderer.hide();\n }\n\n /** Emit an event */\n private emitEvent(type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>): void {\n const event: DeepdotsEvent = { type, surveyId, timestamp: Date.now(), data };\n this.log('Event emitted', event);\n\n const isPartialClick = type === 'popup_clicked' && data?.action === 'partial';\n if (isPartialClick) {\n this.markSurveyProgress(surveyId, 'PARTIAL');\n }\n if (type === 'survey_completed') {\n this.markSurveyAnswered(surveyId);\n }\n if (type === 'popup_shown' || type === 'survey_completed' || isPartialClick) {\n const popupIdFromData = data?.popupId as string | undefined;\n const popupId = popupIdFromData || this.surveyToPopupId.get(surveyId);\n if (popupId) {\n const userIdFromData = data?.userId as string | undefined;\n const status = type === 'popup_shown'\n ? POPUPSESSIONSTATUS.SHOWED\n : type === 'survey_completed'\n ? POPUPSESSIONSTATUS.COMPLETED\n : POPUPSESSIONSTATUS.PARTIAL;\n void this.postPopupEvent(status, popupId, userIdFromData || this.tracking?.getUserId() || this.config?.userId);\n } else {\n this.debug('No popupId available to post event', { type, surveyId });\n }\n }\n\n const listeners = this.listeners.get(type);\n if (listeners) {\n listeners.forEach((listener) => {\n try {\n listener(event);\n } catch (error) {\n if (this.logger.error) this.logger.error('Error in event listener:', error);\n else this.logger.log('Error in event listener:', error);\n }\n });\n }\n }\n\n /** Log debug messages */\n private log(...args: unknown[]): void {\n if (this.config?.debug) {\n this.logger.log('[DeepdotsPopups]', ...args);\n }\n }\n\n /** External debug method for triggers */\n public debug(...args: unknown[]): void {\n this.log(...args);\n }\n\n /** Set a custom renderer */\n public setRenderer(renderer: PopupRenderer): void {\n this.renderer = renderer;\n if (this.initialized && this.renderer.init) {\n this.renderer.init();\n }\n }\n\n private normalizeUrl(value: string): string {\n if (!value) return '';\n const withoutIndex = value.replace(/\\/index\\.html(?=($|[?#]))/i, '');\n return withoutIndex.length > 1 && withoutIndex.endsWith('/') ? withoutIndex.slice(0, -1) : withoutIndex;\n }\n\n private safeParseUrl(url: string): URL | null {\n if (!url) return null;\n try {\n return new URL(url, typeof window !== 'undefined' ? window.location.href : undefined);\n } catch {\n return null;\n }\n }\n\n private mapPopupTriggerType(type: PopupTrigger['type']): TriggerConfig['type'] | null {\n switch (type) {\n case 'time_on_page':\n return 'time';\n case 'scroll':\n return 'scroll';\n case 'exit':\n return 'exit';\n case 'click':\n return 'click';\n case 'event':\n return 'event';\n default:\n return null;\n }\n }\n\n private markSurveyProgress(surveyId: string, status: PopupTriggerConditionStatus): void {\n const current = this.surveyProgress.get(surveyId);\n if (current?.status === 'COMPLETED' && status !== 'COMPLETED') {\n return;\n }\n this.surveyProgress.set(surveyId, { status, timestamp: Date.now() });\n this.persistPopupState();\n }\n\n /**\n * Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana\n * la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias\n * pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión\n * de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las\n * entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.\n */\n private loadPopupState(storage: KeyValueStorage): void {\n const state = readPopupState(storage);\n Object.entries(state.lastShown).forEach(([popupId, timestamp]) => {\n if (timestamp > (this.lastShown.get(popupId) ?? -Infinity)) this.lastShown.set(popupId, timestamp);\n });\n Object.entries(state.progress).forEach(([surveyId, entry]) => {\n const current = this.surveyProgress.get(surveyId);\n if (current && current.timestamp >= entry.timestamp) return;\n this.surveyProgress.set(surveyId, { status: entry.status, timestamp: entry.timestamp });\n if (entry.status === 'COMPLETED') this.answeredSurveys.add(surveyId);\n });\n }\n\n /**\n * Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.\n * Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña\n * hubiese guardado desde nuestro `init()`.\n */\n private persistPopupState(): void {\n if (!this.storage) return;\n this.loadPopupState(this.storage);\n writePopupState(this.storage, {\n lastShown: Object.fromEntries(this.lastShown),\n progress: Object.fromEntries(this.surveyProgress),\n });\n }\n\n private isPopupDefinition(value: unknown): value is NormalizedPopupDefinition {\n if (typeof value !== 'object' || value === null) return false;\n const popup = value as Partial<NormalizedPopupDefinition>;\n\n return typeof popup.id === 'string'\n && typeof popup.title === 'string'\n && typeof popup.message === 'string'\n && typeof popup.surveyId === 'string'\n && typeof popup.productId === 'string'\n && Array.isArray(popup.triggers)\n && popup.triggers.length > 0\n && popup.triggers.every((trigger) => this.isPopupTrigger(trigger))\n && (!popup.cooldown || popup.cooldown.every((condition) => this.isPopupTriggerCondition(condition)));\n }\n\n private isPopupTrigger(value: unknown): value is PopupTrigger {\n if (typeof value !== 'object' || value === null) return false;\n const trigger = value as Partial<PopupTrigger>;\n return this.isPopupTriggerType(trigger.type)\n && (typeof trigger.value === 'number' || typeof trigger.value === 'string');\n }\n\n private isPopupTriggerType(value: unknown): value is PopupTrigger['type'] {\n return value === 'time_on_page'\n || value === 'scroll'\n || value === 'exit'\n || value === 'click'\n || value === 'event';\n }\n\n private isPopupTriggerCondition(value: unknown): value is PopupTriggerCondition {\n if (typeof value !== 'object' || value === null) return false;\n const condition = value as Partial<PopupTriggerCondition>;\n return POPUP_TRIGGER_CONDITION_STATUSES.includes(condition.answered as PopupTriggerConditionStatus)\n && typeof condition.cooldownDays === 'number'\n && Number.isFinite(condition.cooldownDays)\n && condition.cooldownDays >= 0;\n }\n\n private isLegacyPopupTriggerCondition(value: unknown): value is LegacyPopupTriggerCondition {\n if (typeof value !== 'object' || value === null) return false;\n const condition = value as Partial<LegacyPopupTriggerCondition>;\n if (condition.answered !== undefined && typeof condition.answered !== 'boolean') {\n return false;\n }\n if (condition.cooldownDays !== undefined) {\n return typeof condition.cooldownDays === 'number'\n && Number.isFinite(condition.cooldownDays)\n && condition.cooldownDays >= 0;\n }\n return true;\n }\n\n private normalizeTriggers(value: unknown): PopupTrigger[] {\n const items = Array.isArray(value) ? value : value ? [value] : [];\n return items\n .filter((trigger): trigger is PopupTrigger => this.isPopupTrigger(trigger))\n .map((trigger) => ({\n type: trigger.type,\n value: trigger.value,\n }));\n }\n\n private normalizeCooldown(value: unknown): PopupTriggerCondition[] {\n if (!Array.isArray(value)) return [];\n return value.filter((condition): condition is PopupTriggerCondition => this.isPopupTriggerCondition(condition));\n }\n\n private normalizeLegacyConditions(value: unknown): LegacyPopupTriggerCondition[] {\n if (!Array.isArray(value)) return [];\n return value.filter((condition): condition is LegacyPopupTriggerCondition => this.isLegacyPopupTriggerCondition(condition));\n }\n\n private normalizePopupDefinition(def: unknown): NormalizedPopupDefinition | null {\n if (typeof def !== 'object' || def === null) return null;\n const raw = def as Partial<PopupDefinition> & {\n trigger?: PopupTrigger | PopupTrigger[];\n triggers?: PopupTrigger | PopupTrigger[];\n conditions?: LegacyPopupTriggerCondition[];\n cooldown?: PopupTriggerCondition[];\n };\n\n const rawTriggers = raw.triggers ?? raw.trigger;\n const triggers = this.normalizeTriggers(rawTriggers);\n if (!triggers.length) return null;\n\n const rawTriggerList = Array.isArray(rawTriggers) ? rawTriggers : rawTriggers ? [rawTriggers] : [];\n const legacyConditions = [\n ...this.normalizeLegacyConditions(raw.conditions),\n ...rawTriggerList.flatMap((trigger) => this.normalizeLegacyConditions((trigger as { condition?: unknown }).condition)),\n ];\n const cooldown = this.normalizeCooldown(raw.cooldown);\n\n return {\n ...(raw as PopupDefinition),\n triggers,\n cooldown,\n legacyConditions,\n };\n }\n\n private validatePopupDefinitions(defs: unknown[]): NormalizedPopupDefinition[] {\n return defs\n .map((def) => this.normalizePopupDefinition(def))\n .filter((def): def is NormalizedPopupDefinition => !!def && this.isPopupDefinition(def));\n }\n}\n","/**\n * Auto-wiring de React Native: concentra TODA la configuración en el SDK para que el\n * host solo tenga que envolver su app en un `<DeepdotsProvider config={...}>`.\n *\n * No importa React ni librerías nativas: el host (o el Provider) le pasa los módulos\n * nativos que tenga instalados (MMKV, device-info, AppState). Todo es opcional y degrada\n * con elegancia (sin MMKV → in-memory; sin device-info → sin device info).\n */\nimport type { DeepdotsPopups } from '../core/deepdots-popups';\nimport type { DeepdotsInitParams } from '../types';\nimport type { KeyValueStorage } from '../tracking/tracking-manager';\nimport type { DeviceInfo } from '../analytics/device-info';\nimport type { PopupRenderer } from '../platform/renderer';\nimport type { ReactNativeErrorUtils } from '../analytics/crash-reporter';\n\n/** Forma mínima de `react-native-mmkv` (instancia MMKV). */\nexport interface MmkvLike {\n getString(key: string): string | undefined | null;\n set(key: string, value: string): void;\n delete(key: string): void;\n}\n\n/** Forma mínima de `react-native-device-info`. */\nexport interface DeviceInfoLike {\n isTablet?: () => boolean;\n getSystemVersion?: () => string;\n getDeviceId?: () => string;\n getVersion?: () => string;\n}\n\n/** Forma mínima de `AppState` de react-native. */\nexport interface AppStateLike {\n addEventListener(type: 'change', listener: (state: string) => void): { remove: () => void } | void;\n}\n\nexport interface ReactNativeSetupDeps {\n mmkv?: MmkvLike | null;\n deviceInfo?: DeviceInfoLike | null;\n appState?: AppStateLike | null;\n platform?: 'web' | 'android' | 'ios';\n renderer?: PopupRenderer;\n /** `global.ErrorUtils` de RN (para capturar errores JS no manejados). Default: `globalThis.ErrorUtils`. */\n errorUtils?: ReactNativeErrorUtils | null;\n}\n\n/** Adaptador KeyValueStorage (síncrono) sobre una instancia de MMKV. */\nexport function mmkvStorage(mmkv: MmkvLike): KeyValueStorage {\n return {\n getItem: (k) => mmkv.getString(k) ?? null,\n setItem: (k, v) => mmkv.set(k, v),\n removeItem: (k) => mmkv.delete(k),\n };\n}\n\n/** Recoge device info desde react-native-device-info (Technology #11–13). */\nexport function collectRnDevice(d: DeviceInfoLike): DeviceInfo {\n return {\n device_type: d.isTablet?.() ? 'tablet' : 'mobile',\n os_version: d.getSystemVersion?.(),\n device_model: d.getDeviceId?.(),\n app_version: d.getVersion?.(),\n };\n}\n\n/**\n * Configura el SDK para React Native en una sola llamada: storage persistente (MMKV),\n * device info, platform, init y lifecycle (AppState → onForeground/onBackground).\n * Devuelve una función de limpieza (quita el listener de AppState).\n */\nexport function setupReactNative(\n sdk: DeepdotsPopups,\n config: DeepdotsInitParams,\n deps: ReactNativeSetupDeps = {},\n): () => void {\n if (deps.renderer) sdk.setRenderer(deps.renderer);\n\n const storage = deps.mmkv ? mmkvStorage(deps.mmkv) : config.storage;\n const device = deps.deviceInfo ? collectRnDevice(deps.deviceInfo) : config.device;\n const platform = deps.platform ?? config.platform;\n\n sdk.init({ ...config, storage, device, platform });\n\n // Captura de errores JS no manejados en RN (no hay `window`): usa el ErrorUtils inyectado\n // o el global del runtime RN. Degrada si no existe.\n const errorUtils =\n deps.errorUtils ?? (globalThis as { ErrorUtils?: ReactNativeErrorUtils }).ErrorUtils;\n if (errorUtils && typeof errorUtils.setGlobalHandler === 'function') {\n sdk.installReactNativeCrashHandler(errorUtils);\n }\n\n let subscription: { remove: () => void } | void;\n if (deps.appState?.addEventListener) {\n subscription = deps.appState.addEventListener('change', (state) => {\n if (state === 'active') sdk.onForeground();\n // Solo 'background' es fin de sesión. En iOS 'inactive' es transitorio (llamada\n // entrante, app switcher) y cerrar sesión ahí partiría la sesión en dos: se envía\n // lo acumulado, por si el SO mata la app, pero sin cerrar el registro.\n else if (state === 'background') sdk.onBackground();\n else sdk.flushAnalytics();\n });\n }\n\n return () => {\n if (subscription && typeof subscription.remove === 'function') subscription.remove();\n };\n}\n"],"mappings":"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","names":["pending"]}
1
+ {"version":3,"sources":["../src/types/index.ts","../src/triggers/index.ts","../src/config/env.ts","../src/ui/surveyHtml.ts","../src/platform/react-native-renderer.ts","../src/ui/preload.ts","../src/platform/renderer.ts","../src/analytics/analytics-manager.ts","../src/analytics/feedback-payload.ts","../src/analytics/suspended-session.ts","../src/tracking/navigation-observer.ts","../src/analytics/device-info.ts","../src/analytics/geo-info.ts","../src/analytics/language.ts","../src/analytics/engagement-tracker.ts","../src/contact/contact-manager.ts","../src/analytics/crash-reporter.ts","../src/core/popup-state-store.ts","../src/analytics/messaging.ts","../src/core/deepdots-popups.ts","../src/react-native/setup.ts"],"sourcesContent":["/**\n * Configuration options for initializing the DeepdotsPopups SDK\n */\nimport type { KeyValueStorage } from '../tracking/tracking-manager';\nimport type { DeviceInfo } from '../analytics/device-info';\nimport type { AnalyticsKeys } from '../analytics/feedback-payload';\n\n/**\n * Custom logger to receive the SDK's debug output instead of the console.\n * Only `log` is required; `warn`/`error`/`info` fall back to `log` when omitted.\n * Lets the host pipe debug output to a file, Firebase, etc. `console` satisfies this shape.\n */\nexport interface DeepdotsLogger {\n log: (...args: unknown[]) => void;\n warn?: (...args: unknown[]) => void;\n error?: (...args: unknown[]) => void;\n info?: (...args: unknown[]) => void;\n}\n\n/**\n * A session of the analytics channel as the Deepdots API sees it. Every session becomes ONE\n * feedback, and `sessionId` is what the API stores as `sdkSessionId` on it: the value to look\n * that feedback up with (`GET /feedbacks?filter={\"where\":{\"sdkSessionId\":\"<sessionId>\"}}`).\n * A session that closes again keeps that one feedback: the API updates it.\n * - `open`: the API acknowledged the session's first batch; its events keep accumulating.\n * - `closed`: the API acknowledged the session's closing batch (`completed: true`); the\n * feedback is created from it within seconds.\n */\nexport interface FeedbackSession {\n sessionId: string;\n status: 'open' | 'closed';\n}\n\nexport interface DeepdotsInitParams {\n /** API key for authentication */\n apiKey?: string;\n /** Node environment: 'development' or 'production' */\n nodeEnv?: 'development' | 'production';\n /** Enable debug logging */\n debug?: boolean;\n /** Custom logger for debug output. When set (and `debug` is true), the SDK routes its logs here instead of `console`. Default: `console`. */\n logger?: DeepdotsLogger;\n /** Optional user id to send with popup events */\n userId?: string;\n /** Starts tracking enabled (default) or disabled (e.g. until consent is granted). Equivalent to calling `setTrackingEnabled` after init. */\n trackingEnabled?: boolean;\n /** Host app version (for analytics device info / Technology #11). */\n appVersion?: string;\n /** Injectable persistent storage (RN: adapter over AsyncStorage). If missing, uses localStorage/in-memory. */\n storage?: KeyValueStorage;\n /** Platform used in the analytics envelope. Default is 'web'; in RN pass 'android'/'ios'. */\n platform?: 'web' | 'android' | 'ios';\n /**\n * BCP-47 language tag (e.g. 'es-ES') used for the analytics context AND for popup\n * language segmentation (`segments.lang`). If omitted, the SDK auto-detects:\n * `navigator.language` on web, or the `Intl` locale as a fallback (works in\n * React Native with Hermes, where `navigator.language` is absent). If no source\n * resolves a language, popups with `segments.lang` cannot be filtered and are shown.\n */\n language?: string;\n /** Injectable device info (RN: from react-native-device-info). If missing, it is derived from the browser. */\n device?: DeviceInfo;\n /**\n * Analytics channel keys (`POST /sdk/feedback`). If provided, analytics is SENT\n * to the configured integration; if missing, it stays in dry-run mode: nothing is sent, and the payload is only\n * logged when `debug` is true.\n */\n analytics?: AnalyticsKeys;\n /**\n * Called when the analytics channel opens or closes a session (needs `analytics`), with the\n * id the Deepdots API stores as `sdkSessionId` on the feedback the session becomes: send it\n * to your backend to attach data to that feedback later. A session whose only batch is the\n * closing one reports just `closed`. `closed` does not come when the page unloads before the\n * closing request returns, for a session dropped after 30 min hidden, nor when the closing\n * request fails (the API closes those itself, later): their id already came with `open`. Key\n * by `sessionId`, not by order: the next session's `open` can arrive before the previous\n * one's `closed`. Same value as `getFeedbackSessionId()` while the session is open.\n */\n onFeedbackSession?: (session: FeedbackSession) => void;\n /**\n * IP geolocation (country/city) added to analytics. Default `true`. The lookup calls\n * third-party services (ipapi.co, then ipwho.is, then ipinfo.io as fallbacks, 3 s timeout\n * each) and only runs when `analytics` is configured, tracking is enabled and the 30-day\n * cache is missing or expired. Set `false` to never call them nor attach country/city.\n */\n geolocation?: boolean;\n /**\n * Internal user attributes known only by the host (language, age, plan, ...), sent\n * to the backend Contact for segmentation/targeting. Requires `userId`. Equivalent to calling\n * `setContactAttributes` after init (sends only when values changed from the last sync).\n */\n contactAttributes?: Record<string, string | number | boolean>;\n /**\n * Whether the SDK renders the popup \"modal\" (card + backdrop + positioning) around the survey.\n * Default `true`. Only affects React Native (the survey WebView HTML); the web DOM popup is\n * unaffected. With `false` the survey is delivered transparent and edge-to-edge so the host\n * mounts its own Modal/container and controls the styling — avoids the \"double modal\" when the\n * host already wraps the WebView. The survey stays fully functional (bridge, form, nav buttons,\n * close). Ver `ReactNativePopupRenderer`.\n */\n renderChrome?: boolean;\n /**\n * Barra de progreso (\"Question X of Y\" + barra) en la cabecera del popup. Si se omite,\n * manda el `showProgressBar` que la plataforma configura en el estilo del survey; `true` o\n * `false` lo fuerzan desde el host. Solo se pinta con más de una página, fuera de la\n * pantalla de inicio y antes de completar, igual que en la web de surveys.\n */\n showProgressBar?: boolean;\n /**\n * CSS propio del host, inyectado como último bloque de estilos del popup: gana en cascada\n * sobre el del SDK y sobre el de `@magicfeedback/native` sin tener que tocar ninguno. Es la\n * vía para reestilar el área de preguntas (enunciados, opciones, escalas) por integración,\n * en vez de cambiar los defaults que comparten todos los clientes.\n */\n surveyCss?: string;\n /**\n * Nonce de la Content-Security-Policy de la página, para hosts con una CSP estricta\n * (`style-src 'nonce-…'` sin `'unsafe-inline'`). El popup lo pone en cada `<style>` que\n * inyecta en el `<head>` (hoja del survey, spinner, responsive, `surveyCss`, `@font-face`);\n * sin él el navegador las bloquea y el survey sale sin estilos. Debe ser el MISMO nonce de\n * la cabecera CSP de esa respuesta (en Angular, el token `CSP_NONCE`). Solo web: en React\n * Native el survey vive en su propio WebView y no le afecta la CSP del host.\n */\n cspNonce?: string;\n\n}\nexport interface DeepdotsConfig {\n /** API key for authentication */\n apiKey?: string;\n /** Enable debug logging */\n debug?: boolean;\n /** Optional user id to send with popup events */\n userId?: string;\n}\n\n/**\n * Options for configuring survey triggers\n */\nexport interface TriggerConfig {\n /** Trigger type: 'time' (delay), 'scroll' (scroll percentage), 'exit' (route exit), 'click' (element id), 'event' (host event) */\n type: 'time' | 'scroll' | 'exit' | 'click' | 'event';\n /** Value for the trigger (milliseconds for time, percentage for scroll, seconds for exit, element id for click, event name for event) */\n value?: number | string;\n /** Survey ID to show when triggered */\n surveyId: string;\n /** Optional popup definition id (used to disambiguate repeated survey ids) */\n popupId?: string;\n}\n\n/**\n * Event types emitted by the SDK\n */\nexport type DeepdotsEventType =\n | 'popup_shown'\n | 'popup_clicked'\n | 'survey_completed';\n\n/**\n * Event payload structure\n */\nexport interface DeepdotsEvent {\n type: DeepdotsEventType;\n surveyId: string;\n timestamp: number;\n data?: Record<string, unknown>;\n}\n\n/**\n * Event listener callback\n */\nexport type EventListener = (event: DeepdotsEvent) => void;\n\n/**\n * Motivo del `deepdots_session_end` (último lote de la sesión, enviado con `completed:true`):\n * - `page_hide`: la página web se cierra o se navega fuera (`pagehide`).\n * - `background`: la app pasa a background (RN/nativo). Única señal disponible en móvil:\n * el kill de la app por el usuario o el SO NO produce ningún callback.\n * - `user_change`: `setUserId()` o un `init()` con otro `userId` (login/logout del host).\n * - `tracking_disabled`: `setTrackingEnabled(false)` (consentimiento revocado).\n * - `manual`: `endSession()` llamado por el host.\n * - `idle_timeout`: la pestaña vuelve a verse tras más de 30 min oculta (web). La sesión\n * anterior se cierra y se abre otra, como hace GA con su timeout de sesión.\n */\nexport type SessionEndReason =\n | 'page_hide'\n | 'background'\n | 'user_change'\n | 'tracking_disabled'\n | 'manual'\n | 'idle_timeout';\n\n/**\n * Motivo del `deepdots_session_start` (param `reason`): por qué EMPIEZA la sesión. Permite a\n * analytics separar una carga/arranque real de una vuelta desde background (\"Started here\").\n * Mismos valores que el SDK nativo donde el significado coincide:\n * - `cold_start`: `init()` (carga de la página / arranque de la app RN) sin sesión suspendida\n * que reanudar. Una recarga en menos de 1 min no emite `session_start` sino\n * `deepdots_session_resume`.\n * - `foreground`: `onForeground()` (RN, AppState 'active') tras un cierre (`onBackground()`\n * o `endSession()`). Con la sesión abierta no emite nada.\n * - `tracking_enabled`: `setTrackingEnabled(true)` (consentimiento concedido).\n * - `user_change`: `setUserId()` o un `init()` con otro `userId`, tras cerrar la del anterior.\n * - `idle_timeout`: la pestaña vuelve a verse tras más de 30 min oculta (web): la sesión\n * anterior se cerró (o se descartó) y empieza otra.\n * - `bfcache_restore`: la página vuelve de la bfcache (botón atrás, `pageshow` con\n * `persisted`) fuera de la ventana de reanudación (web).\n */\nexport type SessionStartReason =\n | 'cold_start'\n | 'foreground'\n | 'tracking_enabled'\n | 'user_change'\n | 'idle_timeout'\n | 'bfcache_restore';\n\n/** Trigger type used by remote popup definitions */\nexport type PopupTriggerType = 'time_on_page' | 'scroll' | 'exit' | 'click' | 'event';\n\nexport type PopupTriggerConditionStatus = 'SHOWED' | 'PARTIAL' | 'COMPLETED';\n\nexport const POPUP_TRIGGER_CONDITION_STATUSES: PopupTriggerConditionStatus[] = [\n 'SHOWED',\n 'PARTIAL',\n 'COMPLETED',\n];\n\n/** Extra condition used to activate the popup */\nexport interface PopupTriggerCondition {\n answered: PopupTriggerConditionStatus; // user progress status in the survey\n cooldownDays: number; // cooldown days before showing again\n}\n\n/** Trigger attached to the popup definition */\nexport interface PopupTrigger {\n type: PopupTriggerType;\n value: number | string; // seconds on page, scroll percentage, click id, or event name\n}\n\n/** Accept action (open survey) */\nexport interface PopupActionAccept {\n label: string;\n surveyId: string; // survey id to launch\n}\n\n/** Decline action */\nexport interface PopupActionDecline {\n label: string;\n cooldownDays?: number; // optional: do not show again until X days pass\n}\n\n/** Start Surveys Action */\nexport interface PopupActionStart {\n label: string;\n}\n\n/** Complete action (accept and auto-complete survey) */\n// Requires the survey to support auto-completion via parameters.\nexport interface PopupActionComplete {\n label: string;\n surveyId: string; // survey id to launch\n autoCompleteParams: Record<string, unknown>; // parameters used for auto-completion\n cooldownDays?: number;\n}\n\n/** Set of actions available in the popup */\nexport interface PopupActions {\n accept?: PopupActionAccept;\n decline?: PopupActionDecline;\n complete?: PopupActionComplete;\n start?: PopupActionStart;\n back?: PopupActionDecline;\n}\n\n/** Fuente personalizable del popup + survey (viene de la API por popup) */\nexport interface PopupFont {\n /** Nombre de la familia. Ej: \"Inter\" (nombre limpio, NO un stack). */\n family: string;\n /** Opcional: URL a un archivo de fuente (woff2/ttf/otf) para armar un @font-face. */\n url?: string;\n}\n\n/** Configurable popup styles */\nexport interface PopupStyle {\n theme: 'light' | 'dark';\n position: 'bottom' | 'bottom-right' | 'bottom-left' | 'top' | 'top-right' | 'top-left' | 'center';\n /** @deprecated Not used by the SDK; kept only for API compatibility */\n imageUrl?: string | null;\n /** Fuente personalizada. Si falta, se mantiene el comportamiento actual. */\n font?: PopupFont;\n}\n\n/** Segments/targeting rules for showing the popup */\nexport interface PopupSegments {\n lang?: string[]; // allowed languages\n path?: string[]; // app routes where it can be shown\n /**\n * App routes where it must NOT be shown. Wins over `path`: a route listed here\n * blocks the popup even if `path` matches it (so `path: ['/']` +\n * `excludedPaths: ['/cart']` = everywhere but the cart).\n * Same matching rules as `path`: an absolute URL matches the full href, an entry\n * starting with `/` matches any href containing it (this is what covers hash\n * routes like `/#/cart`), anything else matches the pathname exactly.\n */\n excludedPaths?: string[];\n [key: string]: unknown; // possible future extension\n}\n\n/** Full popup definition */\nexport interface PopupDefinition {\n id: string;\n title: string;\n message: string; // rendered safe HTML (recommended to sanitize upstream)\n triggers: PopupTrigger[];\n cooldown?: PopupTriggerCondition[];\n actions?: PopupActions;\n surveyId: string;\n productId: string;\n style?: PopupStyle;\n segments?: PopupSegments;\n}\n\nexport interface FormStyle\n{\n id: string;\n logo: string;\n title: string;\n companyName: string;\n backgroundColor: string;\n boxBackgroundColor: string;\n buttonOnTopColor: string;\n buttonPrimaryColor: string\n buttonSecondaryColor: string;\n loadingBarColor: string;\n favIcon: string;\n startMessage: string;\n successMessage: string;\n addLogoInMsg: boolean;\n privacyPolicyUrl: string;\n helpUrl: string;\n integrationId: string;\n redirectLink: string;\n redirectError: string;\n redirectTime: number;\n integrationThemeId: string;\n contentAlign: 'top' | 'center';\n logoSize: 'small' | 'medium' | 'large';\n logoPosition: 'left' | 'right' | 'center';\n showProgressBar: boolean;\n showProgressUnit: boolean;\n progressUnit: 'percentage' | 'fraction';\n}\n\n\nexport interface FormData {\n id: string;\n name: string;\n description: string;\n type: string;\n identity: string;\n status: string;\n createdAt: Date;\n updatedAt: Date;\n externalId?: string | null;\n companyId: string;\n productId: string;\n userId: string;\n /**\n * Idiomas configurados en la integración del survey. `@magicfeedback/native` localiza\n * preguntas y placeholders con el primero, y el popup traduce su chrome con el mismo.\n */\n lang?: string[];\n style: FormStyle\n}\n\nexport enum POPUPSESSIONSTATUS {\n NONE = \"NONE\",\n SHOWED = \"SHOWED\",\n PARTIAL = \"PARTIAL\",\n COMPLETED = \"COMPLETED\",\n DEPRECATED = \"DEPRECATED\",\n}\n","import type { TriggerConfig } from '../types';\nimport type { DeepdotsPopups } from '../core/deepdots-popups';\n\n/** `TriggerConfig.value` también acepta string (id/nombre para click/event); para time/scroll se necesita un número. */\nfunction toNumericValue(value: number | string | undefined, fallback: number): number {\n const n = Number(value);\n return Number.isFinite(n) ? n : fallback;\n}\n\nexport function setupTrigger(popups: DeepdotsPopups, trigger: TriggerConfig): void {\n switch (trigger.type) {\n case 'time':\n setupTimeTrigger(popups, trigger);\n break;\n case 'scroll':\n setupScrollTrigger(popups, trigger);\n break;\n case 'exit':\n setupExitTrigger(popups, trigger);\n break;\n case 'event':\n setupEventTrigger(popups, trigger);\n break;\n case 'click':\n setupClickTrigger(popups, trigger);\n break;\n }\n}\n\nfunction setupTimeTrigger(popups: DeepdotsPopups, trigger: TriggerConfig): void {\n const delay = toNumericValue(trigger.value, 5000);\n setTimeout(() => {\n popups.triggerSurvey(trigger.surveyId, trigger.popupId);\n }, delay);\n popups.debug(`Time trigger set for ${delay}ms`);\n}\n\nfunction setupScrollTrigger(popups: DeepdotsPopups, trigger: TriggerConfig): void {\n const threshold = toNumericValue(trigger.value, 50);\n const handler = () => {\n const scrollPercentage = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;\n if (scrollPercentage >= threshold) {\n popups.triggerSurvey(trigger.surveyId, trigger.popupId);\n window.removeEventListener('scroll', handler);\n }\n };\n window.addEventListener('scroll', handler);\n popups.debug(`Scroll trigger set for ${threshold}%`);\n}\n\nfunction setupExitTrigger(popups: DeepdotsPopups, trigger: TriggerConfig): void {\n if (typeof window === 'undefined') return;\n\n const delaySeconds = parseExitDelaySeconds(trigger.value);\n const historyRef = window.history;\n const originalPushState = historyRef.pushState.bind(historyRef);\n const originalReplaceState = historyRef.replaceState.bind(historyRef);\n\n let lastUrl = normalizeUrl(window.location.href);\n let lastQueuedKey = '';\n let lastQueuedAt = 0;\n\n const queueLeave = (fromRaw: string, toRaw: string) => {\n const from = normalizeUrl(fromRaw);\n const to = normalizeUrl(toRaw);\n if (!from || !to || from === to) return;\n\n const key = `${from}=>${to}`;\n const now = Date.now();\n if (key === lastQueuedKey && now - lastQueuedAt < 150) return;\n lastQueuedKey = key;\n lastQueuedAt = now;\n popups.queueExitPopup(trigger.surveyId, delaySeconds, from, trigger.popupId);\n };\n\n const handleNavigation = (to: string) => {\n const from = lastUrl;\n queueLeave(from, to);\n lastUrl = normalizeUrl(to);\n };\n\n const handleDocumentClick = (event: MouseEvent) => {\n if (event.defaultPrevented) return;\n if (event.button !== 0) return;\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;\n\n const anchor = (event.target as Element | null)?.closest?.('a');\n if (!(anchor instanceof HTMLAnchorElement)) return;\n\n const href = anchor.getAttribute('href');\n if (!href || href.startsWith('javascript:') || href.startsWith('mailto:') || href.startsWith('tel:')) {\n return;\n }\n const target = anchor.getAttribute('target');\n if (target && target !== '_self') return;\n\n let destination: URL;\n try {\n destination = new URL(href, window.location.href);\n } catch {\n return;\n }\n\n if (destination.origin !== window.location.origin) return;\n queueLeave(window.location.href, destination.href);\n };\n\n const handleHashChange = (event: HashChangeEvent) => {\n const to = event.newURL || window.location.href;\n const from = event.oldURL || lastUrl;\n queueLeave(from, to);\n lastUrl = normalizeUrl(to);\n };\n\n const handlePopState = () => {\n handleNavigation(window.location.href);\n };\n\n historyRef.pushState = function (data: any, unused: string, url?: string | URL | null) {\n const from = window.location.href;\n const result = originalPushState(data, unused, url as any);\n queueLeave(from, window.location.href);\n lastUrl = normalizeUrl(window.location.href);\n return result;\n };\n\n historyRef.replaceState = function (data: any, unused: string, url?: string | URL | null) {\n const from = window.location.href;\n const result = originalReplaceState(data, unused, url as any);\n queueLeave(from, window.location.href);\n lastUrl = normalizeUrl(window.location.href);\n return result;\n };\n\n document.addEventListener('click', handleDocumentClick, true);\n window.addEventListener('hashchange', handleHashChange);\n window.addEventListener('popstate', handlePopState);\n popups.debug('Exit trigger set (deferred to next route)', {\n surveyId: trigger.surveyId,\n delaySeconds,\n });\n}\n\nfunction setupEventTrigger(popups: DeepdotsPopups, trigger: TriggerConfig): void {\n // Event triggers are manually fired by host application using popups.triggerEvent(name).\n popups.debug('Event trigger registered', {\n popupId: trigger.popupId,\n surveyId: trigger.surveyId,\n eventName: String(trigger.value || ''),\n });\n}\n\nfunction parseExitDelaySeconds(value?: number | string): number {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return Math.max(0, value);\n }\n if (typeof value === 'string' && value.trim()) {\n const parsed = Number(value);\n if (Number.isFinite(parsed)) return Math.max(0, parsed);\n }\n return 0;\n}\n\nfunction normalizeUrl(value: string): string {\n if (!value) return '';\n const withoutIndex = value.replace(/\\/index\\.html(?=($|[?#]))/i, '');\n return withoutIndex.length > 1 && withoutIndex.endsWith('/') ? withoutIndex.slice(0, -1) : withoutIndex;\n}\n\nfunction setupClickTrigger(popups: DeepdotsPopups, trigger: TriggerConfig): void {\n const targetId = typeof trigger.value === 'string' ? trigger.value : '';\n if (!targetId) {\n popups.debug('Click trigger missing element id', trigger);\n return;\n }\n\n const attach = () => {\n const el = document.getElementById(targetId);\n if (!el) {\n popups.debug('Click trigger target not found', targetId);\n return false;\n }\n const handler = (e: MouseEvent) => {\n if (e.defaultPrevented) return;\n popups.triggerSurvey(trigger.surveyId, trigger.popupId);\n el.removeEventListener('click', handler);\n };\n el.addEventListener('click', handler);\n popups.debug(`Click trigger set for #${targetId}`);\n return true;\n };\n\n if (typeof document === 'undefined') return;\n if (attach()) return;\n window.addEventListener('DOMContentLoaded', () => attach(), { once: true });\n}\n","import {DeepdotsInitParams} from '../types';\n\n/** Variables específicas por entorno derivadas de nodeEnv */\nexport interface EnvironmentVariables {\n apiBaseUrl: string;\n}\n\nconst ENVIRONMENT_MAP: Record<\"development\" | \"production\", EnvironmentVariables> = {\n development: {\n apiBaseUrl: 'https://api-dev.deepdots.com',\n },\n production: {\n apiBaseUrl: 'https://api.deepdots.com',\n },\n};\n\n/** Resuelve las variables para el entorno indicado */\nexport function resolveEnvironment(nodeEnv: DeepdotsInitParams['nodeEnv']): EnvironmentVariables {\n return ENVIRONMENT_MAP[nodeEnv || 'production'];\n}\n\n","import type { IdentityAnswer } from '../tracking/tracking-manager';\nimport { buildFontFaceCss, buildFontFamilyValue } from './font';\nimport type { PopupActions, PopupFont } from '../types';\nimport { LABELS, RESOLVE_LOCALE_JS, RTL_LOCALES, directionFor, getLabels, resolveActionLabels } from '../i18n/labels';\nimport { REVEAL_TIMEOUT_MS } from './reveal';\nimport { PRIMARY_COLOR_JS } from './surveyPalette';\nimport { BUSY_JS } from './busy';\nimport magicfeedbackCss from '../assets/style.css';\n\n/**\n * Apertura diferida, versión que corre DENTRO del WebView: espejo ES5 de `revealWhenPainted`\n * (`reveal.ts`). El popup se monta invisible y se enseña cuando el survey está pintado y sus\n * imágenes han llegado, en vez de enseñar el spinner girando; `timeoutMs` es el techo para lo\n * que no llegue nunca. En RN esto pesa más que en web, porque a la espera del survey se le suma\n * el arranque del WebView y la descarga del bundle del CDN.\n *\n * Va como texto (y no como import) porque el WebView no comparte módulos con el bundle del SDK.\n * `surveyHtml.reveal.test.ts` lo evalúa para probar el comportamiento, no solo el string.\n */\nexport const REVEAL_JS = `\nfunction ddCreateReveal(popup, onReveal, timeoutMs){\n var revealed=false, timer=null;\n function reveal(){\n if(revealed){ return; }\n revealed=true;\n if(timer){ clearTimeout(timer); timer=null; }\n onReveal();\n }\n function revealWhenPainted(){\n if(revealed){ return; }\n var imgs=popup.querySelectorAll('img'), pending=[];\n for(var i=0;i<imgs.length;i++){ if(!imgs[i].complete){ pending.push(imgs[i]); } }\n if(!pending.length){ reveal(); return; }\n var left=pending.length;\n function onSettled(){ left--; if(left<=0){ reveal(); } }\n for(var j=0;j<pending.length;j++){\n pending[j].addEventListener('load', onSettled);\n pending[j].addEventListener('error', onSettled);\n }\n }\n timer=setTimeout(reveal, timeoutMs);\n return { reveal: reveal, whenPainted: revealWhenPainted };\n}`;\n\n/**\n * Construye un HTML autocontenido que renderiza el popup (chrome + survey de\n * `@magicfeedback/native`) dentro de un WebView (React Native). Es el equivalente RN de\n * `renderPopup.ts` (DOM real, solo navegador): mismo header con botón cerrar, spinner,\n * footer con back/start/send, gate \"Start survey\" y colores/logo desde `formData.style`.\n * Carga el bundle del survey desde CDN, crea el form con la identidad inyectada\n * (`profile`/`metadata`) y reenvía los eventos al host vía el puente de\n * react-native-webview: `window.ReactNativeWebView.postMessage`.\n *\n * Mensajes emitidos (JSON `{name, payload}`): `loaded`, `ready`, `before_submit`,\n * `after_submit`, `survey_completed`, `back`, `popup_close`, y `error:load` / `error:init` /\n * `error:exception` (con `{message}`) cuando el survey no ha podido cargar.\n */\nexport interface BuildSurveyHtmlOptions {\n surveyId: string;\n productId: string;\n env?: string; // 'production' | 'development'\n profile?: IdentityAnswer[];\n metadata?: IdentityAnswer[];\n version?: string; // versión de @magicfeedback/native\n font?: PopupFont; // fuente personalizada (family + url opcional)\n theme?: 'light' | 'dark';\n position?: 'bottom' | 'bottom-right' | 'bottom-left' | 'top' | 'top-right' | 'top-left' | 'center';\n actions?: PopupActions;\n /**\n * Renderiza el \"modal\" del popup (tarjeta con fondo/sombra/bordes + overlay + posicionamiento).\n * Default `true`. Con `false` el HTML es transparente y llena el contenedor: el host monta su\n * propio Modal/tarjeta y controla el estilo. El survey sigue siendo funcional (bridge, form,\n * botones y botón de cerrar). Pensado para React Native cuando el host gestiona el contenedor.\n */\n chrome?: boolean;\n /**\n * Título de la cabecera, a la izquierda del botón de cerrar. Es el `title` de la definición\n * del popup (`GET /sdk/{publicKey}/popups`), así que cambia de un popup a otro. Vacío es un\n * valor legítimo: la cabecera se queda solo con la X. NO cae al título del survey.\n */\n title?: string;\n /**\n * Barra de progreso (\"Question X of Y\" + barra) bajo la cabecera. `undefined` respeta el\n * `showProgressBar` que configure la plataforma en el estilo del survey; `true`/`false` lo\n * fuerzan desde el host. Solo se pinta con más de una página y fuera de la pantalla de inicio.\n */\n showProgressBar?: boolean;\n /**\n * CSS del host, inyectado como último bloque de estilos: gana en cascada sobre el del SDK\n * y sobre el de `@magicfeedback/native` sin tener que tocar ninguno de los dos. Es la vía\n * para reestilar el área de preguntas (enunciados, opciones, escalas) por integración.\n */\n surveyCss?: string;\n /**\n * Idioma con el que arranca el chrome (botones, progreso, aria-labels, errores). Es el\n * idioma resuelto en `init()`. Dentro del WebView lo pisa el idioma del survey\n * (`formData.lang[0]`), y ambos los pisa `actions.*.label` de la API.\n */\n language?: string;\n}\n\ninterface PositionRule {\n justifyContent: string;\n alignItems: string;\n padding: string;\n background: string;\n}\n\nconst POSITION_MAP: Record<string, PositionRule> = {\n center: { justifyContent: 'center', alignItems: 'center', padding: '0', background: 'rgba(0,0,0,0.5)' },\n bottom: { justifyContent: 'center', alignItems: 'flex-end', padding: '16px', background: 'transparent' },\n 'bottom-right': { justifyContent: 'flex-end', alignItems: 'flex-end', padding: '16px', background: 'transparent' },\n 'bottom-left': { justifyContent: 'flex-start', alignItems: 'flex-end', padding: '16px', background: 'transparent' },\n top: { justifyContent: 'center', alignItems: 'flex-start', padding: '16px', background: 'transparent' },\n 'top-right': { justifyContent: 'flex-end', alignItems: 'flex-start', padding: '16px', background: 'transparent' },\n 'top-left': { justifyContent: 'flex-start', alignItems: 'flex-start', padding: '16px', background: 'transparent' },\n};\n\n/**\n * Serializa un valor para incrustarlo dentro de un `<script>`. `JSON.stringify` no escapa `<`,\n * así que un `</script>` dentro de un texto de la API (título, etiqueta de botón, metadata)\n * cerraría el bloque y el resto se interpretaría como HTML. Escapa también los separadores de\n * línea U+2028/U+2029, que son JSON válido pero rompen un literal de JavaScript.\n */\nfunction jsonForScript(value: unknown): string {\n return JSON.stringify(value ?? null)\n .replace(/</g, '\\\\u003C')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n}\n\n/** Devuelve el label de la API solo si trae contenido; en blanco cuenta como \"no configurado\". */\nfunction trimmedOrNull(value: string | undefined): string | null {\n const trimmed = typeof value === 'string' ? value.trim() : '';\n return trimmed ? value as string : null;\n}\n\nexport function buildSurveyHtml(opts: BuildSurveyHtmlOptions): string {\n const env = opts.env === 'development' ? 'dev' : 'prod';\n // Misma versión que la dep npm del bundle web, para que las dos rutas pinten igual.\n const version = opts.version ?? '2.2.33';\n const cdn = `https://cdn.jsdelivr.net/npm/@magicfeedback/native@${version}/dist/magicfeedback-sdk.browser.js`;\n const sid = jsonForScript(opts.surveyId);\n const pid = jsonForScript(opts.productId);\n const profileJson = jsonForScript(opts.profile ?? []);\n const metaJson = jsonForScript(opts.metadata ?? []);\n const fontFaceCss = opts.font ? buildFontFaceCss(opts.font.family, opts.font.url) : '';\n const fontFamilyValue = opts.font ? buildFontFamilyValue(opts.font.family) : '-apple-system,system-ui,sans-serif';\n const popupClass = opts.font?.family ? 'deepdots-popup deepdots-has-font' : 'deepdots-popup';\n\n const isDark = opts.theme === 'dark';\n const popupBg = isDark ? '#1e1e1e' : '#fff';\n const colorScheme = isDark ? 'dark' : 'light';\n const textPrimary = isDark ? '#f0f0f0' : '#111';\n const textMuted = isDark ? '#9ca3af' : '#6b7280';\n const trackBg = isDark ? '#3f3f46' : '#e5e7eb';\n const pos = POSITION_MAP[opts.position ?? 'center'] ?? POSITION_MAP.center;\n\n // chrome:false → el host envuelve el survey en su propio Modal/tarjeta. El HTML se vuelve\n // transparente y llena el contenedor (sin fondo/sombra/borde/overlay/posicionamiento propios);\n // así se evita el \"doble modal\" cuando el host ya monta el suyo. Survey funcional intacto.\n const chrome = opts.chrome !== false;\n const bodyBg = chrome ? pos.background : 'transparent';\n const bodyPadding = chrome ? pos.padding : '0';\n const bodyDisplay = chrome ? 'flex' : 'block';\n const cardBg = chrome ? popupBg : 'transparent';\n const cardShadow = chrome ? '0 4px 6px rgba(0,0,0,0.1)' : 'none';\n const cardRadius = chrome ? '8px' : '0';\n const cardMaxWidth = chrome ? '600px' : 'none';\n const cardWidth = chrome ? '90%' : '100%';\n const cardMinHeight = chrome ? '200px' : '0';\n // Con chrome el alto lo marca el contenido (hasta 90vh); sin chrome llena el contenedor del\n // host. En ambos casos hace falta un alto acotado para que el footer pueda quedar fijo abajo.\n const cardHeight = chrome ? 'auto' : '100%';\n const cardMaxHeight = chrome ? '90vh' : '100%';\n // En móvil la tarjeta se estrecha y se separa de los bordes de la ventana... salvo con\n // chrome:false, donde el contenedor es del host: ahí llena el ancho y NO añade los insets\n // de safe-area (el host ya los aplica; sumarlos otra vez dejaba un hueco muerto bajo el footer).\n const mobileCard = chrome\n ? `.deepdots-popup{width:calc(100% - 24px);max-width:calc(100% - 24px);border-radius:12px;padding:calc(16px + env(safe-area-inset-top)) 16px calc(16px + env(safe-area-inset-bottom)) 16px}`\n : `.deepdots-popup{width:100%;max-width:100%;border-radius:0;padding:16px}`;\n\n // El título viaja como literal JSON y se aplica con textContent: viene de la API y no debe\n // interpolarse en el HTML (mismo criterio anti-inyección que `font.family`).\n const titleJson = jsonForScript(opts.title ?? '');\n // CSS del host: va el último para ganar en cascada. Se corta en `</style>` porque el bloque\n // se cierra ahí y todo lo que siguiera pasaría a interpretarse como HTML.\n const customCss = (opts.surveyCss ?? '').replace(/<\\/\\s*style/gi, '<\\\\/style');\n // `null` = decide la plataforma (style.showProgressBar); true/false = lo fuerza el host.\n const progressPref = opts.showProgressBar === undefined ? 'null' : String(opts.showProgressBar);\n\n // Textos del chrome. Arrancan con el idioma del init; dentro del WebView se re-resuelven con\n // el idioma del survey (`formData.lang[0]`), que es el que la plataforma configura en la\n // integración y con el que `@magicfeedback/native` localiza preguntas y placeholders.\n const initLabels = getLabels(opts.language);\n const initActionLabels = resolveActionLabels(opts.actions, opts.language);\n const backLabel = jsonForScript(initActionLabels.back);\n const startLabel = jsonForScript(initActionLabels.start);\n const completeLabel = jsonForScript(initActionLabels.complete);\n const submitLabel = jsonForScript(initActionLabels.accept);\n const cancelLabel = jsonForScript(initActionLabels.decline);\n // La tabla entera viaja con el HTML: el idioma del survey no se conoce hasta que carga.\n const labelsTableJson = jsonForScript(LABELS);\n // Los labels de la API van aparte para que sigan ganando después de re-resolver.\n const actionOverridesJson = jsonForScript({\n accept: trimmedOrNull(opts.actions?.accept?.label),\n start: trimmedOrNull(opts.actions?.start?.label),\n complete: trimmedOrNull(opts.actions?.complete?.label),\n back: trimmedOrNull(opts.actions?.back?.label),\n decline: trimmedOrNull(opts.actions?.decline?.label),\n });\n const initLangJson = jsonForScript(opts.language ?? '');\n // Dirección del chrome. El survey se voltea solo desde 2.2.22 (`dir=\"rtl\"` en su contenedor);\n // el header/footer/progreso son nuestros y tienen que acompañarlo. Siempre explícito.\n const initDirection = directionFor(opts.language);\n const rtlLocalesJson = jsonForScript(RTL_LOCALES);\n\n return `<!DOCTYPE html><html><head>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta name=\"color-scheme\" content=\"${colorScheme}\"/>\n<style>\n${magicfeedbackCss}\n${fontFaceCss}\nhtml,body{margin:0;padding:0;height:100%;font-family:${fontFamilyValue}}\n/* Apertura diferida: ni la tarjeta ni el velo se pintan hasta que el survey está listo\n (\\`dd-ready\\`). Si el fondo entrara antes, en RN se vería el velo oscuro varios cientos de ms\n antes que el contenido, que es la misma mala sensación que el spinner. */\nbody{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${pos.alignItems};background:transparent;padding:${bodyPadding};box-sizing:border-box}\nbody.dd-ready{background:${bodyBg}}\n#dd-popup{visibility:hidden}\nbody.dd-ready #dd-popup{visibility:visible}\n.deepdots-popup{position:relative;display:flex;flex-direction:column;justify-content:flex-start;background:${cardBg};color-scheme:${colorScheme};border-radius:${cardRadius};padding:24px;box-shadow:${cardShadow};max-width:${cardMaxWidth};width:${cardWidth};min-height:${cardMinHeight};height:${cardHeight};max-height:${cardMaxHeight};box-sizing:border-box}\n.deepdots-popup-header{display:flex;justify-content:space-between;align-items:center;gap:12px;width:100%;flex:0 0 auto}\n/* Neutraliza la regla \\`.deepdots-popup h2\\` del CSS del survey (uppercase, centrado y\n margin-bottom 40px), pensada para los enunciados y no para el título de la cabecera. */\n#dd-title{margin:0;font-family:var(--deepdots-font,'Montserrat',inherit);font-size:17px;font-weight:600;line-height:1.3;color:${textPrimary};text-transform:none;text-align:start;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n#dd-close{background:transparent;border:none;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:8px;cursor:pointer;color:${textPrimary};padding:4px;flex:0 0 auto;margin-inline-start:auto}\n/* Progreso alineado con la cabecera: sin padding horizontal propio, lo marca la tarjeta.\n El vertical es el mismo arriba y abajo, y del mismo valor que el padding de la tarjeta. */\n.deepdots-progress{display:none;flex-direction:column;gap:8px;width:100%;flex:0 0 auto;padding:16px 0;box-sizing:border-box}\n.deepdots-progress-head{display:flex;flex-direction:row;justify-content:space-between;align-items:center;width:100%;gap:8px}\n#dd-progress-label{font-size:13px;line-height:1.2}\n/* \"Question 1\" en negrita y \"of 3\" en regular gris, como el mockup. */\n#dd-progress-current{font-weight:700;color:${textPrimary}}\n#dd-progress-total{font-weight:400;color:${textMuted}}\n#dd-progress-followup{display:none;font-size:12px;font-weight:600;color:#fff;background:rgba(59,130,246,0.44);border-radius:999px;padding:2px 10px}\n.deepdots-progress-track{width:100%;height:4px;border-radius:999px;background:${trackBg};overflow:hidden}\n#dd-progress-bar{height:100%;width:0%;background:#22C55E;border-radius:999px;transition:width 450ms ease}\n/* El popup es una columna: solo el bloque de preguntas hace scroll, para que el footer\n con las acciones quede siempre visible en el borde inferior (antes vivía dentro del\n área scrollable y había que bajar hasta el final para ver \"Send\"). */\n/* Sin sangrado extra: el contenido se alinea con el título y con la barra de progreso (antes\n sumaba 20px a los lados sobre el padding de la tarjeta y quedaba más adentro que la cabecera). */\n/* Sin barra de progreso, este padding-top es el que separa la cabecera de la pregunta. Con\n barra, el JS lo pone a 0 y el hueco lo aporta el padding inferior del bloque de progreso,\n de modo que el espacio por arriba y por abajo de la barra es idéntico. */\n.deepdots-popup-container-content{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;padding:16px 0 0 0;overflow:hidden;box-sizing:border-box}\n/* El enunciado trae margin-top:20px del CSS de surveys, que sumaba al hueco de la barra y\n descompensaba la simetría. Solo se anula en el primero: entre preguntas sigue separando. */\n.deepdots-popup .magicfeedback-questions .magicfeedback-div:first-child .magicfeedback-label{margin-top:0}\n.deepdots-popup-main{display:flex;flex-direction:column;width:100%;flex:1 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden;position:relative}\n#dd-form-wrapper{width:100%;flex:1 1 auto}\n#mf{width:100%;box-sizing:border-box;visibility:hidden}\n.deepdots-popup-main *{max-width:100%;box-sizing:border-box}\n/* El margen lateral lo pone la tarjeta: el CSS del survey lo añade otra vez en el contenedor, en\n el formulario y en el bloque de cada pregunta, y eso deja el enunciado y las opciones más\n adentro que el logo, el título y la barra de progreso. Solo el lateral; el vertical separa las\n preguntas. Mismas reglas que en el popup DOM (renderPopup.ts). */\n.deepdots-popup .magicfeedback-container,\n.deepdots-popup .magicfeedback-form,\n.deepdots-popup .magicfeedback-div{padding-left:0;padding-right:0}\n#dd-logo{max-height:40px;max-width:100%;object-fit:contain;display:block;margin:12px 0 0 0}\n.deepdots-error-hint{display:none;margin:12px 0 0 0;padding:10px 12px;border-radius:6px;background:#FEF3C7;color:#92400E;border:1px solid #FCD34D;font-size:13px}\n/* Pantalla final: el HTML del editor de la plataforma (imagen + texto), centrado. */\n.deepdots-load-error{display:none;width:100%;text-align:center;padding:24px 0;font-size:15px;color:${textPrimary}}\n.deepdots-success{display:none;width:100%;text-align:center;padding:24px 0;color:${textPrimary}}\n.deepdots-success img{max-width:100%;height:auto;margin:0 auto 16px auto;display:block}\n.deepdots-success p{margin:0;font-size:16px;font-weight:600;line-height:1.4}\n.deepdots-popup-footer{display:flex;flex-direction:row-reverse;justify-content:space-between;align-items:center;gap:4px;margin-top:auto;width:100%;padding-top:12px}\n.dd-nav-btn{display:none;border:none;min-height:44px;padding:12px 24px;border-radius:999px;cursor:pointer;font-size:15px;font-weight:600;align-items:center;justify-content:center;text-align:center}\n/* Secundario como botón de texto: sin borde ni fondo, para que el primario sea la única CTA. */\n#dd-back{background:transparent;color:${textMuted};border:none}\n#dd-start,#dd-complete,#dd-submit,#dd-cancel{background:#1E293B;color:#fff;border:none}\n#dd-submit:disabled,#dd-start:disabled,#dd-back:disabled,#dd-complete:disabled{opacity:0.6;cursor:not-allowed}\n.mf-spinner{display:none;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}\n.mf-spinner-circle{width:28px;height:28px;border:3px solid #e0e6ed;border-top-color:#1E293B;border-radius:50%;animation:ddspin 0.9s linear infinite}\n@keyframes ddspin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}\n@media (max-width:640px){\n ${mobileCard}\n /* Apilados: la acción principal (Send/Start/Complete) arriba y Back debajo. El orden del\n DOM ya es ese, así que \\`column\\` (no \\`column-reverse\\`) es lo que lo respeta. */\n .deepdots-popup-footer{flex-direction:column;gap:4px}\n .dd-nav-btn{width:100%}\n .magicfeedback-checkbox-container,.magicfeedback-radio-container{margin:4px 0;padding:4px 8px}\n .magicfeedback-checkbox label,.magicfeedback-radio-container label{font-size:15px}\n .deepdots-popup input[type=\"radio\"]{margin:6px 0 6px 8px}\n .deepdots-popup input[type=\"checkbox\"]{width:16px;height:16px}\n}\n${customCss}\n</style>\n</head><body>\n<div class=\"${popupClass}\" id=\"dd-popup\" dir=\"${initDirection}\">\n <div class=\"deepdots-popup-header\">\n <h2 id=\"dd-title\" class=\"deepdots-popup-title\" hidden></h2>\n <button type=\"button\" id=\"dd-close\" aria-label=\"${initLabels.closeAria}\">\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M5 5L19 19M5 19L19 5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"butt\"/>\n </svg>\n </button>\n </div>\n <div class=\"deepdots-progress\" id=\"dd-progress\">\n <div class=\"deepdots-progress-head\">\n <span id=\"dd-progress-label\"><span id=\"dd-progress-current\"></span><span id=\"dd-progress-total\"></span></span>\n <span id=\"dd-progress-followup\">${initLabels.followUp}</span>\n </div>\n <div class=\"deepdots-progress-track\"><div id=\"dd-progress-bar\"></div></div>\n </div>\n <div class=\"deepdots-popup-container-content\" id=\"dd-content\">\n <div class=\"deepdots-popup-main\" id=\"dd-main\">\n <div id=\"dd-form-wrapper\">\n <div class=\"mf-spinner\" id=\"dd-spinner\" role=\"status\" aria-label=\"${initLabels.loadingAria}\"><div class=\"mf-spinner-circle\"></div></div>\n <div id=\"mf\"></div>\n </div>\n <div class=\"deepdots-load-error\" id=\"dd-load-error\" role=\"alert\"></div>\n <div class=\"deepdots-error-hint\" id=\"dd-error\" role=\"alert\" aria-live=\"polite\"></div>\n </div>\n <div class=\"deepdots-popup-footer\" id=\"dd-footer\">\n <button type=\"button\" class=\"dd-nav-btn\" id=\"dd-submit\"></button>\n <button type=\"button\" class=\"dd-nav-btn\" id=\"dd-back\"></button>\n <button type=\"button\" class=\"dd-nav-btn\" id=\"dd-complete\"></button>\n <button type=\"button\" class=\"dd-nav-btn\" id=\"dd-start\"></button>\n <button type=\"button\" class=\"dd-nav-btn\" id=\"dd-cancel\"></button>\n </div>\n </div>\n</div>\n<script>\n(function(){\n function emit(s){ try{ if(window.ReactNativeWebView){ window.ReactNativeWebView.postMessage(s); } }catch(e){} }\n function emitJSON(n,p){ try{ emit(JSON.stringify({name:n,payload:p||{}})); }catch(e){} }\n window.DeepdotsClose = function(){ emitJSON('popup_close'); };\n var chromeOn=${chrome ? 'true' : 'false'};\n\n var popup=document.getElementById('dd-popup');\n var main=document.getElementById('dd-main');\n var content=document.getElementById('dd-content');\n var footer=document.getElementById('dd-footer');\n var spinner=document.getElementById('dd-spinner');\n var formHost=document.getElementById('mf');\n var formWrapper=document.getElementById('dd-form-wrapper');\n var errorHint=document.getElementById('dd-error');\n var backBtn=document.getElementById('dd-back');\n var startBtn=document.getElementById('dd-start');\n var completeBtn=document.getElementById('dd-complete');\n var submitBtn=document.getElementById('dd-submit');\n var cancelBtn=document.getElementById('dd-cancel');\n var loadErrorEl=document.getElementById('dd-load-error');\n var titleEl=document.getElementById('dd-title');\n var progressEl=document.getElementById('dd-progress');\n var progressLabel=document.getElementById('dd-progress-label');\n var progressCurrent=document.getElementById('dd-progress-current');\n var progressTotal=document.getElementById('dd-progress-total');\n var progressFollowUp=document.getElementById('dd-progress-followup');\n var progressBar=document.getElementById('dd-progress-bar');\n var closeEl=document.getElementById('dd-close');\n\n // --- i18n del chrome -------------------------------------------------------------------\n // La tabla completa viaja embebida porque el idioma del survey (\\`formData.lang[0]\\`) solo se\n // conoce ya dentro del WebView. \\`ddResolveLocale\\` es el espejo del \\`resolveLocale\\` de TS\n // (paridad cubierta por test).\n var DD_LABELS=${labelsTableJson};\n var DD_ACTION_OVERRIDES=${actionOverridesJson};\n ${RESOLVE_LOCALE_JS}\n var DD_LABELS_ACTIVE=DD_LABELS[ddResolveLocale(${initLangJson})];\n var DD_RTL=${rtlLocalesJson};\n function ddIsRtl(lang){ return DD_RTL.indexOf(ddResolveLocale(lang))!==-1; }\n function ddApplyLabels(lang){\n var next=DD_LABELS[ddResolveLocale(lang)];\n if(!next) return;\n DD_LABELS_ACTIVE=next;\n // El survey ya se ha volteado a sí mismo: el chrome tiene que acompañarlo.\n popup.setAttribute('dir', ddIsRtl(lang) ? 'rtl' : 'ltr');\n backBtn.textContent=DD_ACTION_OVERRIDES.back||next.back;\n startBtn.textContent=DD_ACTION_OVERRIDES.start||next.start;\n completeBtn.textContent=DD_ACTION_OVERRIDES.complete||next.complete;\n submitBtn.textContent=DD_ACTION_OVERRIDES.accept||next.accept;\n cancelBtn.textContent=DD_ACTION_OVERRIDES.decline||next.decline;\n loadErrorEl.textContent=next.errorLoad;\n if(closeEl){ closeEl.setAttribute('aria-label',next.closeAria); }\n if(spinner){ spinner.setAttribute('aria-label',next.loadingAria); }\n progressFollowUp.textContent=next.followUp;\n }\n\n function setTitle(t){\n if(!t) return;\n titleEl.textContent=t;\n titleEl.hidden=false;\n }\n setTitle(${titleJson});\n\n // Estado de la barra de progreso. \\`progressPref\\` null = manda la plataforma (style).\n var progressPref=${progressPref};\n var progressEnabled=progressPref===true;\n var progressShowUnit=true;\n var progressUnit='fraction';\n var onStartPage=false;\n\n function updateProgress(p){\n var total=p&&typeof p.total==='number'?p.total:0;\n var progress=p&&typeof p.progress==='number'?p.progress:0;\n // Solo tiene sentido con más de una página, fuera de la pantalla de inicio y sin completar.\n if(!progressEnabled||onStartPage||p&&p.completed||total<=1){\n progressEl.style.display='none';\n // Sin barra, el hueco cabecera→pregunta lo pone el contenido.\n content.style.paddingTop='16px';\n return;\n }\n progressEl.style.display='flex';\n // Con barra, el hueco inferior ya lo pone el bloque de progreso: no duplicarlo.\n content.style.paddingTop='0';\n // Espejo de LineProgressQuestion (MagicSurvey): la barra usa el valor real (las follow-up\n // suman +0.5 y avanzan media casilla) y la etiqueta redondea hacia abajo, porque una\n // follow-up es un paso DENTRO de la misma pregunta, no la siguiente.\n var current=Math.min(total,Math.max(1,progress+1));\n var pct=Math.min(100,Math.max(0,(current/total)*100));\n progressBar.style.width=pct+'%';\n var label=Math.min(total,Math.max(1,Math.floor(progress)+1));\n progressLabel.style.display=progressShowUnit?'block':'none';\n if(progressUnit==='percentage'){\n progressCurrent.textContent=Math.round(pct)+'%';\n progressTotal.textContent='';\n } else {\n progressCurrent.textContent=DD_LABELS_ACTIVE.question+' '+label;\n progressTotal.textContent=' '+DD_LABELS_ACTIVE.of+' '+total;\n }\n progressFollowUp.style.display=p&&p.followup?'inline-block':'none';\n }\n\n backBtn.textContent=${backLabel};\n startBtn.textContent=${startLabel};\n completeBtn.textContent=${completeLabel};\n submitBtn.textContent=${submitLabel};\n cancelBtn.textContent=${cancelLabel};\n\n closeEl.onclick=function(){ emitJSON('popup_close'); };\n backBtn.onclick=function(){ if(window.DeepdotsForm && window.DeepdotsForm.back){ window.DeepdotsForm.back(); } };\n startBtn.onclick=function(){\n if(window.DeepdotsForm && window.DeepdotsForm.startForm){\n window.DeepdotsForm.startForm();\n onStartPage=false;\n updateProgress({ progress:window.DeepdotsForm.progress, total:window.DeepdotsForm.total, completed:false, followup:false });\n }\n };\n completeBtn.onclick=function(){ emitJSON('popup_close'); };\n // Única acción del survey que no carga: cierra igual que la X.\n cancelBtn.onclick=function(){ emitJSON('popup_close'); };\n var surveyCompletedEmitted=false;\n submitBtn.onclick=function(){\n if(surveyCompletedEmitted) return;\n if(window.DeepdotsForm && window.DeepdotsForm.send){ window.DeepdotsForm.send(); }\n };\n\n // Mensaje final configurado en la plataforma (\\`style.successMessage\\`). Se guarda al cargar\n // y se pinta al completar. Va por innerHTML porque es HTML del editor de la plataforma\n // (imagen + texto), igual que ya hace \\`renderStartMessage\\` con el mensaje de inicio.\n var successMessageHtml='';\n function showSuccessScreen(){\n progressEl.style.display='none';\n var wrapper=document.getElementById('dd-form-wrapper');\n if(wrapper){ wrapper.style.display='none'; }\n var done=document.getElementById('dd-success');\n if(!done){\n done=document.createElement('div');\n done.id='dd-success';\n done.className='deepdots-success';\n main.insertBefore(done, errorHint);\n }\n done.innerHTML=successMessageHtml || '<p>Thank you for your feedback!</p>';\n done.style.display='block';\n }\n\n // Profundidad de navegación DENTRO del survey: +1 por página avanzada, -1 al volver.\n // Sustituye a la condición \\`total>1 && progress>0 && progress<total\\`, que escondía el\n // Back siempre que la siguiente pantalla era una follow-up dinámica: las follow-up no\n // entran en el grafo (suman +0.5 al progress y no tocan el total), así que un survey de\n // una pregunta con follow-up tiene total=1 y nunca cumplía \\`total>1\\`.\n var pageDepth=0;\n\n function updateNavButtons(){\n updateButtons(pageDepth>0?'in_progress_next':'in_progress_first');\n }\n\n function updateButtons(state){\n backBtn.style.display='none';\n startBtn.style.display='none';\n submitBtn.style.display='none';\n completeBtn.style.display='none';\n cancelBtn.style.display='none';\n backBtn.style.width='';\n startBtn.style.width='';\n submitBtn.style.width='';\n completeBtn.style.width='';\n if(state==='start'){\n startBtn.style.display='inline-flex';\n startBtn.style.width='100%';\n } else if(state==='in_progress_first'){\n submitBtn.style.display='inline-flex';\n } else if(state==='in_progress_next'){\n backBtn.style.display='inline-flex';\n submitBtn.style.display='inline-flex';\n } else if(state==='completed'){\n completeBtn.style.display='inline-flex';\n completeBtn.style.width='100%';\n } else if(state==='load_error'){\n cancelBtn.style.display='inline-flex';\n }\n }\n\n${PRIMARY_COLOR_JS}\n${BUSY_JS}\n${REVEAL_JS}\n // Foco en la primera pregunta si es de escribir. Al navegar lo pone @magicfeedback/native\n // (autofocus:'navigation'); la apertura la decide el popup, que es quien sabe cuándo se revela.\n // Necesita el popup revelado y el survey cargado, que llegan en cualquier orden, y solo con\n // ratón o trackpad: en táctil sacaría el teclado sin que el usuario haya tocado nada.\n var ddRevealed=false, ddSurveyLoaded=false, ddOpenFocusDone=false;\n function ddAutofocusOnOpen(){\n if(ddOpenFocusDone || !ddRevealed || !ddSurveyLoaded){ return; }\n ddOpenFocusDone=true;\n var fine=typeof window.matchMedia==='function' && window.matchMedia('(pointer: fine)').matches;\n var f=window.DeepdotsForm;\n if(fine && f && typeof f.focusFirstQuestion==='function'){ f.focusFirstQuestion(); }\n }\n // El popup se enseña cuando el survey está pintado; \\`ready\\` deja al host abrir su propio\n // Modal en ese momento en vez de montar un WebView en blanco.\n var ddReveal=ddCreateReveal(popup, function(){\n document.body.classList.add('dd-ready');\n emitJSON('ready');\n ddRevealed=true;\n ddAutofocusOnOpen();\n }, ${REVEAL_TIMEOUT_MS});\n\n function setLoading(isLoading){\n spinner.style.display=isLoading?'flex':'none';\n // El spinner no tapa el survey (28px en posicion absoluta), asi que sin esto el usuario\n // puede seguir cambiando de opcion mientras se envia la pagina, y ese cambio ya no viaja\n // con ella. Se bloquea el wrapper, que es el elemento estable: el div de dentro lo\n // sustituye @magicfeedback/native por su propio contenedor al generar el survey.\n ddSetSurveyBusy(formWrapper, isLoading);\n if(!isLoading){\n formHost.style.visibility='visible';\n // Fin de la carga, sea por survey cargado o por error. Entre páginas es no-op: la\n // revelación solo actúa la primera vez.\n ddReveal.whenPainted();\n }\n footer.style.display=isLoading?'none':'flex';\n backBtn.disabled=isLoading; startBtn.disabled=isLoading; completeBtn.disabled=isLoading; submitBtn.disabled=isLoading;\n if(isLoading){ updateButtons(null); }\n }\n setLoading(true);\n\n // Survey que no ha cargado (406 de /info, sin red, bundle del CDN caído). Antes se emitía\n // \\`loaded\\` y se pintaba un Send sobre un survey vacío. Espejo de KMP: aviso traducido, la X\n // sigue, un único Cancel, y se revela ya (manda \\`ready\\`): no hay nada más que esperar.\n // No es interacción del usuario: el renderer no lo convierte en popup_clicked.\n var ddLoadFailed=false;\n function ddShowLoadError(name, reason){\n if(ddLoadFailed){ return; }\n ddLoadFailed=true;\n var why=(reason && reason.message) ? reason.message : String(reason);\n emitJSON(name, { message: why });\n progressEl.style.display='none';\n formWrapper.style.display='none';\n errorHint.style.display='none';\n loadErrorEl.textContent=DD_LABELS_ACTIVE.errorLoad;\n loadErrorEl.style.display='block';\n setLoading(false);\n updateButtons('load_error');\n ddReveal.reveal();\n }\n\n var initialized=false;\n function initMF(){\n if(initialized || !window.magicfeedback) return initialized;\n initialized=true;\n try{\n window.magicfeedback.init({ debug:false, env:'${env}' });\n var form = window.magicfeedback.form(${sid}, ${pid}, ${profileJson}, ${metaJson});\n window.DeepdotsForm = form;\n var stylesInjected=false;\n form.generate('mf', {\n addButton:false,\n getMetaData:true,\n // La pantalla final la pinta el popup: renderSuccess de @magicfeedback/native usa\n // textContent, así que el mensaje de la plataforma (HTML con imagen) no se vería, y su\n // fallback es un literal genérico que ignora style.successMessage.\n addSuccessScreen:false,\n // Surveys multi-idioma: native pide a la API este idioma (el del init o el del\n // dispositivo); si el survey no lo tiene, la API sirve el idioma por defecto.\n lang:${initLangJson}||undefined,\n // Foco en la primera pregunta de escribir tras Start, Siguiente o Atrás. Las versiones\n // de native sin esta opción la ignoran.\n autofocus:'navigation',\n onLoadedEvent:function(args){\n if(args && args.error){ ddShowLoadError('error:load', args.error); return; }\n // Idioma en el que native muestra el survey: manda sobre el del init para que el\n // chrome no se quede en inglés delante de un survey traducido. Native < 2.3 no lo\n // envía: entonces el primero de formData.lang (el idioma por defecto).\n var shown=args && typeof args.lang==='string' && args.lang.trim() ? args.lang : null;\n var langs=shown ? [shown] : (args && args.formData ? args.formData.lang : null);\n if(langs && langs.length){\n for(var li=0;li<langs.length;li++){\n if(typeof langs[li]==='string' && langs[li].trim()){ ddApplyLabels(langs[li]); break; }\n }\n }\n var s=args && args.formData ? args.formData.style : null;\n if(s && !stylesInjected){\n stylesInjected=true;\n if(s.successMessage){ successMessageHtml=s.successMessage; }\n // La barra de progreso la decide el host (init) y, si no se pronuncia, la plataforma.\n if(progressPref===null){ progressEnabled=s.showProgressBar===true; }\n if(s.showProgressUnit!==undefined){ progressShowUnit=s.showProgressUnit!==false; }\n if(s.progressUnit){ progressUnit=s.progressUnit; }\n if(s.loadingBarColor){ progressBar.style.background=s.loadingBarColor; progressFollowUp.style.background=s.loadingBarColor; }\n if(chromeOn && s.boxBackgroundColor){ popup.style.background=s.boxBackgroundColor; }\n if(s.contentAlign){ main.style.justifyContent = s.contentAlign==='center' ? 'center' : 'flex-start'; }\n // El survey pinta sus controles con --mf-primary, que sale de primaryColor; si la\n // integración solo configura el del botón, el popup mezclaría el color de la marca\n // con el gris azulado por defecto del paquete.\n ddApplySurveyPrimaryColor(popup, s);\n if(s.buttonPrimaryColor){\n submitBtn.style.background=s.buttonPrimaryColor; submitBtn.style.border='none'; submitBtn.style.color='#fff';\n startBtn.style.background=s.buttonPrimaryColor; startBtn.style.border='none'; startBtn.style.color='#fff';\n completeBtn.style.background=s.buttonPrimaryColor; completeBtn.style.border='none'; completeBtn.style.color='#fff';\n }\n if(s.buttonSecondaryColor){\n // Botón de texto: solo tiñe la etiqueta, sin recuperar fondo ni borde.\n backBtn.style.background='transparent'; backBtn.style.color=s.buttonSecondaryColor; backBtn.style.border='none';\n }\n if(s.logo && !document.getElementById('dd-logo')){\n var logoImg=document.createElement('img');\n logoImg.id='dd-logo'; logoImg.src=s.logo; logoImg.alt='Logo';\n if(s.logoSize==='small'){ logoImg.style.maxHeight='30px'; }\n else if(s.logoSize==='medium'){ logoImg.style.maxHeight='50px'; }\n else if(s.logoSize==='large'){ logoImg.style.maxHeight='70px'; }\n // Márgenes lógicos: con un survey RTL el popup va con dir=\"rtl\" y el logo\n // tiene que acompañar al chrome. En LTR el resultado es el mismo de antes.\n if(s.logoPosition==='left'){ logoImg.style.margin='12px 0 0 0'; logoImg.style.setProperty('margin-inline-start','0'); logoImg.style.setProperty('margin-inline-end','16px'); }\n else if(s.logoPosition==='right'){ logoImg.style.margin='12px 0 0 0'; logoImg.style.setProperty('margin-inline-start','auto'); logoImg.style.setProperty('margin-inline-end','0'); }\n else if(s.logoPosition==='center'){ logoImg.style.margin='12px auto 0 auto'; }\n // Encima de la barra de progreso, como hermano del header (espejo de ui/logo.ts en\n // web). Fuera de #dd-main, así el logo no se va con el scroll del formulario.\n popup.insertBefore(logoImg, progressEl);\n }\n if(s.startMessage && s.startMessage!==''){ onStartPage=true; updateButtons('start'); }\n else { updateButtons('in_progress_first'); }\n } else {\n updateButtons('in_progress_first');\n }\n // El total solo se conoce una vez montado el form; sin él no hay barra que pintar.\n updateProgress({ progress:form.progress, total:form.total, completed:false, followup:false });\n emitJSON('loaded');\n setLoading(false);\n // La primera carga es la apertura; las siguientes (tras Start) las enfoca native.\n if(!ddSurveyLoaded){ ddSurveyLoaded=true; ddAutofocusOnOpen(); }\n },\n beforeSubmitEvent:function(){\n setLoading(true);\n emitJSON('before_submit');\n },\n afterSubmitEvent:function(p){\n var error=p && p.error ? (typeof p.error==='string' ? p.error : String(p.error)) : '';\n if(error){\n setLoading(false);\n if(error.toLowerCase().indexOf('no response')!==-1){\n // La página no ha cambiado: el estado de navegación se queda como estaba.\n errorHint.textContent=DD_LABELS_ACTIVE.errorRequired;\n errorHint.style.display='block';\n updateNavButtons();\n return;\n }\n errorHint.textContent=DD_LABELS_ACTIVE.errorSubmit;\n errorHint.style.display='block';\n return;\n }\n errorHint.style.display='none';\n setLoading(false);\n if(p && p.completed){\n surveyCompletedEmitted=true;\n showSuccessScreen();\n updateButtons('completed');\n updateProgress(p);\n emitJSON('survey_completed', p);\n return;\n }\n pageDepth++;\n updateNavButtons();\n updateProgress(p);\n emitJSON('after_submit', p);\n },\n onBackEvent:function(p){\n // Con error (\"No page found\") no hubo navegación: la profundidad no se toca.\n if(!(p&&p.error)){ pageDepth=Math.max(0,pageDepth-1); }\n updateNavButtons();\n // onBackEvent no trae \\`total\\`: se lee del form, que es su dueño.\n updateProgress({ progress:p&&p.progress, total:form.total, completed:false, followup:p&&p.followup });\n emitJSON('back', p);\n }\n }).catch(function(e){ ddShowLoadError('error:init', e); });\n }catch(e){ ddShowLoadError('error:exception', e); }\n return true;\n }\n var s=document.createElement('script'); s.src=${JSON.stringify(cdn)}; s.async=true;\n s.onload=function(){ initMF(); };\n s.onerror=function(){ ddShowLoadError('error:load', 'survey script failed to load'); };\n document.head.appendChild(s);\n var tries=0, t=setInterval(function(){ if(initMF()||++tries>40){ clearInterval(t); } },150);\n})();\n</script>\n</body></html>`;\n}\n","import type { DeepdotsEventType, PopupActions, PopupStyle } from '../types';\nimport type { PopupRenderer, PopupRenderOptions } from './renderer';\nimport { buildSurveyIdentity } from '../tracking/tracking-manager';\nimport { buildSurveyHtml } from '../ui/surveyHtml';\nimport { REVEAL_TIMEOUT_MS } from '../ui/reveal';\nimport { sdkWarn } from '../util/logger';\n\n/**\n * Margen que el aviso nativo de \"listo\" deja por detrás del techo que ya lleva el HTML. El\n * WebView se revela solo a `REVEAL_TIMEOUT_MS`; este respaldo solo debe entrar cuando el WebView\n * no llega ni a ejecutar el HTML (motor que no arranca, documento que no carga), que es el único\n * caso en el que el host se quedaría esperando para siempre.\n */\nexport const NATIVE_READY_GRACE_MS = 500;\n\ntype EmitFn = (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void;\n\n/** Payload que recibe el host para montar el WebView del survey. */\nexport interface ReactNativeSurveyPayload {\n surveyId: string;\n productId: string;\n /** HTML autocontenido para `<WebView source={{ html }}>`. */\n html: string;\n}\n\nexport interface ReactNativeRendererOptions {\n /** Se llama al mostrar un popup: monta el WebView con `payload.html`. */\n onShow?: (payload: ReactNativeSurveyPayload) => void;\n /** Se llama al cerrar el popup: desmonta el WebView. */\n onHide?: () => void;\n /**\n * Se llama cuando el survey ya está pintado dentro del WebView, o pasado el techo de espera.\n *\n * Sirve para no enseñar un WebView en blanco: monta el WebView en cuanto llegue `onShow` (fuera\n * de pantalla o transparente) y ábrelo al recibir `onReady`. El HTML se revela solo de todas\n * formas, así que el host que no lo use sigue funcionando igual.\n */\n onReady?: (payload: { surveyId: string }) => void;\n}\n\n/**\n * Renderer de React Native: el SDK no puede pintar componentes RN, así que entrega el\n * HTML del survey al host (que lo monta en `react-native-webview`) y traduce los\n * mensajes del WebView a eventos de popup del SDK (→ `POST /sdk/popups`, Messaging #18–22).\n *\n * Uso: monta el WebView con `onShow` y enséñalo con `onReady`, para que el usuario no vea el\n * WebView arrancando en blanco (motor + bundle del CDN + fetch del survey).\n * ```tsx\n * const renderer = new ReactNativePopupRenderer({\n * onShow: (p) => setSurvey({ ...p, ready: false }),\n * onReady: () => setSurvey((s) => (s ? { ...s, ready: true } : s)),\n * onHide: () => setSurvey(null),\n * });\n * sdk.setRenderer(renderer);\n * // …\n * // ⚠️ Con `<Modal visible={survey.ready}>` esto NO funciona: React Native no monta los hijos\n * // de un Modal cerrado, así que el WebView no empezaría a cargar y `ready` no llegaría nunca.\n * // El WebView tiene que estar montado y solo invisible.\n * {survey ? (\n * <View style={[StyleSheet.absoluteFill, { opacity: survey.ready ? 1 : 0 }]}\n * pointerEvents={survey.ready ? 'auto' : 'none'}>\n * <WebView source={{ html: survey.html }}\n * onMessage={(e) => renderer.handleMessage(e.nativeEvent.data)} />\n * </View>\n * ) : null}\n * ```\n *\n * Quien prefiera no complicarse puede ignorar `onReady` y abrir su Modal en `onShow`: el HTML se\n * revela solo igualmente, así que el usuario verá el contenedor del host unos cientos de ms antes\n * que la tarjeta, pero nunca el spinner.\n */\nexport class ReactNativePopupRenderer implements PopupRenderer {\n private emitFn: EmitFn | null = null;\n private onCloseFn: (() => void) | null = null;\n private currentSurveyId: string | null = null;\n private partialEmitted = false;\n private readyEmitted = false;\n private readyTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(private options: ReactNativeRendererOptions = {}) {}\n\n init(): void {\n /* nada que preparar */\n }\n\n show(\n surveyId: string,\n productId: string,\n actions: PopupActions | undefined,\n emit: EmitFn,\n onClose: () => void,\n env: string = 'production',\n userId?: string,\n style?: PopupStyle,\n sessionId?: string,\n miniService?: string,\n analyticsFeedbackSessionId?: string,\n renderChrome?: boolean,\n options?: PopupRenderOptions,\n ): void {\n this.emitFn = emit;\n this.onCloseFn = onClose;\n this.currentSurveyId = surveyId;\n this.partialEmitted = false;\n this.readyEmitted = false;\n this.armReadyFallback(surveyId);\n\n const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);\n const html = buildSurveyHtml({\n surveyId,\n productId,\n env,\n profile,\n metadata,\n font: style?.font,\n theme: style?.theme,\n position: style?.position,\n actions,\n // renderChrome:false (init) → HTML sin tarjeta/overlay: el host lo envuelve en su Modal.\n chrome: renderChrome,\n title: options?.title,\n showProgressBar: options?.showProgressBar,\n surveyCss: options?.surveyCss,\n language: options?.language,\n });\n if (this.options.onShow) {\n this.options.onShow({ surveyId, productId, html });\n } else {\n sdkWarn(\n '[Deepdots] ReactNativePopupRenderer has no onShow: pass { onShow } to new ReactNativePopupRenderer({...}) to mount the survey WebView.',\n );\n }\n }\n\n hide(): void {\n this.clearReadyFallback();\n this.options.onHide?.();\n this.currentSurveyId = null;\n }\n\n /** Respaldo del aviso de listo por si el WebView no llega a ejecutar el HTML. */\n private armReadyFallback(surveyId: string): void {\n this.clearReadyFallback();\n if (!this.options.onReady) return;\n this.readyTimer = setTimeout(() => {\n this.readyTimer = null;\n this.emitReady(surveyId);\n }, REVEAL_TIMEOUT_MS + NATIVE_READY_GRACE_MS);\n }\n\n private clearReadyFallback(): void {\n if (this.readyTimer !== null) {\n clearTimeout(this.readyTimer);\n this.readyTimer = null;\n }\n }\n\n /** Idempotente: el WebView puede avisar a la vez que vence el respaldo. */\n private emitReady(surveyId: string): void {\n if (this.readyEmitted) return;\n this.readyEmitted = true;\n this.clearReadyFallback();\n this.options.onReady?.({ surveyId });\n }\n\n /**\n * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a\n * eventos del SDK: primera interacción → `popup_clicked` (PARTIAL), completado →\n * `survey_completed` (COMPLETED) y cierra el popup.\n */\n handleMessage(raw: string): void {\n const surveyId = this.currentSurveyId;\n if (!surveyId || !this.emitFn) return;\n\n let name: string | undefined;\n try {\n name = (JSON.parse(raw) as { name?: string }).name;\n } catch {\n name = raw; // mensajes simples (\"error:load\")\n }\n\n switch (name) {\n case 'ready':\n // El survey ya está pintado dentro del WebView. NO es interacción del usuario: no debe\n // marcar el popup como PARTIAL.\n this.emitReady(surveyId);\n break;\n case 'loaded':\n // Solo avisa de que el survey ha cargado: tampoco es interacción. Antes contaba como\n // la primera, y cada popup que se abría quedaba como PARTIAL en backend.\n break;\n case 'before_submit':\n case 'after_submit':\n case 'back':\n if (!this.partialEmitted) {\n this.partialEmitted = true;\n this.emitFn('popup_clicked', surveyId, { action: 'partial' });\n }\n break;\n case 'survey_completed':\n // Solo reporta el estado COMPLETED. NO cierra: el WebView acaba de pintar la pantalla\n // final del survey y cerrar aquí la hacía invisible. El cierre llega después con\n // `popup_close`, cuando el usuario pulsa el botón de completar.\n this.emitFn('survey_completed', surveyId);\n break;\n case 'popup_close':\n this.onCloseFn?.();\n break;\n default:\n // `error:load` / `error:init` / `error:exception`: el survey no ha cargado y el WebView\n // ya enseña el aviso con su Cancel. El HTML manda `ready` al revelarlo; esto cubre un\n // WebView que solo avise del error, para que el host que difiere el montaje lo enseñe.\n // Tampoco es interacción del usuario: nada de popup_clicked.\n if (typeof name === 'string' && name.startsWith('error:')) this.emitReady(surveyId);\n break;\n }\n }\n}\n\n/** Detección simple de entorno React Native. */\nexport function isReactNativeEnv(): boolean {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return typeof navigator !== 'undefined' && (navigator as any).product === 'ReactNative';\n}\n\n/** Factoría usada por `createDefaultRenderer` cuando se detecta RN (el host debería pasar onShow). */\nexport function createReactNativeRenderer(): PopupRenderer {\n return new ReactNativePopupRenderer();\n}\n","/**\n * Precalentado del chunk de `renderPopup`.\n *\n * `renderPopup` se carga por dynamic import para que importar el SDK sea seguro en React Native\n * y en SSR, pero eso deja su descarga (DOM del popup + `@magicfeedback/native` + el CSS\n * vendorizado, ~238 KB sin comprimir) dentro del camino crítico: empieza cuando el trigger ya ha\n * disparado y el usuario está mirando. Traerlo en idle en cuanto se sabe que hay algún popup que\n * puede abrirse deja el import de `show()` resuelto al instante.\n *\n * Es oportunista por diseño: si falla, no rompe nada porque `show()` hace su propio import.\n */\n\ntype Loader = () => Promise<unknown>;\n\nconst defaultLoader: Loader = () => import('./renderPopup');\n\n/** Precalentado en curso o ya resuelto. Null tras un fallo, para permitir reintento. */\nlet pending: Promise<void> | null = null;\n\n/**\n * Descarga el chunk una sola vez por sesión. Nunca rechaza: un precalentado que falla es un\n * no-evento, la carga de verdad la hará `show()`.\n */\nexport function preloadRenderPopup(loader: Loader = defaultLoader): Promise<void> {\n if (pending) return pending;\n try {\n // El loader se invoca ya, no en un microtask: el import debe empezar en este tick.\n pending = loader().then(() => undefined).catch(() => { pending = null; });\n } catch {\n pending = null;\n return Promise.resolve();\n }\n return pending;\n}\n\n/**\n * Agenda el precalentado para cuando el navegador esté ocioso, para no competir con el render\n * de la página del host. `requestIdleCallback` no existe en Safari, ahí basta con salir del\n * tick actual.\n */\nexport function schedulePreloadRenderPopup(loader?: Loader): void {\n const run = () => { void preloadRenderPopup(loader); };\n const idle = (globalThis as { requestIdleCallback?: (cb: () => void, opts?: { timeout: number }) => unknown }).requestIdleCallback;\n if (typeof idle === 'function') {\n idle(run, { timeout: 2000 });\n return;\n }\n setTimeout(run, 0);\n}\n","import {DeepdotsEventType, PopupActions, PopupStyle} from '../types';\nimport { isReactNativeEnv, createReactNativeRenderer } from './react-native-renderer';\nimport { schedulePreloadRenderPopup } from '../ui/preload';\n// renderPopup (DOM + @magicfeedback/native) se carga PEREZOSAMENTE solo al mostrar\n// un popup en navegador, para que importar el SDK sea seguro en React Native/SSR.\n\n/**\n * Ajustes de presentación que no vienen del estilo del popup. Van agrupados para no seguir\n * alargando la lista posicional de `show`, que ya arrastra doce parámetros.\n */\nexport interface PopupRenderOptions {\n /**\n * Título de la cabecera: el `title` de la definición del popup, tal cual llega de la API.\n * Varía por popup y vacío es un valor válido (la cabecera se queda solo con la X).\n */\n title?: string;\n /** Barra de progreso: `undefined` deja decidir a la plataforma, `true`/`false` la fuerzan. */\n showProgressBar?: boolean;\n /** CSS del host, inyectado el último: gana sobre el del SDK y el de `@magicfeedback/native`. */\n surveyCss?: string;\n /**\n * Idioma con el que arranca el chrome del popup (botones, progreso, aria-labels, errores).\n * Es el idioma resuelto en `init()` (host > navigator > Intl). Lo pisa el idioma del survey\n * (`formData.lang[0]`) en cuanto este carga, y ambos los pisa `actions.*.label` de la API.\n */\n language?: string;\n /**\n * Nonce de la Content-Security-Policy del host (`DeepdotsInitParams.cspNonce`). Se pone en\n * cada `<style>` que inyecta el popup DOM; sin él, una CSP con `style-src 'nonce-…'` bloquea\n * la hoja del survey. No aplica a React Native (el survey vive en su propio WebView).\n */\n cspNonce?: string;\n}\n\nexport interface PopupRenderer {\n /** Preparar recursos si aplica */\n init?(): void;\n /**\n * Traer por adelantado lo que la apertura necesitará, en idle. Opcional: solo lo implementan\n * los renderers cuyo coste de apertura se paga delante del usuario.\n */\n preload?(): void;\n /** Mostrar popup */\n show(\n surveyId: string,\n productId: string,\n actions: PopupActions | undefined,\n emit: (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void,\n onClose: () => void,\n env?: string,\n userId?: string,\n style?: PopupStyle,\n sessionId?: string,\n miniService?: string,\n analyticsFeedbackSessionId?: string,\n renderChrome?: boolean,\n options?: PopupRenderOptions,\n ): void;\n /** Ocultar popup */\n hide(): void;\n}\n\n/** Renderer que no hace nada (SSR / entornos sin DOM) */\nexport class NoopPopupRenderer implements PopupRenderer {\n show(): void { /* no-op */ }\n hide(): void { /* no-op */ }\n}\n\n/** Renderer para navegadores usando la implementación actual basada en renderPopup */\nexport class BrowserPopupRenderer implements PopupRenderer {\n private container: HTMLElement | null = null;\n private visible = false;\n\n init(): void {\n if (typeof document === 'undefined') return;\n if (!this.container) {\n this.container = document.createElement('div');\n this.container.id = 'deepdots-popup-container';\n this.container.style.cssText = `\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: none;\n z-index: 999999;\n background: rgba(0, 0, 0, 0.5);\n justify-content: center;\n align-items: center;\n `;\n }\n if (!document.body.contains(this.container)) {\n document.body.appendChild(this.container);\n }\n }\n\n /**\n * Descarga el chunk de `renderPopup` antes de que ningún trigger dispare, para que abrir el\n * popup no empiece por esperar ~238 KB de JS.\n */\n preload(): void {\n schedulePreloadRenderPopup();\n }\n\n show(\n surveyId: string,\n productId: string,\n actions: PopupActions | undefined,\n emit: (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void,\n onClose: () => void,\n env: string = 'production',\n userId?: string,\n style?: PopupStyle,\n sessionId?: string,\n miniService?: string,\n analyticsFeedbackSessionId?: string,\n _renderChrome?: boolean, // solo aplica a RN (WebView); el popup DOM web siempre lleva su chrome\n options?: PopupRenderOptions,\n ): void {\n if (this.visible) return;\n if (!this.container || !document.body.contains(this.container)) this.init();\n if (!this.container) return; // aún sin DOM\n this.visible = true;\n const container = this.container;\n void import('../ui/renderPopup').then(({ renderPopup }) => {\n renderPopup(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService, analyticsFeedbackSessionId, options);\n });\n }\n\n hide(): void {\n this.visible = false;\n if (this.container) {\n this.container.style.display = 'none';\n // `renderPopup` abre el contenedor invisible y lo revela al cargar el survey; si se cierra\n // antes de esa revelación, el estado oculto no puede quedarse pegado al contenedor.\n this.container.style.visibility = '';\n this.container.style.pointerEvents = '';\n this.container.innerHTML = '';\n }\n }\n}\n\n/** Factoría para obtener renderer por defecto */\nexport function createDefaultRenderer(): PopupRenderer {\n if (typeof document === 'undefined') {\n // Entorno sin DOM: probar RN primero\n if (isReactNativeEnv()) return createReactNativeRenderer();\n return new NoopPopupRenderer();\n }\n // Si estamos en React Native y hay WebView con document emulado, aún queremos RN renderer\n if (isReactNativeEnv()) return createReactNativeRenderer();\n return new BrowserPopupRenderer();\n}\n","/**\n * Capa de Analytics (eventos GA-style) — canal SEPARADO del feedback de survey.\n *\n * El feedback de survey se envía con `magicfeedback.send()`. La analítica de\n * navegación/comportamiento (page views, mini-services, tasks/funnels, meaningful\n * interactions, findability…) va por un ENDPOINT PROPIO, vinculada por `user_id`.\n *\n * De momento NO se hace la llamada: el `sink` por defecto hace `console.log` del\n * payload exacto que se enviaría (dry-run), para poder verificarlo. Cuando el\n * endpoint exista, se sustituye el sink por un POST.\n */\n\nimport type { DeviceInfo } from './device-info';\n\nexport interface AnalyticsEvent {\n name: string;\n timestamp: number;\n params?: Record<string, unknown>;\n}\n\nexport interface AnalyticsEnvelope {\n /** Clave pública del cliente (auth). */\n publicKey?: string;\n /** Id que vincula la analítica al usuario (el backend cose por aquí). */\n userId: string | null;\n /** Session id del backend si ya se conoce (puede ir null). */\n sessionId: string | null;\n context: {\n platform: string; // 'web' | 'android' | 'ios' (RN inyecta la suya)\n language?: string;\n /** Info de dispositivo (Technology #11–13). */\n device?: DeviceInfo;\n /** User attributes del cliente, usados para breakdowns (registration_status, pass_type, …). */\n attributes: Record<string, string>;\n /** Métricas del host: valores medibles persistentes → van en `feedback.metrics` del body. */\n metrics?: Record<string, string>;\n };\n events: AnalyticsEvent[];\n}\n\n/** Contexto del flush, para que el sink pueda adaptar el transporte. */\nexport interface AnalyticsFlushMeta {\n /**\n * `true` cuando el flush ocurre porque la página/app se está cerrando. El sink debe usar\n * un transporte que sobreviva al unload (`fetch` con `keepalive`) y no puede esperar\n * respuestas.\n */\n final?: boolean;\n /**\n * `true` cuando este es el ÚLTIMO lote de la sesión (cierre de página, app a background,\n * cambio de usuario, tracking desactivado). El sink lo envía con `completed:true` y olvida\n * el `sessionId`, para que el lote siguiente abra un registro nuevo.\n */\n sessionEnd?: boolean;\n /**\n * `true` para olvidar la sesión SIN enviar nada (el payload llega vacío): el backend ya la\n * cerró por inactividad. El sink descarta su `sessionId` para que el lote siguiente abra un\n * registro nuevo, igual que tras `sessionEnd`, pero sin POST.\n */\n resetSession?: boolean;\n}\n\n/**\n * Envío del lote. Si devuelve una promesa que **rechaza**, el manager re-encola los\n * eventos para reintentarlos en el siguiente flush (fallo transitorio: red o 5xx).\n * Resolver = lote entregado o descartado definitivamente (no se reintenta).\n */\nexport type AnalyticsSink = (payload: AnalyticsEnvelope, meta?: AnalyticsFlushMeta) => void | Promise<void>;\n\n/** Identidad resuelta por el tracking, inyectada al construir el payload. */\nexport interface AnalyticsIdentity {\n userId: string | null;\n sessionId: string | null;\n}\n\n/**\n * Sink de dry-run: NO envía nada, solo vuelca lo que se enviaría por el `log` dado\n * (default `console.log`, o el logger inyectado por el host en init()).\n */\nexport function createDryRunSink(log: (...args: unknown[]) => void = console.log): AnalyticsSink {\n return (payload, meta) => {\n if (meta?.resetSession) return;\n log(\n '[DeepdotsAnalytics] (dry-run · NOT sent · no init.analytics) POST /sdk/feedback →',\n JSON.stringify(payload, null, 2),\n );\n };\n}\n\n/** Sink por defecto: NO envía nada, solo pinta por consola lo que se enviaría. */\nexport const dryRunSink: AnalyticsSink = createDryRunSink();\n\nexport interface AnalyticsManagerOptions {\n sink?: AnalyticsSink;\n now?: () => number;\n publicKey?: string;\n language?: string;\n device?: DeviceInfo;\n /** Plataforma del envelope (default 'web'; RN inyecta 'android'/'ios'). */\n platform?: string;\n /** Nº máximo de eventos en buffer antes de solicitar un flush automático (default 20). */\n maxBatchSize?: number;\n /**\n * Techo de eventos retenidos en memoria contando los re-encolados por fallo de envío\n * (default 200). Al superarlo se descartan los más antiguos.\n */\n maxBufferedEvents?: number;\n /** Callback invocado cuando el buffer alcanza `maxBatchSize`. El caller hace el flush real. */\n onFlushNeeded?: () => void;\n}\n\nexport class AnalyticsManager {\n private sink: AnalyticsSink;\n private now: () => number;\n private publicKey?: string;\n private language?: string;\n private device?: DeviceInfo;\n private platform: string;\n\n private events: AnalyticsEvent[] = [];\n private attributes: Record<string, string> = {};\n /** Métricas del host (valores medibles persistentes). Sobrescriben por key; se reenvían en cada flush. */\n private metrics: Record<string, string> = {};\n /** Mini-services activos: nombre → timestamp de entrada. El orden de inserción marca el \"más reciente\". */\n private activeMiniServices = new Map<string, number>();\n private maxBatchSize: number;\n private maxBufferedEvents: number;\n private onFlushNeeded?: () => void;\n\n constructor(options: AnalyticsManagerOptions = {}) {\n this.sink = options.sink ?? dryRunSink;\n this.now = options.now ?? (() => Date.now());\n this.publicKey = options.publicKey;\n this.language = options.language;\n this.device = options.device;\n this.platform = options.platform ?? 'web';\n this.maxBatchSize = options.maxBatchSize ?? 20;\n this.maxBufferedEvents = options.maxBufferedEvents ?? 200;\n this.onFlushNeeded = options.onFlushNeeded;\n }\n\n /** Actualiza campos de device en runtime (p.ej. country/city tras resolver geo). */\n updateDevice(partial: Partial<DeviceInfo>): void {\n this.device = { ...this.device, ...partial } as DeviceInfo;\n }\n\n /** Mezcla user attributes (se coercionan a string). Mutable en runtime. */\n setUserAttributes(attrs: Record<string, string | number | boolean>): void {\n for (const [k, v] of Object.entries(attrs)) {\n if (!k) continue;\n this.attributes[k] = String(v);\n }\n }\n\n /**\n * Registra/actualiza una métrica (valor medible). Persistente: se reenvía en cada flush.\n * Repetir la misma key SOBRESCRIBE; el valor se coerciona a string; ignora key vacía.\n */\n setMetric(key: string, value: string | number | boolean): void {\n if (!key) return;\n this.metrics[key] = String(value);\n }\n\n /**\n * Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`.\n * Admite varios activos a la vez (concurrencia); el \"actual\" para etiquetar es el más reciente.\n * Reentrar con un nombre ya activo refresca su orden y su tiempo de entrada.\n */\n enterMiniService(name: string, entryPointType?: string): void {\n this.activeMiniServices.delete(name); // reinsertar = pasa a ser el más reciente\n this.activeMiniServices.set(name, this.now());\n this.track('deepdots_mini_service_enter', { entry_point_type: entryPointType ?? null });\n }\n\n /**\n * Cierra el mini-service `name` emitiendo `mini_service_exit` con su duración (#27).\n * No-op si ese nombre no está activo. Cierre coherente con concurrencia.\n */\n exitMiniService(name: string): void {\n const enteredAt = this.activeMiniServices.get(name);\n if (enteredAt === undefined) return;\n const durationSeconds = Math.max(0, Math.round((this.now() - enteredAt) / 1000));\n this.activeMiniServices.delete(name); // dejar de etiquetar con este antes de emitir\n this.track('deepdots_mini_service_exit', { mini_service: name, duration_seconds: durationSeconds });\n }\n\n /** Cierra TODOS los mini-services activos (orden LIFO). Para el cierre por lifecycle (background/cierre). */\n exitAllMiniServices(): void {\n for (const name of Array.from(this.activeMiniServices.keys()).reverse()) {\n this.exitMiniService(name);\n }\n }\n\n /**\n * Olvida la sesión en curso SIN emitir nada: vacía el buffer y los mini-services activos\n * (sin `mini_service_exit`). Para una sesión que el backend ya cerró por inactividad: lo\n * que quedara pendiente pertenecía a ella y no debe llegar con la sesión nueva.\n */\n discardSession(identity: AnalyticsIdentity): void {\n this.events = [];\n this.activeMiniServices.clear();\n try {\n void this.sink({ ...this.buildPayload(identity), events: [] }, { resetSession: true });\n } catch {\n /* el sink no debe romper el ciclo de vida */\n }\n }\n\n /**\n * Olvida lo que pertenecía al usuario anterior (user attributes + métricas) al cambiar de\n * usuario. NO toca el buffer de eventos: el cierre de sesión ya lo vació con la identidad\n * vieja, y lo que llegue después es del usuario nuevo.\n */\n resetUserScope(): void {\n this.attributes = {};\n this.metrics = {};\n }\n\n /** Registra un evento de analítica (modelo GA: nombre + params). */\n track(name: string, params?: Record<string, unknown>): void {\n const current = this.getMiniService();\n const merged: Record<string, unknown> = {\n ...(current ? { mini_service: current } : {}),\n ...(params ?? {}),\n };\n this.events.push({\n name,\n timestamp: this.now(),\n params: Object.keys(merged).length ? merged : undefined,\n });\n this.enforceBufferCap();\n if (this.events.length >= this.maxBatchSize) {\n this.onFlushNeeded?.();\n }\n }\n\n /** Mini-service actual (el más reciente aún activo) para etiquetar eventos + metadata del survey (#33). */\n getMiniService(): string | null {\n let last: string | null = null;\n for (const name of this.activeMiniServices.keys()) last = name; // última clave = más reciente\n return last;\n }\n\n /** Nº de eventos pendientes de flush. */\n pending(): number {\n return this.events.length;\n }\n\n /** Construye el envelope que se enviaría al endpoint de analytics. */\n buildPayload(identity: AnalyticsIdentity): AnalyticsEnvelope {\n return {\n publicKey: this.publicKey,\n userId: identity.userId,\n sessionId: identity.sessionId,\n context: {\n platform: this.platform,\n language: this.language,\n device: this.device,\n attributes: { ...this.attributes },\n metrics: { ...this.metrics },\n },\n events: [...this.events],\n };\n }\n\n /**\n * Envía (vía sink) el lote acumulado y vacía el buffer. No-op si no hay eventos.\n *\n * Si el sink falla de forma transitoria (promesa rechazada o excepción síncrona) el lote\n * se **re-encola** al principio del buffer y se reintenta en el siguiente flush: un error\n * de red o un 5xx no debe hacer desaparecer los eventos.\n */\n flush(identity: AnalyticsIdentity, meta?: AnalyticsFlushMeta): AnalyticsEnvelope | null {\n if (this.events.length === 0) return null;\n const batch = this.events;\n const payload = this.buildPayload(identity);\n this.events = [];\n\n let result: void | Promise<void>;\n try {\n // sin meta se invoca con un solo argumento (compatibilidad con sinks del host)\n result = meta ? this.sink(payload, meta) : this.sink(payload);\n } catch {\n this.requeue(batch);\n return payload;\n }\n if (result && typeof (result as Promise<void>).then === 'function') {\n (result as Promise<void>).catch(() => this.requeue(batch));\n }\n return payload;\n }\n\n /**\n * Devuelve un lote fallido al buffer, delante de lo que haya llegado mientras se enviaba\n * (orden cronológico). Con el buffer lleno se descartan los eventos más antiguos.\n */\n private requeue(batch: AnalyticsEvent[]): void {\n this.events = [...batch, ...this.events];\n this.enforceBufferCap();\n }\n\n /** Techo del buffer: si el envío lleva rato fallando, se sacrifica lo más antiguo. */\n private enforceBufferCap(): void {\n const overflow = this.events.length - this.maxBufferedEvents;\n if (overflow > 0) this.events.splice(0, overflow);\n }\n}\n","/**\n * Mapeo del envelope de analytics → body de `POST /sdk/feedback`.\n *\n * La analítica se envía como un Feedback del modelo del Surveys SDK, agrupado por una\n * INTEGRACIÓN creada en la plataforma. Se manda en streaming con `completed:false`; el\n * backend agrupa los lotes por `sessionId`, que genera el SDK al abrir cada sesión. El\n * ÚLTIMO lote de una sesión (cierre de página, app a background, cambio de usuario) va con\n * `completed:true`: cierra el registro y el lote siguiente abre otro con un sessionId nuevo.\n * `feedback.finished` se queda siempre en `false` (no es la señal de cierre acordada).\n *\n * Encoding:\n * - todo va en `feedback.metadata`: contexto (user_id, session_id, platform…) + eventos\n * - cada entrada usa `value: string[]` (array de un elemento)\n * - eventos: {key: nombre_evento, value: [JSON(timestamp + params)]}\n * - identidad (user_id) → `profile` como `external-user-id`\n * - métricas del host (setMetric) → `feedback.metrics` (mismo shape que metadata, sin prefijo)\n * - `answers` vacío\n * - `text` vacío\n */\n\nimport type { AnalyticsEnvelope, AnalyticsSink } from './analytics-manager';\nimport type { KeyValueStorage } from '../tracking/tracking-manager';\n\nexport interface AnalyticsKeys {\n publicKey: string;\n integration: string;\n}\n\nexport interface FeedbackKV {\n key: string;\n value: string[];\n}\n\nexport interface AnalyticsFeedbackBody {\n feedback: {\n text: string;\n answers: FeedbackKV[];\n metrics: FeedbackKV[];\n metadata: FeedbackKV[];\n profile: FeedbackKV[];\n finished: boolean;\n };\n publicKey: string;\n integration: string;\n completed: boolean;\n finished?: boolean;\n /** sessionId de la sesión (lo genera el SDK): el backend agrupa todos sus lotes en un solo registro. */\n sessionId?: string;\n}\n\n/** Inserta un par key/value en la lista solo si el valor está definido y no es vacío. */\nfunction pushKV(list: FeedbackKV[], key: string, value: unknown): void {\n if (value === undefined || value === null || value === '') return;\n list.push({ key, value: [String(value)] });\n}\n\nexport interface BuildBodyOptions {\n /**\n * `true` en el último lote de la sesión → `completed:true` (cierra el registro en backend).\n * El body lleva el `sessionId` de la sesión que se cierra; el lote siguiente abre otro\n * registro con un sessionId nuevo.\n */\n sessionEnd?: boolean;\n}\n\nexport function buildAnalyticsFeedbackBody(\n envelope: AnalyticsEnvelope,\n keys: AnalyticsKeys,\n feedbackSessionId?: string,\n options?: BuildBodyOptions,\n): AnalyticsFeedbackBody {\n const { context } = envelope;\n\n const profile: FeedbackKV[] = [];\n pushKV(profile, 'external-user-id', envelope.userId);\n\n // Todo va en metadata: contexto del sistema (prefijo deepdots_) + atributos de usuario + eventos\n const metadata: FeedbackKV[] = [];\n pushKV(metadata, 'deepdots_user_id', envelope.userId);\n pushKV(metadata, 'deepdots_session_id', envelope.sessionId);\n pushKV(metadata, 'deepdots_platform', context.platform);\n pushKV(metadata, 'deepdots_language', context.language);\n if (context.device) {\n const d = context.device;\n pushKV(metadata, 'deepdots_device_type', d.device_type);\n pushKV(metadata, 'deepdots_os_version', d.os_version);\n pushKV(metadata, 'deepdots_device_model', d.device_model);\n pushKV(metadata, 'deepdots_app_version', d.app_version);\n pushKV(metadata, 'deepdots_user_agent', d.user_agent);\n pushKV(metadata, 'deepdots_timezone', d.timezone);\n pushKV(metadata, 'deepdots_referrer', d.referrer);\n pushKV(metadata, 'deepdots_viewport_size', d.viewport_size);\n pushKV(metadata, 'deepdots_screen_resolution', d.screen_resolution);\n pushKV(metadata, 'deepdots_pixel_ratio', d.pixel_ratio);\n pushKV(metadata, 'deepdots_entry_type', d.entry_type);\n pushKV(metadata, 'deepdots_page_load_ms', d.page_load_ms);\n pushKV(metadata, 'deepdots_connection_type', d.connection_type);\n pushKV(metadata, 'deepdots_country', d.country);\n pushKV(metadata, 'deepdots_city', d.city);\n }\n for (const [k, v] of Object.entries(context.attributes ?? {})) {\n pushKV(metadata, k, v);\n }\n for (const e of envelope.events) {\n metadata.push({\n key: e.name,\n value: [JSON.stringify({ timestamp: e.timestamp, ...(e.params ?? {}) })],\n });\n }\n\n // Métricas del host → campo dedicado `feedback.metrics` (mismo shape que metadata, sin prefijo).\n const metrics: FeedbackKV[] = [];\n for (const [k, v] of Object.entries(context.metrics ?? {})) {\n pushKV(metrics, k, v);\n }\n\n return {\n feedback: {\n text: '',\n answers: [],\n metrics,\n metadata,\n profile,\n finished: false,\n },\n publicKey: keys.publicKey,\n integration: keys.integration,\n // Único marcador de cierre acordado con backend: el último lote de la sesión.\n completed: options?.sessionEnd === true,\n ...(feedbackSessionId ? { sessionId: feedbackSessionId } : {}),\n };\n}\n\nexport interface FeedbackSinkOptions {\n /** Base URL del backend (api(.|-dev.)deepdots.com) ya resuelta por el entorno. */\n baseUrl: string;\n keys: AnalyticsKeys;\n /** Logger opcional (gated por debug). */\n log?: (...args: unknown[]) => void;\n /** fetch inyectable (tests / RN). */\n fetchImpl?: typeof fetch;\n /**\n * @deprecated Se ignora. El flush final salía por `sendBeacon`, que va SIEMPRE con\n * credenciales; contra la API (`Access-Control-Allow-Origin: *` + `Allow-Credentials:\n * true`) el preflight fallaba y el lote de cierre, el único con `completed:true`, no\n * llegaba nunca. Ahora sale por `fetch` con `keepalive`. Se mantiene en el tipo para no\n * romper a quien lo pase.\n */\n sendBeaconImpl?: (url: string, body: Blob | string) => boolean;\n /**\n * Registro en el que seguir desde el primer lote: el de una sesión suspendida por un\n * `pagehide` y reanudada tras recargar la página (ver `suspended-session.ts`).\n */\n initialSessionId?: string;\n /**\n * Genera el sessionId de cada sesión (default: UUID v4). Inyectable para tests.\n */\n newSessionId?: () => string;\n /**\n * Persistencia de los lotes pendientes (React Native: MMKV). Con él, el lote de cierre se\n * guarda antes de enviarse y cualquier lote que falle sobrevive a que la app muera: el sink\n * del arranque siguiente lo reenvía a su registro. Sin él, los pendientes viven en memoria.\n */\n storage?: KeyValueStorage;\n /** Reloj inyectable (tests). */\n now?: () => number;\n /** Callback invocado cuando la API acepta el primer lote de una sesión (registro abierto). */\n onSessionId?: (id: string) => void;\n /** Callback invocado al cerrar la sesión (`completed:true`): el sessionId cacheado se olvida. */\n onSessionReset?: () => void;\n /**\n * Callback invocado cuando el backend acepta el lote de cierre, con el sessionId del\n * registro cerrado. Ese registro se convierte en Feedback en segundos.\n */\n onSessionClosed?: (id: string) => void;\n}\n\n/** Límite práctico de `fetch({keepalive:true})` (~64KB en Chrome). */\nconst KEEPALIVE_MAX_BYTES = 60_000;\n\n/** Un fallo transitorio merece reintento; un 4xx (payload/claves/Contact) no. */\nfunction isRetryableStatus(status: number): boolean {\n return status >= 500 || status === 408 || status === 429;\n}\n\nasync function safeBodyText(res: Response): Promise<string> {\n try {\n return (await res.text()).slice(0, 500);\n } catch {\n return '';\n }\n}\n\n/** Un lote ya construido a la espera de entregarse. */\ninterface PendingBatch {\n body: AnalyticsFeedbackBody;\n /** Cuándo se construyó (ms epoch): los de más de PENDING_MAX_AGE_MS se descartan al cargar. */\n at: number;\n}\n\n/** Techo de lotes pendientes; al superarlo se descartan los más antiguos. */\nconst PENDING_MAX = 50;\n/** Un pendiente más viejo ya no se reenvía: el backend cerró ese registro hace mucho. */\nconst PENDING_MAX_AGE_MS = 24 * 60 * 60 * 1000;\n/**\n * Un POST que no responde en este tiempo se da por fallido y se reintenta: un `fetch` sin\n * respuesta (la app pasó a background con él en vuelo) no puede retener el resto de envíos.\n */\nconst POST_TIMEOUT_MS = 20_000;\n\n/** Clave de los pendientes en el storage, una por integración. */\nfunction pendingKey(integration: string): string {\n return `__deepdots_analytics_pending__:${integration}`;\n}\n\n/** UUID v4. `crypto.randomUUID` no existe en todos los runtimes (Hermes, navegadores viejos). */\nexport function generateSessionId(): string {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (typeof c?.randomUUID === 'function') return c.randomUUID();\n const b = new Uint8Array(16);\n if (typeof c?.getRandomValues === 'function') c.getRandomValues(b);\n else for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);\n b[6] = (b[6] & 0x0f) | 0x40;\n b[8] = (b[8] & 0x3f) | 0x80;\n const h = Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;\n}\n\n/**\n * Sink real: transforma el envelope y hace `POST {baseUrl}/sdk/feedback`.\n *\n * El `sessionId` de cada sesión lo genera el SDK (UUID) y va en TODOS sus lotes, desde el\n * primero. La API guarda cada lote bajo el sessionId que recibe, así que una sesión es un\n * registro aunque no llegue ninguna respuesta. Antes lo asignaba la API en la respuesta del\n * primer POST: si esa respuesta se perdía (la app pasaba a background con el POST en vuelo),\n * el resto de la sesión acababa en otro registro.\n *\n * Garantías de entrega:\n * - un lote que falla de forma transitoria (red, 5xx, 408, 429, sin respuesta en\n * POST_TIMEOUT_MS) se guarda TAL CUAL —con su sessionId y su `completed`— y se reenvía\n * antes del lote siguiente. Nunca vuelve al buffer del manager: allí se mezclaba con la\n * sesión siguiente y viajaba con su registro. La promesa del sink no rechaza;\n * - con `storage` (React Native) los pendientes sobreviven a que la app muera: el lote de\n * cierre se guarda ANTES de enviarse y el sink del arranque siguiente lo reenvía. La API\n * descarta los eventos repetidos de una misma sesión al ensamblarla;\n * - `keepalive` para que el navegador no cancele el POST al navegar fuera ni al cerrar la\n * página (sin `sendBeacon`: ver `sendBeaconImpl`);\n * - sin credenciales, que es lo único que pasa el CORS de la API (`Allow-Origin: *`);\n * - un 4xx se loguea y se descarta.\n */\nexport function createFeedbackSink(options: FeedbackSinkOptions): AnalyticsSink {\n const url = `${options.baseUrl}/sdk/feedback`;\n const newSessionId = options.newSessionId ?? generateSessionId;\n const now = options.now ?? (() => Date.now());\n const storageKey = pendingKey(options.keys.integration);\n\n /** Registro de la sesión en curso; se crea con el primer lote y se olvida al cerrarla. */\n let feedbackSessionId: string | undefined = options.initialSessionId || undefined;\n /** Sesión ya avisada como abierta (`onSessionId`). La reanudada la avisa el host. */\n let announcedId: string | undefined = feedbackSessionId;\n\n const pending: PendingBatch[] = loadPending();\n let draining: Promise<void> | null = null;\n\n function loadPending(): PendingBatch[] {\n if (!options.storage) return [];\n try {\n const raw = options.storage.getItem(storageKey);\n const list = raw ? (JSON.parse(raw) as PendingBatch[]) : [];\n return Array.isArray(list) ? list.filter((p) => p?.body && now() - p.at <= PENDING_MAX_AGE_MS) : [];\n } catch {\n return [];\n }\n }\n\n function savePending(): void {\n if (!options.storage) return;\n try {\n if (pending.length) options.storage.setItem(storageKey, JSON.stringify(pending));\n else options.storage.removeItem(storageKey);\n } catch {\n /* sin storage los pendientes viven solo en memoria */\n }\n }\n\n function addPending(batch: PendingBatch): void {\n pending.push(batch);\n if (pending.length > PENDING_MAX) pending.splice(0, pending.length - PENDING_MAX);\n savePending();\n }\n\n function removePending(batch: PendingBatch): void {\n const i = pending.indexOf(batch);\n if (i >= 0) pending.splice(i, 1);\n savePending();\n }\n\n /** Un POST. 'retry' = fallo transitorio; 'drop' = la API lo rechazó (4xx) o no hay fetch. */\n async function post(body: AnalyticsFeedbackBody, final: boolean): Promise<'ok' | 'retry' | 'drop'> {\n const json = JSON.stringify(body);\n const small = json.length <= KEEPALIVE_MAX_BYTES;\n\n // Sin `sendBeacon` a propósito, también en el cierre de página (ver `sendBeaconImpl`):\n // `keepalive` sobrevive igual al unload y, sin credenciales, pasa el preflight de la API.\n if (final) options.log?.('[DeepdotsAnalytics] final flush via fetch keepalive');\n\n const f = options.fetchImpl ?? (typeof fetch !== 'undefined' ? fetch : undefined);\n if (!f) {\n options.log?.('[DeepdotsAnalytics] no fetch available; payload not sent', body);\n return 'drop';\n }\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n const request = f(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: json,\n // La API autentica por `publicKey` en el body y responde `Allow-Origin: *`: una\n // petición con credenciales fallaría el CORS. Explícito para no depender del default.\n credentials: 'omit',\n // keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.\n ...(small ? { keepalive: true } : {}),\n });\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new Error('timeout')), POST_TIMEOUT_MS);\n });\n const res = await Promise.race([request, timeout]);\n if (res.ok) return 'ok';\n const detail = await safeBodyText(res);\n if (isRetryableStatus(res.status)) {\n options.log?.(`[DeepdotsAnalytics] POST /sdk/feedback ${res.status} (retryable): ${detail}`);\n return 'retry';\n }\n options.log?.(\n `[DeepdotsAnalytics] POST /sdk/feedback rejected with ${res.status}; batch DISCARDED:`,\n detail,\n );\n return 'drop';\n } catch (error) {\n options.log?.('[DeepdotsAnalytics] POST /sdk/feedback failed (will retry):', error);\n return 'retry';\n } finally {\n clearTimeout(timer);\n }\n }\n\n /** Avisos al host tras entregar un lote. Un fallo del callback no afecta al envío. */\n function delivered(body: AnalyticsFeedbackBody): void {\n const id = body.sessionId;\n if (!id) return;\n try {\n if (body.completed) {\n options.onSessionClosed?.(id);\n } else if (id === feedbackSessionId && id !== announcedId) {\n // Solo la sesión EN CURSO: un lote que responde después de su cierre no la reabre.\n announcedId = id;\n options.log?.('[DeepdotsAnalytics] feedback session open:', id);\n options.onSessionId?.(id);\n }\n } catch {\n /* el callback del host no puede romper el envío */\n }\n }\n\n /** Entrega un lote y, si falla de forma transitoria, lo deja pendiente. */\n async function send(batch: PendingBatch, final: boolean, alreadyPending: boolean): Promise<void> {\n const result = await post(batch.body, final);\n if (result === 'retry') {\n if (!alreadyPending) addPending(batch);\n return;\n }\n if (alreadyPending) removePending(batch);\n if (result === 'ok') delivered(batch.body);\n }\n\n /**\n * Reenvía los pendientes en orden y para en el primero que vuelva a fallar. Cada llamada\n * encadena una pasada sobre la lista VIVA, así que un lote añadido mientras otra pasada está\n * en curso sale en la suya en vez de quedarse esperando al envío siguiente.\n */\n function drain(): Promise<void> {\n const run = async () => {\n for (let i = 0; i < pending.length; ) {\n const batch = pending[i];\n await send(batch, false, true);\n if (pending[i] === batch) return; // sigue fallando: el resto, en el próximo envío\n }\n };\n // Sin otra pasada en curso arranca YA: el primer POST sale en esta misma tarea, que en un\n // `pagehide` puede ser la última que ejecute la página.\n const next = draining ? draining.then(run) : run();\n draining = next;\n void next.finally(() => {\n if (draining === next) draining = null;\n });\n return next;\n }\n\n // Pendientes de una ejecución anterior (la app murió con ellos): se reenvían ya.\n if (pending.length) {\n savePending(); // sin los caducados\n setTimeout(() => void drain(), 0);\n }\n\n return async (envelope: AnalyticsEnvelope, meta) => {\n if (meta?.resetSession) {\n // Sesión cerrada por el backend: no se envía nada, solo se olvida el registro.\n feedbackSessionId = undefined;\n options.onSessionReset?.();\n return;\n }\n const final = meta?.final === true;\n const sessionEnd = meta?.sessionEnd === true;\n\n if (!feedbackSessionId) feedbackSessionId = newSessionId();\n const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId, { sessionEnd });\n options.log?.('[DeepdotsAnalytics] POST /sdk/feedback →', body);\n const batch: PendingBatch = { body, at: now() };\n\n if (sessionEnd) {\n // Registro cerrado con `completed:true`: el lote siguiente abre otro con un id nuevo.\n feedbackSessionId = undefined;\n options.onSessionReset?.();\n // El cierre es lo que se pierde si la app muere: se guarda ANTES de enviarlo.\n addPending(batch);\n await drain();\n return;\n }\n\n const retries = pending.length ? drain() : null;\n await Promise.all([retries, send(batch, final, false)]);\n };\n}\n","/**\n * Sesión de analytics suspendida por un `pagehide` (web).\n *\n * El navegador lanza `pagehide` igual al recargar (Cmd+R / F5) o navegar con carga completa\n * que al cerrar la pestaña, y la página no puede distinguirlo. Cerrar la sesión ahí partía\n * cada recarga en dos registros (dos feedbacks, dos mensajes en Slack). Ahora el `pagehide`\n * SUSPENDE la sesión: manda lo pendiente sin `completed` y deja aquí su `sessionId`.\n *\n * - Recarga / navegación en la misma pestaña: `init()` lo encuentra y SIGUE en ese registro.\n * - Cierre de la pestaña: `sessionStorage` muere con ella. El backend (Run_Jobs\n * `incomplete-surveys`) cierra la sesión cuando su último lote es un\n * `deepdots_session_suspend` y pasan unos minutos sin otro (`SUSPEND_GRACE_MINUTES`).\n *\n * `sessionStorage` y no `localStorage`: es por pestaña y sobrevive a la recarga, así que\n * otra pestaña nunca hereda la sesión de esta. Todo es tolerante a fallos (storage bloqueado,\n * JSON corrupto): ante cualquier error no hay sesión que reanudar y se abre una nueva.\n */\n\nimport type { KeyValueStorage } from '../tracking/tracking-manager';\n\nexport const SUSPENDED_SESSION_KEY = 'deepdots.analytics.suspended';\n\nexport interface SuspendedSession {\n /** Registro de analytics (`sessionId` de POST /sdk/feedback) que queda abierto. */\n sessionId: string;\n /** Integración de analytics: otra integración en el mismo origen no la hereda. */\n integration: string;\n /** `deepdots_user_id`: un cambio de usuario entre cargas no la hereda. */\n userId: string | null;\n /** Cuándo se suspendió (ms epoch). */\n at: number;\n}\n\n/** `window.sessionStorage` si existe y es accesible; null en RN, SSR o con storage bloqueado. */\nexport function getSessionStorage(): KeyValueStorage | null {\n try {\n return typeof window !== 'undefined' && window.sessionStorage ? window.sessionStorage : null;\n } catch {\n return null;\n }\n}\n\n/** Guarda la sesión suspendida. `false` si no se pudo: entonces hay que cerrarla como antes. */\nexport function writeSuspendedSession(storage: KeyValueStorage, session: SuspendedSession): boolean {\n try {\n storage.setItem(SUSPENDED_SESSION_KEY, JSON.stringify(session));\n return true;\n } catch {\n return false;\n }\n}\n\n/** Borra la sesión suspendida. Tolerante a fallos. */\nexport function clearSuspendedSession(storage: KeyValueStorage): void {\n try {\n storage.removeItem(SUSPENDED_SESSION_KEY);\n } catch {\n /* storage no disponible: se ignora */\n }\n}\n\n/**\n * Lee y CONSUME la sesión suspendida: el `sessionId` si es de la misma integración y usuario\n * y se suspendió hace como mucho `windowMs`; null en otro caso. La entrada se borra siempre,\n * para que una carga posterior no reanude una sesión que ya no le toca.\n */\nexport function takeSuspendedSession(\n storage: KeyValueStorage,\n match: { integration: string; userId: string | null },\n now: number,\n windowMs: number,\n): SuspendedSession | null {\n let raw: string | null = null;\n try {\n raw = storage.getItem(SUSPENDED_SESSION_KEY);\n } catch {\n return null;\n }\n if (!raw) return null;\n clearSuspendedSession(storage);\n try {\n const s = JSON.parse(raw) as Partial<SuspendedSession>;\n if (typeof s.sessionId !== 'string' || !s.sessionId) return null;\n if (typeof s.at !== 'number' || now - s.at < 0 || now - s.at > windowMs) return null;\n if (s.integration !== match.integration) return null;\n if ((s.userId ?? null) !== match.userId) return null;\n return { sessionId: s.sessionId, integration: s.integration, userId: s.userId ?? null, at: s.at };\n } catch {\n return null;\n }\n}\n","/**\n * Observador de navegación (Fase 2). Detecta cambios de pantalla y emite \"visitas\"\n * completadas (screen + entrada/salida/duración) cuando se abandona una pantalla.\n *\n * En Web se engancha a la History API (pushState/replaceState/popstate/hashchange).\n * La lógica de timing y normalización es pura y testeable sin DOM (vía `begin`/`visit`/`stop`\n * y un reloj inyectable).\n *\n * El consumidor (DeepdotsPopups) reenvía cada visita como un evento `page_view` por el\n * canal de analytics; el grafo se reconstruye en backend por orden + timestamp.\n */\n\nexport interface ScreenVisit {\n /** Pantalla normalizada (IDs → `:id`): agrupa las visitas a la misma pantalla. */\n screen: string;\n /**\n * La ruta real, sin query: `/insights/requests/2f1c…` donde `screen` dice\n * `/insights/requests/:id`. Es lo que permite enlazar la visita al elemento concreto.\n */\n path: string;\n entry: number;\n exit: number;\n durationSeconds: number;\n}\n\n/**\n * La pantalla en la que está el usuario AHORA, aún sin `page_view` (sale al abandonarla).\n * Mismos `screen`/`path` que llevará su visita y `seconds` medidos igual que su\n * `durationSeconds` (tiempo de reloj desde que se entró), pero hasta este momento.\n */\nexport interface CurrentScreen {\n screen: string;\n path: string;\n seconds: number;\n}\n\nexport type VisitListener = (visit: ScreenVisit) => void;\n\nconst UUID_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-/i;\n\n/** La ruta de una URL: path (+hash route), sin query. Sin normalizar los IDs. */\nexport function screenPath(href: string): string {\n let pathAndHash: string;\n try {\n const u = new URL(href, 'http://deepdots.local');\n pathAndHash = u.pathname + (u.hash || '');\n } catch {\n pathAndHash = href;\n }\n // quitar query (incluida la que pueda venir dentro del hash route)\n return pathAndHash.replace(/\\?[^#]*/g, '') || '/';\n}\n\n/** Normaliza una URL a un nombre de pantalla: path (+hash route), sin query, IDs → :id. */\nexport function normalizeScreen(href: string): string {\n const pathAndHash = screenPath(href);\n const collapsed = pathAndHash\n .split('/')\n .map((seg) => (/^[0-9]+$/.test(seg) || UUID_PREFIX.test(seg) ? ':id' : seg))\n .join('/');\n return collapsed || '/';\n}\n\nexport interface NavigationObserverOptions {\n now?: () => number;\n}\n\nexport class NavigationObserver {\n private now: () => number;\n private listeners = new Set<VisitListener>();\n private currentScreen: string | null = null;\n private currentPath: string | null = null;\n private entryAt = 0;\n private installed = false;\n private domHandlers: Array<() => void> = [];\n\n constructor(options: NavigationObserverOptions = {}) {\n this.now = options.now ?? (() => Date.now());\n }\n\n onVisit(listener: VisitListener): void {\n this.listeners.add(listener);\n }\n\n /** Fija la pantalla inicial (sin emitir visita; su duración se cierra al salir). */\n begin(href: string): void {\n this.currentScreen = normalizeScreen(href);\n this.currentPath = screenPath(href);\n this.entryAt = this.now();\n }\n\n /** Navega a una pantalla nueva: cierra la anterior (emite visita) y abre la nueva. */\n visit(href: string): void {\n const nextPath = screenPath(href);\n // Misma ruta (solo cambia la query) → no es una visita nueva. Se compara la ruta y no\n // la pantalla normalizada: pasar de una research a otra (`/insights/requests/A` → `/B`)\n // sí es otra visita, aunque las dos sean `/insights/requests/:id`.\n if (this.currentScreen != null && nextPath === this.currentPath) return;\n this.closeCurrent();\n this.currentScreen = normalizeScreen(href);\n this.currentPath = nextPath;\n this.entryAt = this.now();\n }\n\n /** Cierra la pantalla actual emitiendo su visita (p. ej. al cerrar la app/pestaña). */\n stop(): void {\n this.closeCurrent();\n this.currentScreen = null;\n this.currentPath = null;\n }\n\n /**\n * Olvida la pantalla actual SIN emitir su visita. Para una sesión que el backend ya dio\n * por cerrada: su visita no tiene dónde ir, y mandarla con la sesión nueva la contaría\n * donde no ocurrió. Después, `restart()` vuelve a empezar en la pantalla actual.\n */\n discard(): void {\n this.currentScreen = null;\n this.currentPath = null;\n }\n\n /**\n * Retoma el seguimiento en la pantalla actual tras un `stop()`, cuando se abre una sesión\n * nueva sin navegar (cambio de usuario, vuelta de la bfcache). No-op si nunca se instaló\n * (navegación manual por `setScreen`, RN) o si ya hay pantalla en curso.\n */\n restart(): void {\n if (!this.installed || this.currentScreen != null || typeof window === 'undefined') return;\n this.begin(window.location.href);\n }\n\n /**\n * La pantalla en curso, sin cerrarla ni emitir nada; null si no hay (antes de `begin`, tras\n * `stop`/`discard`). Para que el `user_engagement` lleve la pantalla: si el cierre de la\n * sesión no llega nunca (pestaña dormida y descartada, app matada), la última pantalla no\n * tiene `page_view` y solo queda esto.\n */\n current(): CurrentScreen | null {\n if (this.currentScreen == null) return null;\n return {\n screen: this.currentScreen,\n path: this.currentPath ?? this.currentScreen,\n seconds: this.secondsSinceEntry(this.now()),\n };\n }\n\n private secondsSinceEntry(at: number): number {\n return Math.max(0, Math.round((at - this.entryAt) / 1000));\n }\n\n private closeCurrent(): void {\n if (this.currentScreen == null) return;\n const exit = this.now();\n const visit: ScreenVisit = {\n screen: this.currentScreen,\n path: this.currentPath ?? this.currentScreen,\n entry: this.entryAt,\n exit,\n durationSeconds: this.secondsSinceEntry(exit),\n };\n this.listeners.forEach((l) => {\n try {\n l(visit);\n } catch {\n /* listener no debe romper la navegación */\n }\n });\n }\n\n /** Instala los hooks de History en el navegador y registra la pantalla actual. */\n install(): void {\n if (this.installed || typeof window === 'undefined' || typeof history === 'undefined') return;\n this.installed = true;\n\n const handle = () => this.visit(window.location.href);\n\n const originalPush = history.pushState.bind(history);\n const originalReplace = history.replaceState.bind(history);\n history.pushState = (data: unknown, unused: string, url?: string | URL | null) => {\n const r = originalPush(data as never, unused, url as never);\n handle();\n return r;\n };\n history.replaceState = (data: unknown, unused: string, url?: string | URL | null) => {\n const r = originalReplace(data as never, unused, url as never);\n handle();\n return r;\n };\n window.addEventListener('popstate', handle);\n window.addEventListener('hashchange', handle);\n\n this.domHandlers.push(() => {\n history.pushState = originalPush;\n history.replaceState = originalReplace;\n window.removeEventListener('popstate', handle);\n window.removeEventListener('hashchange', handle);\n });\n\n this.begin(window.location.href);\n }\n\n uninstall(): void {\n this.domHandlers.forEach((off) => off());\n this.domHandlers = [];\n this.installed = false;\n }\n}\n","/**\n * Device info para el context de analytics (Technology #11–13).\n *\n * En Web enviamos `device_type` + `user_agent` crudo y dejamos que el backend\n * derive OS/navegador del UA (como hace GA). Los campos adicionales (timezone,\n * referrer, viewport, etc.) se recogen de las APIs del navegador sin permisos.\n * `country`/`city` se rellenan de forma asíncrona (ver geo-info.ts).\n * En KMP las APIs nativas dan os_version/device_model reales.\n */\n\nexport interface DeviceInfo {\n device_type: 'mobile' | 'tablet' | 'desktop';\n os_version?: string;\n device_model?: string;\n app_version?: string;\n user_agent?: string;\n timezone?: string;\n referrer?: string;\n viewport_size?: string;\n screen_resolution?: string;\n pixel_ratio?: number;\n entry_type?: string;\n page_load_ms?: number;\n connection_type?: string;\n country?: string;\n city?: string;\n}\n\nexport function parseDeviceType(ua: string): 'mobile' | 'tablet' | 'desktop' {\n const s = (ua || '').toLowerCase();\n if (/ipad|tablet|playbook|silk/.test(s) || (/android/.test(s) && !/mobile/.test(s))) {\n return 'tablet';\n }\n if (/mobi|iphone|ipod|windows phone/.test(s) || (/android/.test(s) && /mobile/.test(s))) {\n return 'mobile';\n }\n return 'desktop';\n}\n\nfunction getEntryType(): string | undefined {\n if (typeof performance === 'undefined') return undefined;\n const entries = performance.getEntriesByType('navigation');\n if (!entries.length) return undefined;\n return (entries[0] as PerformanceNavigationTiming).type;\n}\n\nfunction getPageLoadMs(): number | undefined {\n if (typeof performance === 'undefined') return undefined;\n const entries = performance.getEntriesByType('navigation');\n if (!entries.length) return undefined;\n const load = (entries[0] as PerformanceNavigationTiming).loadEventEnd;\n return load > 0 ? Math.round(load) : undefined;\n}\n\nfunction getConnectionType(): string | undefined {\n if (typeof navigator === 'undefined') return undefined;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (navigator as any).connection?.effectiveType ?? undefined;\n}\n\nexport function collectDeviceInfo(appVersion?: string): DeviceInfo {\n const ua = typeof navigator !== 'undefined' ? navigator.userAgent : '';\n return {\n device_type: parseDeviceType(ua),\n user_agent: ua || undefined,\n app_version: appVersion,\n timezone: typeof Intl !== 'undefined' ? Intl.DateTimeFormat().resolvedOptions().timeZone : undefined,\n referrer: typeof document !== 'undefined' && document.referrer ? document.referrer : undefined,\n viewport_size:\n typeof window !== 'undefined' ? `${window.innerWidth}x${window.innerHeight}` : undefined,\n screen_resolution:\n typeof screen !== 'undefined' ? `${screen.width}x${screen.height}` : undefined,\n pixel_ratio: typeof window !== 'undefined' ? window.devicePixelRatio : undefined,\n entry_type: getEntryType(),\n page_load_ms: getPageLoadMs(),\n connection_type: getConnectionType(),\n };\n}\n","/**\n * Geolocalización por IP (Option B: client-side) robusta.\n *\n * Robustez:\n * - CACHE persistente en `KeyValueStorage` con TTL: en arranques posteriores el país/ciudad\n * está disponible de inmediato (sin llamada, sin gap de timing) y ahorra cuota del proveedor.\n * - CADENA de proveedores con fallback: si uno está bloqueado (ad-blocker), rate-limited (429)\n * o caído, se prueba el siguiente.\n * - TIMEOUT por proveedor vía `AbortController` para no colgarse en uno lento.\n *\n * Todo es fire-and-forget y tolerante a fallos: ante cualquier error devuelve `null`.\n */\n\nimport type { KeyValueStorage } from '../tracking/tracking-manager';\n\nexport interface GeoInfo {\n country?: string;\n city?: string;\n}\n\n/** Clave de cache en storage. */\nexport const GEO_STORAGE_KEY = 'deepdots.geo';\n/** El país/ciudad casi no cambian: cache de 30 días. */\nexport const GEO_TTL_MS = 30 * 24 * 60 * 60 * 1000;\n/** Timeout por proveedor. */\nexport const GEO_TIMEOUT_MS = 3000;\n\ninterface CachedGeo extends GeoInfo {\n ts: number;\n}\n\n/** Lee el geo cacheado si sigue fresco (no supera el TTL). Tolerante a JSON corrupto. */\nexport function readCachedGeo(storage: KeyValueStorage, now: number): GeoInfo | null {\n try {\n const raw = storage.getItem(GEO_STORAGE_KEY);\n if (!raw) return null;\n const cached = JSON.parse(raw) as CachedGeo;\n if (typeof cached.ts !== 'number' || now - cached.ts > GEO_TTL_MS) return null;\n const geo: GeoInfo = {};\n if (cached.country) geo.country = cached.country;\n if (cached.city) geo.city = cached.city;\n return geo.country || geo.city ? geo : null;\n } catch {\n return null;\n }\n}\n\n/** Persiste el geo con la marca de tiempo actual. Tolerante a fallos de storage. */\nexport function writeCachedGeo(storage: KeyValueStorage, geo: GeoInfo, now: number): void {\n try {\n const payload: CachedGeo = { ...geo, ts: now };\n storage.setItem(GEO_STORAGE_KEY, JSON.stringify(payload));\n } catch {\n /* storage lleno / no disponible: se ignora */\n }\n}\n\nexport interface GeoProvider {\n url: string;\n parse: (data: Record<string, unknown>) => GeoInfo | null;\n}\n\n/** Proveedores en orden de preferencia. Todos devuelven JSON sin auth. */\nexport const DEFAULT_GEO_PROVIDERS: GeoProvider[] = [\n {\n // https://ipapi.co/json/\n url: 'https://ipapi.co/json/',\n parse: (d) => geoFrom(d.country_code, d.city),\n },\n {\n // https://ipwho.is/ (devuelve success:false con 200 en errores)\n url: 'https://ipwho.is/',\n parse: (d) => (d.success === false ? null : geoFrom(d.country_code, d.city)),\n },\n {\n // https://ipinfo.io/json (country = código ISO)\n url: 'https://ipinfo.io/json',\n parse: (d) => geoFrom(d.country, d.city),\n },\n];\n\nfunction geoFrom(country: unknown, city: unknown): GeoInfo | null {\n const c = typeof country === 'string' && country ? country : undefined;\n const t = typeof city === 'string' && city ? city : undefined;\n if (!c && !t) return null;\n return { country: c, city: t };\n}\n\nasync function fetchOne(provider: GeoProvider, f: typeof fetch, timeoutMs: number): Promise<GeoInfo | null> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n const res = await f(provider.url, { signal: controller.signal });\n if (!res.ok) return null;\n const data = (await res.json()) as Record<string, unknown>;\n return provider.parse(data);\n } catch {\n return null;\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport interface FetchGeoOptions {\n fetchImpl?: typeof fetch;\n providers?: GeoProvider[];\n timeoutMs?: number;\n}\n\n/**\n * Intenta cada proveedor en orden (con timeout) y devuelve el primer geo válido.\n * `null` si ninguno responde. No lanza.\n */\nexport async function fetchGeo(options: FetchGeoOptions = {}): Promise<GeoInfo | null> {\n const f = options.fetchImpl ?? (typeof fetch !== 'undefined' ? fetch : undefined);\n if (!f) return null;\n const providers = options.providers ?? DEFAULT_GEO_PROVIDERS;\n const timeoutMs = options.timeoutMs ?? GEO_TIMEOUT_MS;\n for (const provider of providers) {\n const geo = await fetchOne(provider, f, timeoutMs);\n if (geo) return geo;\n }\n return null;\n}\n","/**\n * Resolución del idioma para el context de analytics.\n *\n * Prioridad: idioma explícito del host > `navigator.language` (web) > locale de `Intl`\n * (fallback automático que funciona en React Native con Hermes, donde `navigator.language`\n * no existe). Si nada resuelve, devuelve `undefined` y el campo se omite del metadata.\n */\n\n/** Forma mínima de `Intl` que necesitamos (inyectable para tests / entornos sin Intl). */\nexport interface IntlLike {\n DateTimeFormat: () => { resolvedOptions: () => { locale?: string } };\n}\n\nexport interface LanguageSources {\n /** Idioma forzado por el host (init.language). Máxima prioridad. */\n explicit?: string;\n /** `navigator` global (web). En RN suele no tener `language`. */\n navigator?: { language?: string };\n /** `Intl` global. Fallback cross-platform (web + RN/Hermes). */\n intl?: IntlLike;\n}\n\nfunction clean(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? trimmed : undefined;\n}\n\nexport function resolveLanguage(sources: LanguageSources): string | undefined {\n const explicit = clean(sources.explicit);\n if (explicit) return explicit;\n\n const navLang = clean(sources.navigator?.language);\n if (navLang) return navLang;\n\n try {\n const locale = clean(sources.intl?.DateTimeFormat().resolvedOptions().locale);\n if (locale) return locale;\n } catch {\n /* Intl no disponible o falló: se ignora */\n }\n\n return undefined;\n}\n","/**\n * Mide el tiempo ACTIVO (en primer plano), estilo GA4 (engagement time).\n * Acumula mientras está \"resumed\" (pestaña visible / app en foreground) y pausa en\n * background. `consume()` devuelve los ms activos desde la última lectura y reinicia,\n * dejando el timer corriendo. El consumidor emite un evento `user_engagement` con\n * `engagement_time_msec`; el backend lo suma por sesión → \"Average Time Spent per Session\" (#8).\n */\nexport class EngagementTracker {\n private now: () => number;\n private activeMs = 0;\n private lastResumeAt: number | null = null;\n\n constructor(now: () => number = () => Date.now()) {\n this.now = now;\n }\n\n /** Reanuda el conteo (pestaña visible / app en foreground). Idempotente. */\n resume(): void {\n if (this.lastResumeAt === null) this.lastResumeAt = this.now();\n }\n\n /** Pausa el conteo (pestaña oculta / app en background), acumulando el tramo activo. */\n pause(): void {\n if (this.lastResumeAt !== null) {\n this.activeMs += this.now() - this.lastResumeAt;\n this.lastResumeAt = null;\n }\n }\n\n /** Devuelve los ms activos acumulados desde la última lectura y reinicia (sin parar el timer). */\n consume(): number {\n if (this.lastResumeAt !== null) {\n const t = this.now();\n this.activeMs += t - this.lastResumeAt;\n this.lastResumeAt = t;\n }\n const ms = this.activeMs;\n this.activeMs = 0;\n return ms;\n }\n\n isActive(): boolean {\n return this.lastResumeAt !== null;\n }\n}\n","import type { KeyValueStorage } from '../tracking/tracking-manager';\nimport { STORAGE_KEYS } from '../tracking/tracking-manager';\n\n/** Valor de un atributo de contact (info interna del usuario que solo conoce el host). */\nexport type ContactAttributeValue = string | number | boolean;\nexport type ContactAttributes = Record<string, ContactAttributeValue>;\n\n/** Body de `POST /sdk/popups/contact`. */\nexport interface ContactBody {\n publicKey: string;\n userId: string;\n userAttributes: ContactAttributes;\n}\n\nexport interface ContactManagerOptions {\n storage: KeyValueStorage;\n publicKey: string;\n userId: string;\n /**\n * Envío real (inyectable para tests): hace el `POST /sdk/popups/contact`. Tiene que\n * RECHAZAR si la API no acepta el envío (sin red o respuesta no-2xx): solo así se sabe\n * que no hay que guardar la caché y que la llamada siguiente debe reintentar.\n */\n post: (body: ContactBody) => Promise<void>;\n}\n\n/**\n * Serialización estable (claves ordenadas) para el diff contra lo último enviado. Incluye el\n * `userId`: la clave de storage es una sola por dispositivo, y sin él un segundo usuario con\n * los mismos atributos que el anterior no llegaría a enviar los suyos.\n */\nfunction stableSerialize(userId: string, attrs: ContactAttributes): string {\n const sorted: ContactAttributes = {};\n for (const key of Object.keys(attrs).sort()) {\n sorted[key] = attrs[key];\n }\n return JSON.stringify({ userId, attributes: sorted });\n}\n\n/**\n * Gestiona los atributos de contact del usuario identificado (`userId` del init).\n * Solo envía cuando cambian respecto a lo último persistido en storage, para ahorrar\n * peticiones (el host puede llamar `setAttributes` en cada identificación sin miedo).\n */\nexport class ContactManager {\n private readonly storage: KeyValueStorage;\n private readonly publicKey: string;\n private readonly userId: string;\n private readonly post: (body: ContactBody) => Promise<void>;\n\n constructor(options: ContactManagerOptions) {\n this.storage = options.storage;\n this.publicKey = options.publicKey;\n this.userId = options.userId;\n this.post = options.post;\n }\n\n /**\n * Envía los atributos si cambiaron respecto a lo último enviado. La caché solo se guarda\n * cuando la API acepta el envío; si falla, la llamada siguiente reintenta. Nunca rechaza.\n * @returns `true` si se envió, `false` si no hubo cambios o el envío falló.\n */\n async setAttributes(attributes: ContactAttributes): Promise<boolean> {\n const serialized = stableSerialize(this.userId, attributes);\n const last = this.storage.getItem(STORAGE_KEYS.contactAttributes);\n if (last === serialized) return false;\n\n try {\n await this.post({\n publicKey: this.publicKey,\n userId: this.userId,\n userAttributes: attributes,\n });\n } catch {\n return false;\n }\n this.storage.setItem(STORAGE_KEYS.contactAttributes, serialized);\n return true;\n }\n}\n","/**\n * Crash & error reporting (Stability #14–17) — captura de errores GESTIONADOS.\n *\n * - `reportError()`: API pública para el host (app viva) → emite `deepdots_app_crash` ya.\n * - handlers de `window` (errores no capturados): persisten a disco y se reenvían\n * en el siguiente arranque (el proceso puede morir antes del flush normal).\n *\n * El contexto de device/sesión se captura EN EL MOMENTO del crash y se guarda en el\n * record — no se lee del envelope en el replay (el replay puede ocurrir en otra\n * app_version/OS). Las dims OS/device en Web suelen ir vacías (se derivan del UA en backend).\n */\n\nimport type { KeyValueStorage } from '../tracking/tracking-manager';\n\nexport type CrashSeverity = 'fatal' | 'error' | 'warning';\n\nexport interface CrashRecord {\n crashedAt: number;\n crashType: string;\n message: string;\n stack: string;\n fatal: boolean;\n handled: boolean;\n severity: CrashSeverity;\n sessionId: string | null;\n appVersion?: string;\n osVersion?: string;\n deviceModel?: string;\n context?: Record<string, string>;\n}\n\nexport interface ReportErrorOptions {\n severity?: CrashSeverity;\n handled?: boolean;\n context?: Record<string, unknown>;\n}\n\nexport interface DeviceSnapshot {\n appVersion?: string;\n osVersion?: string;\n deviceModel?: string;\n}\n\n/** Forma mínima de `global.ErrorUtils` de React Native. */\nexport interface ReactNativeErrorUtils {\n getGlobalHandler?(): ((error: unknown, isFatal?: boolean) => void) | undefined;\n setGlobalHandler(handler: (error: unknown, isFatal?: boolean) => void): void;\n}\n\nexport interface CrashReporterOptions {\n storage: KeyValueStorage;\n /** Emite un evento deepdots_app_crash AHORA (app viva). */\n emit: (params: Record<string, unknown>) => void;\n /** Snapshot de device en el momento del crash. */\n device: () => DeviceSnapshot;\n /** session_id en el momento del crash. */\n sessionId: () => string | null;\n now?: () => number;\n /** Kill-switch de consentimiento. */\n enabled?: () => boolean;\n}\n\nconst STACK_MAX = 8000;\nconst QUEUE_KEY = 'deepdots.crash.queue';\nconst MAX_QUEUED = 20;\n\n/** Convierte un CrashRecord en los params del evento `deepdots_app_crash` (omite undefined). */\nexport function crashRecordToParams(r: CrashRecord): Record<string, unknown> {\n const params: Record<string, unknown> = {\n crashed_at: r.crashedAt,\n crash_type: r.crashType,\n message: r.message,\n stack: r.stack,\n fatal: r.fatal,\n handled: r.handled,\n severity: r.severity,\n };\n if (r.sessionId) params.crashed_session_id = r.sessionId;\n if (r.appVersion) params.crashed_app_version = r.appVersion;\n if (r.osVersion) params.crashed_os_version = r.osVersion;\n if (r.deviceModel) params.crashed_device_model = r.deviceModel;\n if (r.context) {\n for (const [k, v] of Object.entries(r.context)) params[`ctx_${k}`] = v;\n }\n return params;\n}\n\nexport class CrashReporter {\n private options: CrashReporterOptions;\n private now: () => number;\n /** Los handlers de `window` ya están puestos: `install()` es idempotente. */\n private windowInstalled = false;\n /** ErrorUtils ya enganchados: engancharlos dos veces capturaría cada error dos veces. */\n private hookedErrorUtils = new Set<ReactNativeErrorUtils>();\n\n constructor(options: CrashReporterOptions) {\n this.options = options;\n this.now = options.now ?? (() => Date.now());\n }\n\n private isEnabled(): boolean {\n return this.options.enabled ? this.options.enabled() : true;\n }\n\n /** Construye un CrashRecord capturando contexto en el momento del crash. */\n private buildRecord(\n error: unknown,\n severity: CrashSeverity,\n handled: boolean,\n fatal: boolean,\n context?: Record<string, unknown>,\n ): CrashRecord {\n const err = error as { name?: string; message?: string; stack?: string };\n const isErr = error instanceof Error;\n const dev = this.options.device();\n const ctx = context\n ? Object.fromEntries(Object.entries(context).map(([k, v]) => [k, String(v)]))\n : undefined;\n return {\n crashedAt: this.now(),\n crashType: (isErr && err.name) || 'Error',\n message: isErr ? String(err.message ?? '') : String(error),\n stack: (isErr && typeof err.stack === 'string' ? err.stack : '').slice(0, STACK_MAX),\n fatal,\n handled,\n severity,\n sessionId: this.options.sessionId(),\n appVersion: dev.appVersion,\n osVersion: dev.osVersion,\n deviceModel: dev.deviceModel,\n context: ctx,\n };\n }\n\n /** API pública del host: reporta un error (app viva) → emite el evento ya. */\n reportError(error: unknown, options: ReportErrorOptions = {}): void {\n if (!this.isEnabled()) return;\n const severity = options.severity ?? 'error';\n const handled = options.handled ?? true;\n const fatal = severity === 'fatal';\n const record = this.buildRecord(error, severity, handled, fatal, options.context);\n this.options.emit(crashRecordToParams(record));\n }\n\n /** Captura un error no manejado (fatal) → persiste a disco para replay en el siguiente arranque. */\n captureUnhandled(error: unknown): void {\n this.persist(this.buildRecord(error, 'fatal', false, true));\n }\n\n /** Persiste un crash no capturado a disco para reenviarlo en el siguiente arranque. */\n private persist(record: CrashRecord): void {\n let queue: CrashRecord[] = [];\n try {\n const raw = this.options.storage.getItem(QUEUE_KEY);\n if (raw) queue = JSON.parse(raw) as CrashRecord[];\n if (!Array.isArray(queue)) queue = [];\n } catch {\n queue = [];\n }\n queue.push(record);\n if (queue.length > MAX_QUEUED) queue = queue.slice(queue.length - MAX_QUEUED);\n try {\n this.options.storage.setItem(QUEUE_KEY, JSON.stringify(queue));\n } catch {\n /* storage lleno / no disponible — se pierde el crash, no rompemos la app */\n }\n }\n\n /** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */\n drainPendingCrashes(): CrashRecord[] {\n let queue: CrashRecord[] = [];\n try {\n const raw = this.options.storage.getItem(QUEUE_KEY);\n if (raw) queue = JSON.parse(raw) as CrashRecord[];\n if (!Array.isArray(queue)) queue = [];\n } catch {\n queue = [];\n }\n if (queue.length) {\n try { this.options.storage.removeItem(QUEUE_KEY); } catch { /* noop */ }\n }\n return queue;\n }\n\n /** Instala los handlers globales de errores no capturados (no-op sin `window`). Idempotente. */\n install(): void {\n if (this.windowInstalled) return;\n // En React Native `window` existe pero NO tiene addEventListener; comprobamos\n // la capacidad, no solo la existencia de `window`. En RN el host usa\n // installReactNative(ErrorUtils) para capturar los errores no manejados.\n if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') return;\n this.windowInstalled = true;\n window.addEventListener('error', (e: ErrorEvent) => {\n if (!this.isEnabled()) return;\n this.captureUnhandled(e.error ?? e.message ?? 'error');\n });\n window.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => {\n if (!this.isEnabled()) return;\n this.captureUnhandled(e.reason ?? 'unhandledrejection');\n });\n }\n\n /** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. Idempotente por instancia de ErrorUtils. */\n installReactNative(errorUtils: ReactNativeErrorUtils): void {\n if (this.hookedErrorUtils.has(errorUtils)) return;\n this.hookedErrorUtils.add(errorUtils);\n const previous = errorUtils.getGlobalHandler?.();\n errorUtils.setGlobalHandler((error: unknown, isFatal?: boolean) => {\n if (this.isEnabled()) this.captureUnhandled(error);\n previous?.(error, isFatal);\n });\n }\n\n /** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */\n _persistForTest(error: unknown): void {\n this.captureUnhandled(error);\n }\n}\n","/**\n * Persistencia del estado que alimenta las reglas de redisplay (cooldown) de los popups.\n *\n * Hasta la 1.7.0 `lastShown`/`surveyProgress` vivían SOLO en memoria, así que cualquier\n * recarga de página reiniciaba el cooldown y el popup volvía a salir aunque su regla\n * dijese \"no antes de N días\". El SDK nativo (KMP) sí lo persistía (`getLastShown`/\n * `setLastShown` contra el `KeyValueStorage`); esto cierra esa divergencia en Web.\n *\n * ⚠️ El alcance es POR NAVEGADOR: otro dispositivo, otro perfil o un borrado de datos\n * vuelven a empezar. El cooldown por usuario de verdad exige que el backend filtre el\n * `GET /sdk/{publicKey}/popups` con el historial que ya recibe en `POST /sdk/popups`.\n */\nimport type { KeyValueStorage } from '../tracking/tracking-manager';\nimport { POPUP_TRIGGER_CONDITION_STATUSES } from '../types';\nimport type { PopupTriggerConditionStatus } from '../types';\n\n/** Clave única en el namespace `deepdots.*` (mismo que `STORAGE_KEYS`). */\nexport const POPUP_STATE_STORAGE_KEY = 'deepdots.popups.state';\n\n/**\n * Techo de entradas por mapa, con desalojo del más ANTIGUO (mismo trade-off explícito\n * que `MAX_TRACKED_MESSAGES` en `MessageGuard`). Una cuenta real tiene unos pocos popups;\n * el techo solo evita que el registro crezca sin límite si se rotan definiciones.\n * No se poda por antigüedad a propósito: un `cooldownDays` largo debe seguir valiendo.\n */\nexport const MAX_TRACKED_POPUP_ENTRIES = 200;\n\nexport interface PersistedProgressEntry {\n status: PopupTriggerConditionStatus;\n timestamp: number;\n}\n\nexport interface PersistedPopupState {\n /** popupId → timestamp de la última vez que se mostró. */\n lastShown: Record<string, number>;\n /** surveyId → último estado conocido (PARTIAL/COMPLETED) y cuándo. */\n progress: Record<string, PersistedProgressEntry>;\n}\n\nfunction emptyState(): PersistedPopupState {\n return { lastShown: {}, progress: {} };\n}\n\nfunction isValidTimestamp(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value);\n}\n\n/** Se queda con las `MAX_TRACKED_POPUP_ENTRIES` entradas de timestamp más reciente. */\nfunction capEntries<T>(entries: Array<[string, T]>, timestampOf: (value: T) => number): Array<[string, T]> {\n if (entries.length <= MAX_TRACKED_POPUP_ENTRIES) return entries;\n return [...entries]\n .sort((a, b) => timestampOf(b[1]) - timestampOf(a[1]))\n .slice(0, MAX_TRACKED_POPUP_ENTRIES);\n}\n\n/**\n * Lee el estado persistido. Tolerante a todo: sin clave, JSON corrupto, entradas con\n * forma inválida o un storage que lanza (Safari en privado, cookies bloqueadas) →\n * estado vacío o entradas buenas, nunca una excepción.\n */\nexport function readPopupState(storage: KeyValueStorage): PersistedPopupState {\n let raw: string | null = null;\n try {\n raw = storage.getItem(POPUP_STATE_STORAGE_KEY);\n } catch {\n return emptyState();\n }\n if (!raw) return emptyState();\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return emptyState();\n }\n if (typeof parsed !== 'object' || parsed === null) return emptyState();\n\n const source = parsed as Partial<Record<keyof PersistedPopupState, unknown>>;\n const state = emptyState();\n\n if (typeof source.lastShown === 'object' && source.lastShown !== null) {\n const valid = Object.entries(source.lastShown as Record<string, unknown>)\n .filter((entry): entry is [string, number] => isValidTimestamp(entry[1]));\n capEntries(valid, (timestamp) => timestamp).forEach(([popupId, timestamp]) => {\n state.lastShown[popupId] = timestamp;\n });\n }\n\n if (typeof source.progress === 'object' && source.progress !== null) {\n const valid = Object.entries(source.progress as Record<string, unknown>)\n .filter((entry): entry is [string, PersistedProgressEntry] => {\n const value = entry[1];\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Partial<PersistedProgressEntry>;\n return (\n POPUP_TRIGGER_CONDITION_STATUSES.includes(candidate.status as PopupTriggerConditionStatus)\n && isValidTimestamp(candidate.timestamp)\n );\n });\n capEntries(valid, (entry) => entry.timestamp).forEach(([surveyId, entry]) => {\n state.progress[surveyId] = { status: entry.status, timestamp: entry.timestamp };\n });\n }\n\n return state;\n}\n\n/** Escribe el estado (acotado). Un storage que lanza se ignora en silencio. */\nexport function writePopupState(storage: KeyValueStorage, state: PersistedPopupState): void {\n const capped: PersistedPopupState = emptyState();\n capEntries(Object.entries(state.lastShown), (timestamp) => timestamp).forEach(([popupId, timestamp]) => {\n capped.lastShown[popupId] = timestamp;\n });\n capEntries(Object.entries(state.progress), (entry) => entry.timestamp).forEach(([surveyId, entry]) => {\n capped.progress[surveyId] = entry;\n });\n\n try {\n storage.setItem(POPUP_STATE_STORAGE_KEY, JSON.stringify(capped));\n } catch {\n // Storage lleno o bloqueado por el navegador: el cooldown degrada a solo-memoria.\n }\n}\n\n/** Borra el registro (cambio de usuario: el historial era del anterior). */\nexport function clearPopupState(storage: KeyValueStorage): void {\n try {\n storage.removeItem(POPUP_STATE_STORAGE_KEY);\n } catch {\n // idem\n }\n}\n","/**\n * Messaging (#18–22): tracking de notificaciones del host (push / in-app).\n * Un único evento reservado `deepdots_message` con un campo `stage` discriminador;\n * el funnel se correlaciona por `message_id` y se agrupa por `message_title`.\n */\n\nexport type MessageStage = 'delivered' | 'clicked' | 'converted';\n\n/** Canales válidos. Un mensaje tiene UN canal: se valida en runtime porque el host puede no ser TS. */\nexport const MESSAGE_CHANNELS = ['push', 'in_app'] as const;\nexport type MessageChannel = (typeof MESSAGE_CHANNELS)[number];\n\nexport interface TrackMessageOptions {\n /** Identificador del mensaje/notificación — correlaciona el funnel delivered→clicked→converted. */\n id: string;\n /** Título del mensaje — dimensión de agrupación de #18–22. */\n title: string;\n /** Canal de entrega. */\n channel: MessageChannel;\n /** Nombre de la campaña (opcional). */\n campaign?: string;\n /** Valor de conversión (opcional, típico en `stage: 'converted'`). */\n value?: number;\n currency?: string;\n /** Parámetros libres adicionales. */\n params?: Record<string, unknown>;\n}\n\n/** Construye los params del evento `deepdots_message` (omite opcionales ausentes). */\nexport function buildMessageParams(stage: MessageStage, o: TrackMessageOptions): Record<string, unknown> {\n const p: Record<string, unknown> = {\n stage,\n message_id: o.id,\n message_title: o.title,\n channel: o.channel,\n };\n if (o.campaign) p.campaign = o.campaign;\n if (o.value !== undefined) p.value = o.value;\n if (o.currency) p.currency = o.currency;\n return { ...p, ...(o.params ?? {}) };\n}\n\n/** Techo de `message_id` vigilados por sesión; los más antiguos se evictan (memoria acotada). */\nexport const MAX_TRACKED_MESSAGES = 500;\n\nexport type MessageRejectionReason = 'invalid_channel' | 'duplicate_stage' | 'channel_conflict';\n\nexport type MessageGuardVerdict =\n | { emit: true }\n | { emit: false; reason: MessageRejectionReason; detail: string };\n\n/**\n * Protege el funnel de Messaging de las formas imposibles que el host puede producir por error\n * (visto en producción: CTR > 100% por falta de `delivered` + el mismo `message_id` reportado en\n * dos canales). Tres reglas, todas dentro de la sesión (se reinicia en cada `init()`):\n *\n * 1. `channel` fuera de `MESSAGE_CHANNELS` → se descarta el evento.\n * 2. Un `(message_id, stage)` ya emitido → se emite una sola vez.\n * 3. Un `message_id` ya visto en un canal → se descartan los eventos de otro canal.\n *\n * Los eventos rechazados NO mutan el estado: un rechazo nunca \"consume\" el stage bueno.\n * Lo que el SDK no puede arreglar es la ausencia de `delivered`: eso lo instrumenta el host.\n */\nexport class MessageGuard {\n /** message_id → canal fijado + stages ya emitidos. El orden de inserción marca la eviction. */\n private seen = new Map<string, { channel: MessageChannel; stages: Set<MessageStage> }>();\n\n constructor(private maxTrackedMessages: number = MAX_TRACKED_MESSAGES) {}\n\n evaluate(stage: MessageStage, options: TrackMessageOptions): MessageGuardVerdict {\n const channel = options.channel;\n if (!(MESSAGE_CHANNELS as readonly string[]).includes(channel)) {\n return {\n emit: false,\n reason: 'invalid_channel',\n detail: `invalid channel \"${channel}\" (expected ${MESSAGE_CHANNELS.join(' | ')})`,\n };\n }\n\n const entry = this.seen.get(options.id);\n if (!entry) {\n this.remember(options.id, channel, stage);\n return { emit: true };\n }\n if (entry.channel !== channel) {\n return {\n emit: false,\n reason: 'channel_conflict',\n detail: `message_id \"${options.id}\" was already reported on channel \"${entry.channel}\"; discarding \"${channel}\"`,\n };\n }\n if (entry.stages.has(stage)) {\n return {\n emit: false,\n reason: 'duplicate_stage',\n detail: `stage \"${stage}\" was already sent for message_id \"${options.id}\"`,\n };\n }\n entry.stages.add(stage);\n return { emit: true };\n }\n\n /** Olvida todo lo vigilado (nueva sesión). */\n reset(): void {\n this.seen.clear();\n }\n\n private remember(id: string, channel: MessageChannel, stage: MessageStage): void {\n this.seen.set(id, { channel, stages: new Set([stage]) });\n while (this.seen.size > this.maxTrackedMessages) {\n const oldest = this.seen.keys().next();\n if (oldest.done) break;\n this.seen.delete(oldest.value);\n }\n }\n}\n","import {\n DeepdotsConfig,\n TriggerConfig,\n DeepdotsEvent,\n DeepdotsEventType,\n EventListener,\n DeepdotsInitParams,\n DeepdotsLogger,\n FeedbackSession,\n PopupDefinition,\n PopupTriggerCondition,\n PopupActions,\n PopupTrigger,\n PopupTriggerConditionStatus,\n POPUP_TRIGGER_CONDITION_STATUSES, POPUPSESSIONSTATUS,\n PopupStyle,\n SessionEndReason,\n SessionStartReason,\n} from '../types';\n// renderPopup se carga de forma PEREZOSA (dynamic import) para no arrastrar\n// `@magicfeedback/native` (browser) ni el CSS al importar el SDK en entornos sin DOM (React Native).\nimport { setupTrigger } from '../triggers';\nimport { resolveEnvironment } from '../config/env';\nimport { PopupRenderer, createDefaultRenderer } from '../platform/renderer';\nimport { TrackingManager, createDefaultStorage, type KeyValueStorage } from '../tracking/tracking-manager';\nimport { AnalyticsManager, createDryRunSink, type AnalyticsEnvelope } from '../analytics/analytics-manager';\nimport { createFeedbackSink } from '../analytics/feedback-payload';\nimport {\n clearSuspendedSession,\n getSessionStorage,\n takeSuspendedSession,\n writeSuspendedSession,\n} from '../analytics/suspended-session';\nimport { NavigationObserver } from '../tracking/navigation-observer';\nimport { collectDeviceInfo } from '../analytics/device-info';\nimport { readCachedGeo, writeCachedGeo, fetchGeo } from '../analytics/geo-info';\nimport { resolveLanguage } from '../analytics/language';\nimport { EngagementTracker } from '../analytics/engagement-tracker';\nimport { ContactManager, type ContactAttributes } from '../contact/contact-manager';\nimport { CrashReporter, crashRecordToParams, type ReportErrorOptions, type ReactNativeErrorUtils } from '../analytics/crash-reporter';\nimport { clearPopupState, readPopupState, writePopupState } from './popup-state-store';\nimport { setLogger } from '../util/logger';\nimport { buildMessageParams, MessageGuard, type MessageStage, type TrackMessageOptions } from '../analytics/messaging';\n\nconst EXIT_QUEUE_STORAGE_KEY = '__deepdots_exit_popup_queue__';\nconst DAY_IN_MS = 24 * 60 * 60 * 1000;\nconst ANALYTICS_MAX_BATCH_SIZE = 20;\nconst ANALYTICS_FLUSH_INTERVAL_MS = 30_000;\n/**\n * Latido: con la pestaña visible (o la app en foreground), el flush periódico manda el\n * engagement acumulado al menos cada ANALYTICS_HEARTBEAT_MS. Sin él, alguien leyendo una\n * sola página no enviaba nada — el page_view sale al SALIR de la pantalla y el engagement\n * al ocultar/cerrar — y el backend daba la sesión por abandonada con solo su session_start.\n */\nconst ANALYTICS_HEARTBEAT_MS = 5 * 60_000;\n/** Una pestaña que vuelve a verse tras este tiempo oculta empieza sesión nueva (timeout de sesión, como GA). */\nconst ANALYTICS_SESSION_TIMEOUT_MS = 30 * 60_000;\n/**\n * Inactividad tras la que el backend cierra una sesión por su cuenta (Run_Jobs\n * `incomplete-surveys`, `IDLE_MINUTES`, 60 min; el barrido corre cada 20). Mantener en sincronía.\n */\nconst ANALYTICS_SERVER_IDLE_MS = 60 * 60_000;\n/**\n * Pasado este tiempo oculta, la sesión se da por cerrada en el backend: reenviarle el cierre\n * la volvería a montar entera en un segundo feedback, así que se descarta sin enviar nada.\n * Margen de 10 min bajo la ventana del backend: el último lote enviado puede ser algo\n * anterior al momento en que se ocultó la pestaña.\n */\nconst ANALYTICS_SESSION_DISCARD_MS = ANALYTICS_SERVER_IDLE_MS - 10 * 60_000;\n/**\n * Una página que vuelve a cargar en la misma pestaña (recarga, navegación con carga completa,\n * bfcache) dentro de este tiempo tras su `pagehide` SIGUE en la sesión suspendida, en vez de\n * abrir otra. Muy por debajo del margen con el que el backend cierra una sesión suspendida\n * (Run_Jobs `incomplete-surveys`, `SUSPEND_GRACE_MINUTES`, 3 min): reanudar una sesión que\n * el backend ya montó la partiría en dos feedbacks. Mantener en sincronía.\n */\nconst ANALYTICS_RESUME_WINDOW_MS = 60_000;\n/** Namespace reservado para los eventos que emite el propio SDK (page_view, message, mini_service…). */\nconst RESERVED_EVENT_PREFIX = 'deepdots_';\n/** Prefijo que se antepone a los eventos custom del host para poder identificarlos frente a los reservados. */\nconst CUSTOM_EVENT_PREFIX = 'deepdots_event_';\n/** Techo del buffer de disparos recibidos mientras se cargan las definiciones (descarta los más viejos). */\nexport const MAX_PENDING_TRIGGERS = 20;\n\n/** Disparo del host recibido antes de que lleguen las definiciones de popups. */\ntype PendingTrigger =\n | { kind: 'event'; name: string }\n | { kind: 'survey'; surveyId: string; popupId?: string };\n\ninterface DeferredExitPopup {\n id: string;\n surveyId: string;\n dueAt: number;\n sourceUrl: string;\n}\n\ninterface LegacyPopupTriggerCondition {\n answered?: boolean;\n cooldownDays?: number;\n}\n\ninterface PopupProgressState {\n status: PopupTriggerConditionStatus;\n timestamp: number;\n}\n\ntype NormalizedPopupDefinition = PopupDefinition & {\n legacyConditions?: LegacyPopupTriggerCondition[];\n};\n\n/**\n * Main class for managing survey popups\n */\nexport class DeepdotsPopups {\n private config: DeepdotsConfig | null = null;\n private listeners: Map<DeepdotsEventType, Set<EventListener>> = new Map();\n private triggers: TriggerConfig[] = [];\n private initialized = false;\n private renderer: PopupRenderer = createDefaultRenderer();\n /** Logger para el output de debug. Default `console`; el host puede inyectar el suyo en init(). */\n private logger: DeepdotsLogger = console;\n private popupDefinitions: NormalizedPopupDefinition[] = [];\n private popupsLoaded = false;\n /**\n * `true` mientras el `GET /sdk/{apiKey}/popups` de `init()` está en vuelo. En esa ventana los\n * disparos del host (`triggerEvent`, `triggerSurvey`) no tienen contra qué evaluarse, así que\n * se guardan en `pendingTriggers` y se reproducen al cargar. Click, scroll, time y exit no\n * hacen falta: son listeners que `autoLaunch` instala ya con las definiciones cargadas.\n */\n private awaitingPopups = false;\n private pendingTriggers: PendingTrigger[] = [];\n private pendingAutoLaunch = false;\n\n // Estado que alimenta las reglas de redisplay. Copia de trabajo en memoria; la fuente\n // duradera es el storage (`popup-state-store`), rehidratada en `init()` — sin eso, una\n // recarga reiniciaba el cooldown y el popup volvía a salir. `answeredSurveys` se deriva\n // de las entradas COMPLETED de `surveyProgress` (los dos los escribe `markSurveyAnswered`).\n private answeredSurveys: Set<string> = new Set();\n private surveyProgress: Map<string, PopupProgressState> = new Map();\n private lastShown: Map<string, number> = new Map(); // popupId -> timestamp mostrado\n private surveyToPopupId: Map<string, string> = new Map(); // surveyId -> popupId\n private deferredExitTimers: number[] = [];\n\n private baseUrl: string = '';\n private env: 'production' | 'development' = 'production';\n /** Si el SDK pinta el \"modal\" del popup (RN). Default true; false = el host lo envuelve. */\n private renderChrome = true;\n /** Barra de progreso. undefined = la decide la plataforma (style.showProgressBar). */\n private showProgressBar: boolean | undefined = undefined;\n /** CSS del host inyectado al final de los estilos del popup. */\n private surveyCss: string | undefined = undefined;\n private cspNonce: string | undefined = undefined;\n\n /** Identidad + sesión (Fase 1 tracking). Null hasta init(). */\n private tracking: TrackingManager | null = null;\n /** Capa de analytics (canal separado del feedback). Null hasta init(). */\n private analytics: AnalyticsManager | null = null;\n /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */\n private analyticsFeedbackSessionId: string | undefined = undefined;\n /** `init({ onFeedbackSession })`: avisa al host al abrir y cerrar cada registro. */\n private onFeedbackSession: DeepdotsInitParams['onFeedbackSession'] = undefined;\n /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */\n private analyticsFlushTimer: ReturnType<typeof setInterval> | undefined = undefined;\n /** Crash & error reporting (#14–17). Null hasta init(). */\n private crashReporter: CrashReporter | null = null;\n /** ErrorUtils de RN recibido del host (o del Provider); se engancha en cuanto haya consentimiento. */\n private rnErrorUtils: ReactNativeErrorUtils | null = null;\n /** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */\n private messageGuard = new MessageGuard();\n /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */\n private navObserver: NavigationObserver | null = null;\n /** Tiempo activo (engagement time, #8). */\n private engagement: EngagementTracker | null = null;\n /** Atributos internos del usuario identificado → POST /sdk/popups/contact. Null si no hay userId. */\n private contact: ContactManager | null = null;\n /** Storage resuelto en init() (inyectado por el host o localStorage). Reusado al cambiar de usuario. */\n private storage: KeyValueStorage | null = null;\n /** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */\n private navStarted = false;\n /**\n * `true` entre `session_start` y `session_end`. Hace idempotente tanto la apertura como\n * el cierre (p. ej. `visibilitychange` + `pagehide` seguidos) y permite abrir una sesión\n * nueva al volver a foreground o al conceder el consentimiento más tarde.\n */\n private sessionOpen = false;\n /** Último `user_engagement` emitido (ms epoch): marca el latido. */\n private lastEngagementAt = 0;\n /** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */\n private hiddenAt: number | null = null;\n /**\n * Sesión suspendida por un `pagehide` (ms epoch y `deepdots_user_id`); null si no hay.\n * `openSession` la reanuda si sigue a tiempo (ver ANALYTICS_RESUME_WINDOW_MS).\n */\n private suspended: { at: number; userId: string | null } | null = null;\n /** Integración de analytics del init: la sesión suspendida solo la hereda la misma. */\n private analyticsIntegration: string | undefined = undefined;\n /**\n * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de\n * verdad para el context de analytics Y para la segmentación por idioma de los popups.\n */\n private language: string | undefined = undefined;\n /** `geolocation` del init: si es false no se usa ni la caché ni el lookup. */\n private geoEnabled = true;\n /** Solo se consulta a terceros si el país/ciudad se va a enviar (analytics configurado). */\n private geoWanted = false;\n /** Evita más de un lookup por instancia (el consentimiento puede ir y venir). */\n private geoLookupStarted = false;\n\n /** Initialize the SDK with configuration */\n init(config: DeepdotsInitParams): void {\n this.logger = config.logger ?? console;\n setLogger(this.logger); // los módulos sin `this` (renderPopup, renderers) enrutan por aquí\n if (this.initialized) {\n // Un init() con OTRO userId es un CAMBIO DE USUARIO (login/logout del host): cierra\n // la sesión anterior y cambia la identidad en caliente. Reinicializar de verdad\n // duplicaría listeners, timers y la carga de popups.\n const nextUserId = config.userId || undefined;\n if (nextUserId !== this.config?.userId) {\n this.setUserId(nextUserId);\n return;\n }\n this.log('SDK already initialized');\n return;\n }\n\n const env = resolveEnvironment(config.nodeEnv);\n\n this.baseUrl = env.apiBaseUrl;\n this.env = config.nodeEnv || 'production';\n this.renderChrome = config.renderChrome ?? true;\n this.showProgressBar = config.showProgressBar;\n this.surveyCss = config.surveyCss;\n this.cspNonce = config.cspNonce;\n this.onFeedbackSession = config.onFeedbackSession;\n\n this.config = {\n apiKey: config.apiKey || undefined,\n debug: config.debug || false,\n userId: config.userId || undefined,\n } as DeepdotsConfig;\n\n // user_id lo gestiona el SDK (persistente); el session_id lo provee el backend\n // (respuesta de POST /sdk/popups) y se cachea — el SDK no genera ni expira sesiones.\n const storage = config.storage ?? createDefaultStorage();\n this.storage = storage;\n this.loadPopupState(storage);\n this.tracking = new TrackingManager({\n storage,\n clientUserId: this.config.userId,\n enabled: config.trackingEnabled ?? true,\n });\n\n // Contact: info interna del usuario que solo conoce el host (segmentación/targeting).\n // Solo trackeamos usuarios IDENTIFICADOS → se crea únicamente si hay userId del init.\n if (this.config.userId) {\n const userId = this.config.userId;\n const publicKey = this.config.apiKey ?? '';\n this.contact = new ContactManager({\n storage,\n publicKey,\n userId,\n post: (body) => this.postContact(body),\n });\n // Atributos de contact pasados en init: envío fire-and-forget (solo si cambiaron).\n if (config.contactAttributes) {\n void this.setContactAttributes(config.contactAttributes);\n }\n }\n this.log('tracking · user_id:', this.tracking.getUserId(), '· new_user:', this.tracking.isNewUser(), '· enabled:', this.tracking.isTrackingEnabled());\n\n // Recarga en la misma pestaña: la sesión que suspendió el `pagehide` sigue abierta en el\n // backend. Se recoge aquí para que el sink siga en ese registro desde el primer lote.\n this.analyticsIntegration = config.analytics?.integration;\n const resumable = this.readResumableSession();\n if (resumable) {\n this.analyticsFeedbackSessionId = resumable.sessionId;\n this.suspended = { at: resumable.at, userId: resumable.userId };\n }\n\n // Analytics: se envía como Feedback a la integración (`POST /sdk/feedback`) si se\n // pasan claves en init.analytics; si no, queda en dry-run: no envía nada y el payload\n // solo se pinta con `debug`, para no ensuciar la consola de la web del host en producción.\n const analyticsSink = config.analytics\n ? createFeedbackSink({\n baseUrl: this.baseUrl,\n keys: config.analytics,\n log: (...a) => this.log(...a),\n initialSessionId: resumable?.sessionId,\n // En móvil (RN) la app puede morir con el lote de cierre sin entregar: se\n // persiste y se reenvía en el arranque siguiente. En web el cierre sale con\n // `keepalive` y varias pestañas compartirían el mismo storage.\n storage: (config.platform ?? 'web') !== 'web' ? storage : undefined,\n onSessionId: (id) => {\n this.analyticsFeedbackSessionId = id;\n this.notifyFeedbackSession(id, 'open');\n },\n onSessionClosed: (id) => this.notifyFeedbackSession(id, 'closed'),\n onSessionReset: () => { this.analyticsFeedbackSessionId = undefined; },\n })\n : createDryRunSink((...a) => { if (this.config?.debug) this.logger.log(...a); });\n const device = config.device ?? collectDeviceInfo(config.appVersion);\n this.language = resolveLanguage({\n explicit: config.language,\n navigator: typeof navigator !== 'undefined' ? navigator : undefined,\n intl: typeof Intl !== 'undefined' ? Intl : undefined,\n });\n this.messageGuard.reset();\n this.analytics = new AnalyticsManager({\n sink: analyticsSink,\n publicKey: config.analytics?.publicKey ?? this.config.apiKey,\n language: this.language,\n platform: config.platform ?? 'web',\n device,\n maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,\n onFlushNeeded: () => this.flushAnalytics(),\n });\n // Crash & error reporting (#14–17): captura errores no manejados (a disco, replay\n // en el siguiente arranque) y expone reportError() para el host (emite ya).\n this.crashReporter = new CrashReporter({\n storage,\n emit: (params) => this.track('deepdots_app_crash', params),\n device: () => ({ appVersion: device.app_version, osVersion: device.os_version, deviceModel: device.device_model }),\n sessionId: () => this.tracking?.getSessionId() ?? null,\n now: () => Date.now(),\n enabled: () => this.tracking?.isTrackingEnabled() ?? false,\n });\n this.installCrashHandlers();\n // Geolocalización por IP: ver `resolveGeo()` para cuándo se consulta a terceros.\n this.geoEnabled = config.geolocation ?? true;\n this.geoWanted = this.geoEnabled && !!config.analytics;\n this.resolveGeo();\n // Fase 2: navegación → eventos page_view por el canal de analytics.\n this.navObserver = new NavigationObserver();\n this.navObserver.onVisit((v) =>\n this.track('deepdots_page_view', { screen: v.screen, path: v.path, duration_seconds: v.durationSeconds })\n );\n this.navObserver.install();\n // Engagement time (#8): cuenta tiempo activo en primer plano.\n // No arranca aquí: lo hace `openSession`, y solo con la pestaña visible.\n this.engagement = new EngagementTracker();\n this.setupAnalyticsFlush();\n // Marca de inicio de sesión (base para Crash-Free Users #14), o reanudación de la que\n // suspendió la carga anterior de esta pestaña.\n this.openSession('cold_start');\n if (resumable && this.analyticsFeedbackSessionId === resumable.sessionId) {\n this.notifyFeedbackSession(resumable.sessionId, 'open');\n }\n // Drena SIEMPRE la cola (descarta los pendientes si tracking está off, spec §7);\n // solo reenvía como evento cuando el tracking está activo.\n const pendingCrashes = this.crashReporter.drainPendingCrashes();\n if (this.tracking?.isTrackingEnabled()) {\n for (const rec of pendingCrashes) {\n this.track('deepdots_app_crash', crashRecordToParams(rec));\n }\n }\n\n this.initialized = true;\n this.log('SDK initialized', this.config);\n // El contenedor del popup (`#deepdots-popup-container`) lo crea el renderer, no el core.\n if (this.renderer.init) this.renderer.init();\n\n // Los popups SIEMPRE se reciben de la API (no se definen en init). Sin apiKey no hay\n // GET que esperar, así que tampoco se guardan disparos.\n this.awaitingPopups = Boolean(this.config.apiKey && this.baseUrl);\n this.fetchPopupsFromServer().then((defs) => {\n if (!defs) this.dropPendingTriggers();\n this.popupDefinitions = this.validatePopupDefinitions(defs ?? []);\n this.popupsLoaded = true;\n this.log('Popups loaded from API', this.popupDefinitions);\n // Con al menos un popup que pueda abrirse, el renderer se trae ya lo que costará la\n // apertura (en navegador, el chunk de renderPopup). Un host sin popups no descarga\n // nada.\n if (this.popupDefinitions.length) this.renderer.preload?.();\n this.configureTriggersFromDefinitions();\n this.processDeferredExitQueue();\n if (this.pendingAutoLaunch) {\n this.startTriggers();\n }\n this.replayPendingTriggers();\n });\n }\n\n /** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */\n getUserId(): string | null {\n return this.tracking?.getUserId() ?? null;\n }\n\n /** Session id de navegación actual. Null si tracking off. */\n getSessionId(): string | null {\n return this.tracking?.getSessionId() ?? null;\n }\n\n /**\n * Id del registro de analytics abierto: el `sessionId` que devolvió `POST /sdk/feedback`,\n * que la API guarda como `sdkSessionId` en el Feedback en que se convierte la sesión. No es\n * `getSessionId()`. Null hasta que el backend acepta el primer lote y de nuevo al cerrar la\n * sesión: para saber también qué registro se cerró, usa `init({ onFeedbackSession })`.\n */\n getFeedbackSessionId(): string | null {\n return this.analyticsFeedbackSessionId ?? null;\n }\n\n /** Avisa al host del registro abierto o cerrado; un fallo del host no corta el envío. */\n private notifyFeedbackSession(sessionId: string, status: FeedbackSession['status']): void {\n try {\n this.onFeedbackSession?.({ sessionId, status });\n } catch (error) {\n this.log('analytics · onFeedbackSession threw:', error);\n }\n }\n\n /** Activa/desactiva el tracking (identidad + sesión + analytics). Kill-switch del contrato §7bis. */\n setTrackingEnabled(enabled: boolean): void {\n // Al revocar el consentimiento se cierra la sesión ANTES de apagar el canal: si no, lo\n // acumulado se quedaría sin enviar y el registro abierto sin cerrar.\n if (!enabled) this.closeSession('tracking_disabled');\n this.tracking?.setTrackingEnabled(enabled);\n // Consentimiento concedido (ahora o más tarde que el init): abre sesión.\n if (enabled) this.openSession('tracking_enabled');\n // Si el consentimiento llega después del init, los handlers de crash aún no están puestos.\n if (enabled) this.installCrashHandlers();\n // El lookup de geo se aplaza hasta que haya consentimiento.\n if (enabled) this.resolveGeo();\n this.log('tracking · setTrackingEnabled:', enabled, '· session_id:', this.tracking?.getSessionId() ?? null);\n }\n\n // ───────── Analytics (canal separado del feedback, vinculado por user_id) ─────────\n\n /** Registra un evento de analítica (modelo GA: nombre + params). No-op si tracking off. */\n track(name: string, params?: Record<string, unknown>): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n // Los eventos del SDK ya van en el namespace `deepdots_`; los custom del host se\n // prefijan con `deepdots_event_` para poder distinguirlos en analítica.\n const eventName = name.startsWith(RESERVED_EVENT_PREFIX) ? name : `${CUSTOM_EVENT_PREFIX}${name}`;\n this.analytics?.track(eventName, params);\n this.log('analytics · track:', eventName, params ?? {});\n }\n\n /** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */\n setUserAttributes(attributes: Record<string, string | number | boolean>): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n this.analytics?.setUserAttributes(attributes);\n }\n\n /**\n * Registra/actualiza una métrica (valor medible) → `feedback.metrics` del body.\n * Persistente y sobrescribe por key. No-op si tracking off.\n */\n setMetric(key: string, value: string | number | boolean): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n this.analytics?.setMetric(key, value);\n this.log('analytics · setMetric:', key, value);\n }\n\n /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */\n reportError(error: unknown, options?: ReportErrorOptions): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n this.crashReporter?.reportError(error, options);\n }\n\n /**\n * Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. Con tracking off\n * se recuerda y se engancha cuando se conceda el consentimiento (`setTrackingEnabled(true)`).\n */\n installReactNativeCrashHandler(errorUtils: ReactNativeErrorUtils): void {\n this.rnErrorUtils = errorUtils;\n this.installCrashHandlers();\n }\n\n /**\n * Pone los handlers de errores no capturados: `window` en web y `ErrorUtils` en RN (el que\n * pasó el host o, si no pasó ninguno, `globalThis.ErrorUtils`, para que un `init()` sin\n * `setupReactNative` también capture). Solo con consentimiento; idempotente, así que se\n * llama en `init()` y cada vez que se activa el tracking.\n */\n private installCrashHandlers(): void {\n if (!this.crashReporter || !this.tracking?.isTrackingEnabled()) return;\n this.crashReporter.install();\n const errorUtils = this.rnErrorUtils\n ?? (globalThis as { ErrorUtils?: ReactNativeErrorUtils }).ErrorUtils;\n if (errorUtils && typeof errorUtils.setGlobalHandler === 'function') {\n this.crashReporter.installReactNative(errorUtils);\n }\n }\n\n /**\n * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el\n * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del\n * init. Solo envía si los atributos cambiaron (diff en storage). No-op si tracking off o sin userId.\n * @returns `true` si se envió al backend, `false` si no hubo cambios o está deshabilitado.\n */\n async setContactAttributes(attributes: ContactAttributes): Promise<boolean> {\n if (!this.tracking?.isTrackingEnabled()) return false;\n if (!this.contact) return false;\n return this.contact.setAttributes(attributes);\n }\n\n /** Marca el inicio de un mini-service; etiqueta los eventos siguientes. No-op si tracking off. */\n enterMiniService(name: string, entryPointType?: string): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n this.analytics?.enterMiniService(name, entryPointType);\n this.log('analytics · enterMiniService:', name, entryPointType ?? '');\n }\n\n /** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */\n exitMiniService(name: string): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n this.analytics?.exitMiniService(name);\n this.log('analytics · exitMiniService:', name);\n }\n\n /** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */\n trackSearch(query: string, resultsCount: number, params?: Record<string, unknown>): void {\n this.track('deepdots_search', { query, results_count: resultsCount, has_results: resultsCount > 0, ...(params ?? {}) });\n }\n\n /** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */\n trackFindabilityFriction(frictionTopic: string, params?: Record<string, unknown>): void {\n this.track('deepdots_findability_friction', { friction_topic: frictionTopic, ...(params ?? {}) });\n }\n\n /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */\n trackFunnelStep(funnel: string, step: string, taskId: string, params?: Record<string, unknown>): void {\n this.track('deepdots_funnel_step', { funnel, step, task_id: taskId, ...(params ?? {}) });\n }\n\n /**\n * Product Effectiveness: una interacción significativa del usuario (`interaction_type` es la\n * dimensión de agrupación, p. ej. `get_help`, `homepage`, `contact_support`).\n *\n * Existe como evento RESERVADO para que la convención no la invente cada host: un `track()`\n * custom acaba como `deepdots_event_…` y no lo cuenta la analítica de effectiveness.\n * Un `interactionType` vacío se descarta con warning (no aporta dato agrupable).\n */\n trackMeaningfulInteraction(interactionType: string, params?: Record<string, unknown>): void {\n const type = typeof interactionType === 'string' ? interactionType.trim() : '';\n if (!type) {\n // Siempre visible (como en `trackMessage`): es un error de integración del host.\n console.warn('[DeepdotsPopups] trackMeaningfulInteraction discarded: empty interaction_type');\n return;\n }\n this.track('deepdots_meaningful_interaction', { interaction_type: type, ...(params ?? {}) });\n }\n\n /**\n * Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app).\n * No-op si tracking off. El evento se descarta (con warning) si el `channel` no es válido,\n * si ese `(message_id, stage)` ya se emitió en la sesión, o si el `message_id` ya se reportó\n * en otro canal — ver `MessageGuard`.\n */\n trackMessage(stage: MessageStage, options: TrackMessageOptions): void {\n const verdict = this.messageGuard.evaluate(stage, options);\n if (!verdict.emit) {\n console.warn(`[DeepdotsPopups] trackMessage discarded (${verdict.reason}): ${verdict.detail}`);\n return;\n }\n this.track('deepdots_message', buildMessageParams(stage, options));\n }\n\n /**\n * Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).\n * El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.\n */\n setScreen(name: string): void {\n if (!this.navObserver) return;\n if (!this.navStarted) {\n this.navObserver.begin(name);\n this.navStarted = true;\n } else {\n this.navObserver.visit(name);\n }\n }\n\n /** App a foreground (RN: AppState 'active'): reanuda el engagement time y abre sesión nueva. */\n onForeground(): void {\n this.engagement?.resume();\n this.openSession('foreground');\n }\n\n /**\n * App a background (RN: AppState 'background'): FIN DE SESIÓN. Cierra pantalla actual\n * (page_view), mini-services (mini_service_exit), emite engagement + `session_end` y\n * envía el último lote con `completed:true`.\n *\n * Es la única señal disponible en móvil: el kill de la app (swipe) NO da callback.\n */\n onBackground(): void {\n this.closeSession('background');\n this.engagement?.pause();\n }\n\n /**\n * Cierre EXPLÍCITO de la sesión por parte del host (logout, fin de flujo…): emite\n * `session_end` y envía el último lote con `completed:true`.\n */\n endSession(): void {\n this.closeSession('manual');\n }\n\n /**\n * Cambio de usuario (login / logout / cambio de cuenta). Cierra la sesión del usuario\n * anterior (`session_end` con `reason: 'user_change'` + `completed:true`), cambia la\n * identidad y abre una sesión nueva. Sin `userId` vuelve al id anónimo del SDK.\n *\n * Equivale a llamar a `init()` otra vez con otro `userId`.\n */\n setUserId(userId?: string): void {\n const next = userId || undefined;\n if (!this.initialized || !this.config || next === this.config.userId) return;\n\n this.closeSession('user_change');\n\n const enabled = this.tracking?.isTrackingEnabled() ?? true;\n const storage = this.storage ?? createDefaultStorage();\n this.config.userId = next;\n this.tracking = new TrackingManager({ storage, clientUserId: next, enabled });\n // El Contact solo existe para usuarios identificados.\n this.contact = next\n ? new ContactManager({\n storage,\n publicKey: this.config.apiKey ?? '',\n userId: next,\n post: (body) => this.postContact(body),\n })\n : null;\n // Atributos, métricas y protecciones de messaging pertenecían al usuario anterior.\n this.analytics?.resetUserScope();\n this.messageGuard.reset();\n // El historial de redisplay era del usuario anterior: el nuevo empieza de cero.\n this.lastShown.clear();\n this.surveyProgress.clear();\n this.answeredSurveys.clear();\n clearPopupState(storage);\n\n this.log('tracking · user_change · user_id:', this.tracking.getUserId());\n this.openSession('user_change');\n }\n\n /**\n * Cierra la sesión actual: vuelca todo lo que quedaba abierto y envía el último lote\n * marcado con `completed:true`, para que el backend cierre el registro. El `sessionId`\n * cacheado se olvida → el lote siguiente abre un registro nuevo.\n *\n * Idempotente: dos cierres seguidos (p. ej. `visibilitychange` + `pagehide`) no duplican\n * el `session_end`.\n */\n private closeSession(reason: SessionEndReason): void {\n if (!this.sessionOpen) return;\n if (!this.tracking?.isTrackingEnabled()) return;\n this.sessionOpen = false;\n\n this.navObserver?.stop(); // cierra la pantalla actual → page_view\n this.navStarted = false;\n this.analytics?.exitAllMiniServices(); // → mini_service_exit con duración\n this.flushEngagement(); // → user_engagement con el tiempo activo\n // Parado hasta la sesión siguiente. Si siguiera en marcha, lo que llega después del\n // cierre (en Chromium el `visibilitychange` va DESPUÉS de `pagehide`) emitía un\n // `user_engagement` sin sessionId que abría en la API un registro que nunca se cierra.\n this.engagement?.pause();\n this.track('deepdots_session_end', { reason });\n // `final` solo cuando el documento se está muriendo (el transporte no espera respuestas).\n this.flushAnalytics({ final: reason === 'page_hide', sessionEnd: true });\n // El session_id del canal de analytics y el de popups pertenecían a la sesión cerrada.\n this.analyticsFeedbackSessionId = undefined;\n this.tracking?.setSessionId(null);\n this.log('tracking · session_end:', reason);\n }\n\n /**\n * Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente.\n * El `reason` dice por qué empieza (ver `SessionStartReason`):\n * - `cold_start`: init() (carga de la página / arranque de la app RN).\n * - `foreground`: onForeground() tras un cierre (RN).\n * - `tracking_enabled`: setTrackingEnabled(true).\n * - `user_change`: setUserId() o init() con otro userId.\n * - `idle_timeout`: la pestaña vuelve a verse tras más de 30 min oculta.\n * - `bfcache_restore`: vuelta desde la bfcache fuera de la ventana de reanudación.\n * Si hay una sesión suspendida a tiempo emite `deepdots_session_resume` en su lugar, sin reason.\n */\n private openSession(reason: SessionStartReason): void {\n if (this.sessionOpen) return;\n if (!this.tracking?.isTrackingEnabled()) return;\n const suspendedMs = this.takeSuspendedSession();\n this.sessionOpen = true;\n this.lastEngagementAt = Date.now();\n if (suspendedMs === null) {\n this.track('deepdots_session_start', { reason });\n } else {\n // Misma sesión: sin `session_start`, que contaría una sesión de más.\n this.track('deepdots_session_resume', { suspended_ms: suspendedMs });\n this.log('tracking · session resumed after', suspendedMs, 'ms:', this.analyticsFeedbackSessionId);\n }\n // El cierre anterior dejó sin pantalla al observador: sin esto, la pantalla en la que\n // está el usuario al reabrir (cambio de usuario, vuelta de la bfcache) no contaba.\n this.navObserver?.restart();\n // Una pestaña abierta en segundo plano no ha sido vista: no cuenta hasta que se muestre.\n if (this.isDocumentVisible()) this.engagement?.resume();\n }\n\n /** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */\n private isDocumentVisible(): boolean {\n return typeof document === 'undefined' || document.visibilityState !== 'hidden';\n }\n\n /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */\n previewAnalytics(): AnalyticsEnvelope {\n return (\n this.analytics?.buildPayload(this.analyticsIdentity()) ?? {\n userId: null,\n sessionId: null,\n context: { platform: 'web', attributes: {} },\n events: [],\n }\n );\n }\n\n /**\n * Envía el lote acumulado de analytics y vacía el buffer.\n * `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque\n * el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.\n */\n flushAnalytics(options?: { final?: boolean; sessionEnd?: boolean }): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n this.analytics?.flush(this.analyticsIdentity(), options);\n }\n\n /**\n * Emite un evento `user_engagement` con el tiempo activo acumulado (#8) y la pantalla en\n * curso: `screen` y `path` (los mismos que llevará su `page_view`) y `screen_seconds` (lo\n * que lleva en ella, medido como el `duration_seconds` del `page_view`).\n *\n * El `page_view` sale al SALIR de la pantalla; si el cierre no llega nunca (pestaña dormida\n * y descartada al volver, pestaña o app matada en segundo plano), la última pantalla de la\n * sesión se perdía y una visita de una sola pantalla se quedaba sin ninguna. Con la pantalla\n * en el latido y en el engagement de ocultar la pestaña, el backend la reconstruye.\n *\n * En los cierres (`closeSession`, `suspendSession`) el observador ya emitió el `page_view`\n * y no tiene pantalla, así que ese engagement va sin `screen`: nunca se cuenta dos veces.\n * Tampoco se emite un `page_view` aquí, por lo mismo.\n */\n private flushEngagement(): void {\n if (!this.tracking?.isTrackingEnabled()) return;\n const ms = this.engagement?.consume() ?? 0;\n if (ms > 0) {\n const current = this.navObserver?.current();\n this.track('deepdots_user_engagement', {\n engagement_time_msec: ms,\n ...(current\n ? { screen: current.screen, path: current.path, screen_seconds: current.seconds }\n : {}),\n });\n this.lastEngagementAt = Date.now();\n }\n }\n\n /** Tick del flush periódico: latido de engagement si toca, y envío de lo acumulado. */\n private onAnalyticsFlushTick(): void {\n if (\n this.sessionOpen &&\n this.isDocumentVisible() &&\n Date.now() - this.lastEngagementAt >= ANALYTICS_HEARTBEAT_MS\n ) {\n this.flushEngagement();\n }\n this.flushAnalytics();\n }\n\n /**\n * La pestaña vuelve a verse. Tras más de ANALYTICS_SESSION_TIMEOUT_MS oculta, la sesión\n * anterior termina y empieza otra: seguir con el mismo sessionId mezclaba dos visitas y,\n * si el backend ya la había cerrado por inactividad, la partía en dos feedbacks.\n * - Oculta menos de ANALYTICS_SESSION_DISCARD_MS: el backend aún la tiene abierta → se\n * cierra con normalidad (page_view de la pantalla, session_end `idle_timeout`, completed).\n * - Más: ya la cerró el backend → se descarta sin enviar nada.\n */\n private onDocumentVisible(): void {\n const hiddenFor = this.hiddenAt === null ? 0 : Date.now() - this.hiddenAt;\n this.hiddenAt = null;\n if (!this.sessionOpen) return; // sin sesión abierta no hay dónde contar ese tiempo\n if (hiddenFor < ANALYTICS_SESSION_TIMEOUT_MS) {\n this.engagement?.resume();\n return;\n }\n if (hiddenFor < ANALYTICS_SESSION_DISCARD_MS) {\n this.closeSession('idle_timeout');\n } else {\n this.discardSession();\n }\n this.openSession('idle_timeout');\n }\n\n /**\n * `pagehide` en web: SUSPENDE la sesión en vez de cerrarla, porque el navegador lo lanza\n * igual al recargar que al cerrar la pestaña. Vuelca lo pendiente (page_view de la\n * pantalla, mini-services, engagement) con un `deepdots_session_suspend` y SIN\n * `completed`, y deja el registro en `sessionStorage` (ver `suspended-session.ts`):\n * - si la página vuelve a cargar en esta pestaña, `init()` sigue en el mismo registro;\n * - si la pestaña se cerró, el backend lo cierra a los pocos minutos sin otro lote.\n *\n * Devuelve false cuando no se puede suspender — aún no se conoce el registro (el primer\n * POST sigue en vuelo) o no hay `sessionStorage` — y entonces el caller cierra como antes.\n */\n private suspendSession(): boolean {\n if (!this.sessionOpen || !this.tracking?.isTrackingEnabled()) return false;\n const sessionId = this.analyticsFeedbackSessionId;\n const integration = this.analyticsIntegration;\n const storage = getSessionStorage();\n if (!sessionId || !integration || !storage) return false;\n const at = Date.now();\n const userId = this.tracking.getUserId();\n if (!writeSuspendedSession(storage, { sessionId, integration, userId, at })) return false;\n\n this.sessionOpen = false;\n this.suspended = { at, userId };\n this.navObserver?.stop(); // cierra la pantalla actual → page_view\n this.navStarted = false;\n this.analytics?.exitAllMiniServices();\n this.flushEngagement();\n this.engagement?.pause(); // como en closeSession: nada de engagement tras el pagehide\n this.track('deepdots_session_suspend', { reason: 'page_hide' });\n this.flushAnalytics({ final: true });\n this.log('tracking · session suspended:', sessionId);\n return true;\n }\n\n /**\n * Recoge la sesión que dejó suspendida la carga anterior de esta pestaña (ver\n * `suspendSession`). Solo con analytics configurado y el tracking activo: sin ellos no hay\n * registro en el que seguir.\n */\n private readResumableSession() {\n const integration = this.analyticsIntegration;\n const storage = getSessionStorage();\n if (!integration || !storage || !this.tracking?.isTrackingEnabled()) return null;\n return takeSuspendedSession(\n storage,\n { integration, userId: this.tracking.getUserId() },\n Date.now(),\n ANALYTICS_RESUME_WINDOW_MS,\n );\n }\n\n /**\n * Consume la sesión suspendida al abrir: los ms que estuvo suspendida si se puede seguir en\n * ella (mismo usuario, dentro de ANALYTICS_RESUME_WINDOW_MS); null si no había o ya no toca.\n * En ese caso se olvida su registro sin enviar nada: lo cierra el backend, y la sesión que\n * se abre ahora va a uno nuevo.\n */\n private takeSuspendedSession(): number | null {\n const suspended = this.suspended;\n if (!suspended) return null;\n this.suspended = null;\n const storage = getSessionStorage();\n if (storage) clearSuspendedSession(storage);\n const suspendedMs = Math.max(0, Date.now() - suspended.at);\n if (\n this.analyticsFeedbackSessionId &&\n suspendedMs <= ANALYTICS_RESUME_WINDOW_MS &&\n suspended.userId === (this.tracking?.getUserId() ?? null)\n ) {\n return suspendedMs;\n }\n this.analytics?.discardSession(this.analyticsIdentity());\n this.analyticsFeedbackSessionId = undefined;\n this.tracking?.setSessionId(null);\n return null;\n }\n\n /** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */\n private discardSession(): void {\n this.sessionOpen = false;\n this.navObserver?.discard();\n this.analytics?.discardSession(this.analyticsIdentity());\n this.engagement?.consume(); // tiramos lo acumulado: era de la sesión descartada\n this.engagement?.pause();\n this.analyticsFeedbackSessionId = undefined;\n this.tracking?.setSessionId(null);\n this.log('tracking · session discarded after', ANALYTICS_SESSION_DISCARD_MS / 60_000, 'min hidden');\n }\n\n /**\n * Geolocalización por IP (país/ciudad para analytics). La caché persistente es local y se\n * aplica siempre que `geolocation` no esté desactivado. El lookup a terceros (ipapi.co →\n * ipwho.is → ipinfo.io, fallback con timeout) solo se hace si el dato se va a ENVIAR:\n * analytics configurado + tracking activo + caché ausente o caducada. Una vez por instancia.\n */\n private resolveGeo(): void {\n const storage = this.storage;\n if (!this.geoEnabled || !storage || !this.analytics) return;\n const cachedGeo = readCachedGeo(storage, Date.now());\n if (cachedGeo) {\n this.analytics.updateDevice(cachedGeo);\n return;\n }\n if (!this.geoWanted || this.geoLookupStarted) return;\n if (!this.tracking?.isTrackingEnabled()) return;\n this.geoLookupStarted = true;\n fetchGeo()\n .then((geo) => {\n if (geo) {\n this.analytics?.updateDevice(geo);\n writeCachedGeo(storage, geo, Date.now());\n }\n })\n .catch(() => {});\n }\n\n private analyticsIdentity() {\n return {\n userId: this.tracking?.getUserId() ?? null,\n sessionId: this.tracking?.getSessionId() ?? null,\n };\n }\n\n /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */\n private setupAnalyticsFlush(): void {\n // Flush periódico + latido en todos los entornos. En RN no había ninguno: una app en\n // foreground que no llegaba a `maxBatchSize` no enviaba nada hasta ir a background.\n this.startAnalyticsFlushTimer();\n if (typeof document === 'undefined' || typeof window === 'undefined') return;\n // al ocultar la página (recarga, navegación o cierre, indistinguibles aquí): se SUSPENDE\n // la sesión para que una recarga siga en ella (ver suspendSession). Si no se puede, FIN\n // DE SESIÓN como antes (page_view + mini_service_exit + engagement + session_end) y\n // último lote con completed:true.\n window.addEventListener('pagehide', () => {\n clearInterval(this.analyticsFlushTimer);\n if (!this.suspendSession()) this.closeSession('page_hide');\n });\n // Vuelta desde la bfcache (botón atrás): la página no se recarga, así que `init()` no\n // vuelve a correr. El `pagehide` suspendió (o cerró) la sesión y paró el flush; sin\n // reabrir ambos, la pestaña no mandaba nada más hasta una recarga. `openSession`\n // reanuda la suspendida si sigue a tiempo.\n window.addEventListener('pageshow', (event) => {\n if (!(event as PageTransitionEvent).persisted) return;\n this.startAnalyticsFlushTimer();\n this.openSession('bfcache_restore');\n });\n // al ocultar/mostrar la pestaña: pausar/reanudar engagement y enviar lo acumulado. El\n // engagement de ocultar lleva la pantalla en curso: si la pestaña no vuelve (la descarta\n // el navegador, se cierra dormida) es lo último que se sabe de dónde estaba el usuario.\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') {\n this.flushEngagement();\n this.engagement?.pause();\n this.flushAnalytics();\n this.hiddenAt = Date.now();\n } else if (document.visibilityState === 'visible') {\n this.onDocumentVisible();\n }\n });\n }\n\n private startAnalyticsFlushTimer(): void {\n clearInterval(this.analyticsFlushTimer);\n this.analyticsFlushTimer = setInterval(() => this.onAnalyticsFlushTick(), ANALYTICS_FLUSH_INTERVAL_MS);\n }\n\n /** Enable auto-launch functionality with configured triggers */\n autoLaunch(): void {\n if (!this.initialized) {\n throw new Error('SDK not initialized. Call init() first.');\n }\n if (!this.popupsLoaded) {\n this.pendingAutoLaunch = true;\n this.log('Auto-launch deferred until popups are loaded');\n return;\n }\n this.startTriggers();\n }\n\n /** Inicia los triggers configurados */\n private startTriggers(): void {\n this.log('Auto-launch enabled');\n this.triggers.forEach((trigger) => setupTrigger(this, trigger));\n }\n\n /** Configure triggers for auto-launching popups (manual) */\n configureTriggers(triggers: TriggerConfig[]): void {\n if (!this.initialized) {\n throw new Error('SDK not initialized. Call init() first.');\n }\n this.triggers = triggers;\n this.log('Triggers configured', triggers);\n }\n\n /** Deriva triggers desde las definiciones de popup */\n private configureTriggersFromDefinitions(): void {\n const derived: TriggerConfig[] = [];\n this.debug('Validated popup definitions', this.popupDefinitions);\n\n this.popupDefinitions.forEach((def) => {\n def.triggers.forEach((trigger) => {\n const type = this.mapPopupTriggerType(trigger.type);\n if (!type) {\n this.debug('Unsupported trigger type', trigger.type);\n return;\n }\n const value = type === 'time' && typeof trigger.value === 'number'\n ? trigger.value * 1000\n : trigger.value;\n derived.push({ type, value, surveyId: def.surveyId, popupId: def.id });\n });\n });\n\n if (derived.length) {\n this.configureTriggers(derived);\n }\n }\n\n /** Lógica para evaluar condiciones antes de mostrar una encuesta */\n triggerSurvey(surveyId: string, popupId?: string): void {\n if (this.bufferIfAwaitingPopups({ kind: 'survey', surveyId, popupId })) return;\n const def = this.findPopupDefinition(surveyId, popupId);\n if (!def) {\n this.debug('No popup definition for trigger', { surveyId, popupId });\n return;\n }\n if (!this.shouldShow(def)) {\n this.debug('Conditions prevented showing popup', def.id);\n return;\n }\n this.showDefinition(def);\n }\n\n /** Trigger popups configured with trigger.type = 'event' and matching trigger.value */\n public triggerEvent(eventName: string): void {\n if (!this.initialized) {\n throw new Error('SDK not initialized. Call init() first.');\n }\n const normalized = String(eventName || '').trim();\n if (!normalized) {\n this.debug('Ignoring empty event trigger name');\n return;\n }\n if (this.bufferIfAwaitingPopups({ kind: 'event', name: normalized })) return;\n this.evaluateEvent(normalized);\n }\n\n private evaluateEvent(normalized: string): void {\n const candidates = this.popupDefinitions.filter((def) => {\n return def.triggers.some((trigger) => {\n const triggerValue = String(trigger.value ?? '').trim();\n return trigger.type === 'event' && triggerValue === normalized;\n });\n });\n\n if (!candidates.length) {\n this.debug('No event popup definitions found', { eventName: normalized });\n return;\n }\n\n const matched = candidates.find((def) => this.shouldShow(def));\n if (!matched) {\n this.debug('Event popup blocked by conditions/segments', { eventName: normalized });\n return;\n }\n\n this.showDefinition(matched);\n }\n\n /**\n * Guarda `trigger` si las definiciones aún no han llegado. Devuelve `true` si lo guardó (el\n * llamador no debe evaluarlo ahora) y `false` si no hay carga en vuelo (se evalúa ya, como\n * siempre: tras cargar, tras un fallo o sin apiKey).\n */\n private bufferIfAwaitingPopups(trigger: PendingTrigger): boolean {\n if (!this.awaitingPopups) return false;\n if (this.pendingTriggers.length >= MAX_PENDING_TRIGGERS) {\n const dropped = this.pendingTriggers.shift();\n this.debug('Pending trigger buffer full; dropped oldest', dropped);\n }\n this.pendingTriggers.push(trigger);\n this.debug('Popups not loaded yet; trigger buffered', trigger);\n return true;\n }\n\n /** La carga falló: sin definiciones no hay nada que reproducir, se descarta todo. */\n private dropPendingTriggers(): void {\n const dropped = this.pendingTriggers.length;\n this.awaitingPopups = false;\n this.pendingTriggers = [];\n if (dropped > 0) this.debug('Popups failed to load; dropped pending triggers', dropped);\n }\n\n /** Reproduce en orden por la evaluación normal (segmentos, cooldowns y condiciones de AHORA). */\n private replayPendingTriggers(): void {\n this.awaitingPopups = false;\n const replay = this.pendingTriggers;\n this.pendingTriggers = [];\n if (!replay.length) return;\n this.debug('Replaying triggers received before popups loaded', replay.length);\n replay.forEach((trigger) => {\n if (trigger.kind === 'event') this.evaluateEvent(trigger.name);\n else this.triggerSurvey(trigger.surveyId, trigger.popupId);\n });\n }\n\n private showDefinition(def: PopupDefinition): void {\n this.log('Showing popup (definition)', def);\n this.surveyToPopupId.set(def.surveyId, def.id);\n this.lastShown.set(def.id, Date.now());\n this.persistPopupState();\n this.renderPopup(def.surveyId, def.productId, def.actions, def.style, def.title);\n this.emitEvent('popup_shown', def.surveyId, { popupId: def.id });\n }\n\n private shouldShow(def: NormalizedPopupDefinition, pathUrl?: string, skipPathCheck = false): boolean {\n // Otra pestaña puede haber enseñado o contestado este popup desde nuestro `init()`.\n if (this.storage) this.loadPopupState(this.storage);\n // La exclusión se evalúa SIEMPRE, también cuando `skipPathCheck` levanta la\n // restricción de \"dónde PUEDE mostrarse\" (popup de exit ya encolado): decir\n // \"no en /cart\" es una regla sobre la pantalla en la que se va a pintar.\n if (this.isPathExcluded(def, pathUrl)) {\n return false;\n }\n if (!skipPathCheck && !this.matchesSegmentsPath(def, pathUrl)) {\n return false;\n }\n if (!this.matchesSegmentsLang(def)) {\n return false;\n }\n\n const cooldowns = def.cooldown || [];\n if (cooldowns.length && !cooldowns.every((cooldown) => this.evaluateCooldown(def, cooldown))) {\n return false;\n }\n\n const legacyConditions = def.legacyConditions || [];\n if (!legacyConditions.length) return true;\n return legacyConditions.every((condition) => this.evaluateLegacyCondition(def, condition));\n }\n\n private matchesSegmentsLang(def: PopupDefinition): boolean {\n const langs = def.segments?.lang;\n if (!langs || langs.length === 0) return true;\n\n // Mismo idioma que reporta el context de analytics: explícito > navigator.language\n // > Intl. ⚠️ NO leer `navigator.language` aquí: en React Native `navigator` existe\n // pero no tiene `language`, y `undefined.toLowerCase()` reventaba la evaluación\n // COMPLETA del trigger (no solo este popup).\n const currentLang = (this.language ?? '').toLowerCase();\n\n if (!currentLang) {\n this.debug('No language available for lang comparison', { popupId: def.id, langs });\n return true;\n }\n\n // Acepta si el idioma actual empieza con alguno de los configurados\n // ej: \"en-US\" coincide con segment \"en\". Ignora entradas no-string del backend.\n const matches = langs.some(\n (lang) => typeof lang === 'string' && currentLang.startsWith(lang.toLowerCase()),\n );\n if (!matches) {\n this.debug('Lang mismatch for popup', { popupId: def.id, langs, currentLang });\n }\n return matches;\n }\n\n /** `segments.path`: rutas donde el popup PUEDE mostrarse. Sin lista, en todas. */\n private matchesSegmentsPath(def: PopupDefinition, pathUrl?: string): boolean {\n const paths = def.segments?.path;\n if (!Array.isArray(paths) || paths.length === 0) return true;\n\n const location = this.resolveCurrentLocation(pathUrl);\n if (!location) {\n this.debug('No window.location available for path comparison', { popupId: def.id, paths });\n return true;\n }\n\n const matches = paths.some((rawCandidate) => {\n const match = this.matchesPathCandidate(rawCandidate, location);\n this.debug('Path comparison', {\n popupId: def.id,\n candidate: rawCandidate,\n currentPath: location.path,\n currentHref: location.href,\n match,\n });\n return match;\n });\n\n if (!matches) {\n this.debug('No path match for popup', { popupId: def.id, paths, currentPath: location.path });\n }\n\n return matches;\n }\n\n /**\n * `segments.excludedPaths`: rutas donde el popup NO debe mostrarse. Gana sobre\n * `segments.path` (`path: ['/']` + `excludedPaths: ['/cart']` = en todas menos el\n * carrito) y se aplica también cuando no hay `path` (= en todas menos las excluidas).\n */\n private isPathExcluded(def: PopupDefinition, pathUrl?: string): boolean {\n const excluded = def.segments?.excludedPaths;\n if (!Array.isArray(excluded) || excluded.length === 0) return false;\n\n const location = this.resolveCurrentLocation(pathUrl);\n if (!location) {\n // Sin `window.location` (RN/SSR) no se puede evaluar: no bloqueamos, igual\n // que hace `matchesSegmentsPath` con la lista de inclusión.\n this.debug('No window.location available for excluded path comparison', { popupId: def.id, excluded });\n return false;\n }\n\n const hit = excluded.find((rawCandidate) => this.matchesPathCandidate(rawCandidate, location));\n if (hit !== undefined) {\n this.debug('Popup excluded by path', {\n popupId: def.id,\n candidate: hit,\n currentPath: location.path,\n currentHref: location.href,\n });\n return true;\n }\n return false;\n }\n\n /**\n * Compara UN candidato de `segments.path`/`segments.excludedPaths` con la ruta\n * actual. Mismas reglas para incluir y para excluir:\n * - URL absoluta → href completo exacto\n * - empieza por / → subcadena del href (así entran las rutas con hash: `/#/home`)\n * - resto → pathname exacto\n * Ignora entradas que no sean string: el backend puede colar un `null` en la lista\n * y no puede tumbar la evaluación del trigger entero (mismo fallo que hubo con\n * `segments.lang`).\n */\n private matchesPathCandidate(rawCandidate: unknown, location: { href: string; path: string }): boolean {\n if (typeof rawCandidate !== 'string') return false;\n const candidate = this.normalizeUrl(rawCandidate);\n if (!candidate) return false;\n if (candidate.startsWith('http://') || candidate.startsWith('https://')) {\n return location.href === candidate;\n }\n if (candidate.startsWith('/')) {\n return location.href.includes(candidate);\n }\n return location.path === candidate;\n }\n\n /** href + pathname normalizados de la ruta a evaluar, o null si no hay `window.location`. */\n private resolveCurrentLocation(pathUrl?: string): { href: string; path: string } | null {\n if (typeof window === 'undefined' || !window.location) return null;\n const href = this.normalizeUrl(pathUrl || window.location.href || '');\n const parsed = this.safeParseUrl(href);\n return { href, path: this.normalizeUrl(parsed?.pathname || window.location.pathname || '') };\n }\n\n private evaluateCooldown(def: PopupDefinition, condition: PopupTriggerCondition): boolean {\n switch (condition.answered) {\n case 'SHOWED':\n return this.hasCooldownElapsed(this.lastShown.get(def.id), condition.cooldownDays);\n case 'PARTIAL':\n case 'COMPLETED': {\n const progress = this.surveyProgress.get(def.surveyId);\n if (!progress || progress.status !== condition.answered) {\n return true;\n }\n return this.hasCooldownElapsed(progress.timestamp, condition.cooldownDays);\n }\n default:\n return true;\n }\n }\n\n private evaluateLegacyCondition(def: PopupDefinition, condition: LegacyPopupTriggerCondition): boolean {\n if (!condition.answered && this.answeredSurveys.has(def.surveyId)) {\n return false;\n }\n return this.hasCooldownElapsed(this.lastShown.get(def.id), condition.cooldownDays);\n }\n\n private hasCooldownElapsed(timestamp: number | undefined, cooldownDays?: number): boolean {\n if (!timestamp || !cooldownDays) {\n return true;\n }\n return Date.now() - timestamp >= cooldownDays * DAY_IN_MS;\n }\n\n /** Marcar encuesta como contestada externamente */\n public markSurveyAnswered(surveyId: string): void {\n this.answeredSurveys.add(surveyId);\n this.markSurveyProgress(surveyId, 'COMPLETED');\n }\n\n /** Queue an exit popup so it can render after navigation */\n public queueExitPopup(surveyId: string, delaySeconds: number, sourceUrl?: string, popupId?: string): void {\n const def = this.findPopupDefinition(surveyId, popupId);\n if (!def) {\n this.debug('No popup definition for exit trigger', { surveyId, popupId });\n return;\n }\n\n const originUrl = sourceUrl || (typeof window !== 'undefined' ? window.location.href : '');\n if (!originUrl) {\n this.debug('Exit popup skipped: missing source URL', { surveyId });\n return;\n }\n\n if (!this.shouldShow(def, originUrl)) {\n this.debug('Exit popup skipped by conditions/path', { popupId: def.id, sourceUrl: originUrl });\n return;\n }\n\n const safeDelayMs = Number.isFinite(delaySeconds) ? Math.max(0, delaySeconds * 1000) : 0;\n const deferred: DeferredExitPopup = {\n id: def.id,\n surveyId: def.surveyId,\n sourceUrl: this.normalizeUrl(originUrl),\n dueAt: Date.now() + safeDelayMs,\n };\n\n const queue = this.getDeferredExitQueue().filter((item) => !(item.id === deferred.id && item.sourceUrl === deferred.sourceUrl));\n queue.push(deferred);\n this.setDeferredExitQueue(queue);\n this.scheduleDeferredExitPopup(deferred);\n this.debug('Exit popup queued', deferred);\n }\n\n private processDeferredExitQueue(): void {\n const queue = this.getDeferredExitQueue();\n if (!queue.length) return;\n\n const pending: DeferredExitPopup[] = [];\n const now = Date.now();\n queue.forEach((item) => {\n const def = this.popupDefinitions.find((popup) => popup.id === item.id && popup.surveyId === item.surveyId);\n if (!def) {\n return;\n }\n if (item.dueAt <= now) {\n this.tryShowDeferredExitPopup(item);\n return;\n }\n pending.push(item);\n this.scheduleDeferredExitPopup(item);\n });\n this.setDeferredExitQueue(pending);\n }\n\n private scheduleDeferredExitPopup(item: DeferredExitPopup): void {\n if (typeof window === 'undefined') return;\n const delay = Math.max(0, item.dueAt - Date.now());\n const timer = window.setTimeout(() => this.tryShowDeferredExitPopup(item), delay);\n this.deferredExitTimers.push(timer);\n }\n\n private tryShowDeferredExitPopup(item: DeferredExitPopup): void {\n const def = this.popupDefinitions.find((popup) => popup.id === item.id && popup.surveyId === item.surveyId);\n if (!def) {\n this.removeDeferredExitPopup(item);\n return;\n }\n\n const currentUrl = typeof window !== 'undefined' ? this.normalizeUrl(window.location.href || '') : '';\n if (!currentUrl || currentUrl === this.normalizeUrl(item.sourceUrl)) {\n this.removeDeferredExitPopup(item);\n this.debug('Exit popup dropped because route did not change', item);\n return;\n }\n\n if (!this.shouldShow(def, undefined, true)) {\n this.removeDeferredExitPopup(item);\n this.debug('Exit popup skipped at render-time conditions', item);\n return;\n }\n\n this.showDefinition(def);\n this.removeDeferredExitPopup(item);\n }\n\n private findPopupDefinition(surveyId: string, popupId?: string): NormalizedPopupDefinition | undefined {\n if (popupId) {\n const byId = this.popupDefinitions.find((popup) => popup.id === popupId);\n if (byId) return byId;\n }\n return this.popupDefinitions.find((popup) => popup.surveyId === surveyId);\n }\n\n private removeDeferredExitPopup(item: DeferredExitPopup): void {\n const queue = this.getDeferredExitQueue().filter((entry) => !(entry.id === item.id && entry.sourceUrl === item.sourceUrl));\n this.setDeferredExitQueue(queue);\n }\n\n private getDeferredExitQueue(): DeferredExitPopup[] {\n if (typeof window === 'undefined') return [];\n try {\n const raw = window.sessionStorage.getItem(EXIT_QUEUE_STORAGE_KEY);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return parsed\n .filter((entry: unknown): entry is DeferredExitPopup => {\n if (typeof entry !== 'object' || entry === null) return false;\n const item = entry as Partial<DeferredExitPopup>;\n return typeof item.id === 'string'\n && typeof item.surveyId === 'string'\n && typeof item.sourceUrl === 'string'\n && typeof item.dueAt === 'number'\n && Number.isFinite(item.dueAt);\n })\n .map((item) => ({\n ...item,\n sourceUrl: this.normalizeUrl(item.sourceUrl),\n }));\n } catch {\n return [];\n }\n }\n\n private setDeferredExitQueue(queue: DeferredExitPopup[]): void {\n if (typeof window === 'undefined') return;\n try {\n if (!queue.length) {\n window.sessionStorage.removeItem(EXIT_QUEUE_STORAGE_KEY);\n return;\n }\n window.sessionStorage.setItem(EXIT_QUEUE_STORAGE_KEY, JSON.stringify(queue));\n } catch {\n // Ignore storage errors in host environments that block storage access.\n }\n }\n\n /** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */\n private async fetchPopupsFromServer(): Promise<unknown[] | null> {\n const apiKey = this.config?.apiKey;\n const baseUrl = this.baseUrl;\n // El backend aplica las reglas de redisplay AQUI: si este userId ya tiene un\n // SHOWED/PARTIAL/COMPLETED dentro del cooldown, no devuelve ese popup. Para que\n // cuadre, hay que preguntar por el MISMO id con el que se reporta el estado en\n // `POST /sdk/popups` — el del tracking (host si lo hay, si no el anonimo\n // persistente). Con `config.userId` a secas, la integracion que no pasa userId\n // (el caso por defecto) preguntaba sin filtro y el backend devolvia todo.\n const userId = this.tracking?.getUserId() ?? this.config?.userId;\n if (!apiKey || !baseUrl) {\n this.log('Missing apiKey or baseUrl. Skipping popups fetch.');\n return [];\n }\n const filter = userId ? { where: { userId } } : null;\n const query = filter ? `?filter=${encodeURIComponent(JSON.stringify(filter))}` : '';\n const endpoint = `${baseUrl}/sdk/${encodeURIComponent(apiKey)}/popups${query}`;\n try {\n const response = await fetch(endpoint);\n if (!response.ok) {\n this.log('Failed to fetch popups', response.status, response.statusText);\n return null;\n }\n const raw = await response.text();\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n this.log('Unexpected popups payload', parsed);\n return null;\n }\n this.debug('Fetched popups payload', parsed);\n return parsed;\n } catch (error) {\n this.log('Error fetching popups', error);\n return null;\n }\n }\n\n /**\n * Envía los atributos de contact a la API (`POST /sdk/popups/contact`). Rechaza si no se\n * pudo enviar (sin red o respuesta no-2xx), para que `ContactManager` no guarde la caché\n * y la llamada siguiente reintente.\n */\n private async postContact(body: { publicKey: string; userId: string; userAttributes: ContactAttributes }): Promise<void> {\n const baseUrl = this.baseUrl;\n if (!baseUrl) throw new Error('no base URL');\n const endpoint = `${baseUrl}/sdk/popups/contact`;\n let response: Response;\n try {\n response = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n } catch (error) {\n this.log('Error posting contact', error);\n throw error;\n }\n if (!response.ok) {\n this.log('Failed to post contact', response.status, response.statusText);\n throw new Error(`HTTP ${response.status}`);\n }\n this.log('contact · sent attributes for user:', body.userId);\n }\n\n /** Notifica eventos de popup a la API */\n private async postPopupEvent(status: POPUPSESSIONSTATUS, popupId: string, userId?: string): Promise<void> {\n const apiKey = this.config?.apiKey;\n const baseUrl = this.baseUrl;\n if (!apiKey || !baseUrl) {\n return;\n }\n const endpoint = `${baseUrl}/sdk/popups`;\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n publicKey: apiKey,\n status,\n popupId,\n userId: userId || undefined,\n }),\n });\n if (!response.ok) {\n this.log('Failed to post popup event', response.status, response.statusText);\n return;\n }\n // Contrato Fase 1 §6: el backend devuelve el sessionId; lo cacheamos para\n // inyectarlo en las respuestas de survey (lo cose por user_id).\n try {\n const data = await response.json();\n if (data && typeof data.sessionId === 'string') {\n this.tracking?.setSessionId(data.sessionId);\n this.log('tracking · session_id from backend:', data.sessionId);\n }\n } catch {\n // respuesta sin cuerpo JSON: nada que cachear\n }\n } catch (error) {\n this.log('Error posting popup event', error);\n }\n }\n\n /** Add an event listener */\n on(eventType: DeepdotsEventType, listener: EventListener): void {\n if (!this.listeners.has(eventType)) {\n this.listeners.set(eventType, new Set());\n }\n this.listeners.get(eventType)!.add(listener);\n }\n\n /** Remove an event listener */\n off(eventType: DeepdotsEventType, listener: EventListener): void {\n const listeners = this.listeners.get(eventType);\n if (listeners) {\n listeners.delete(listener);\n }\n }\n\n /** Render the popup UI */\n private renderPopup(surveyId: string, productId: string, actions?: PopupActions, style?: PopupStyle, title?: string): void {\n const userId = this.tracking?.getUserId() ?? this.config?.userId;\n const sessionId = this.tracking?.getSessionId() ?? undefined;\n const miniService = this.analytics?.getMiniService() ?? undefined;\n const analyticsFeedbackSessionId = this.analyticsFeedbackSessionId;\n this.log('tracking · survey identity →', { userId: userId ?? null, sessionId: sessionId ?? null, miniService: miniService ?? null, analyticsFeedbackSessionId: analyticsFeedbackSessionId ?? null });\n this.renderer.show(\n surveyId,\n productId,\n actions,\n (type, id, payload) => this.emitEvent(type, id, payload),\n () => this.hidePopup(),\n this.env,\n userId,\n style,\n sessionId,\n miniService,\n analyticsFeedbackSessionId,\n this.renderChrome,\n { title, showProgressBar: this.showProgressBar, surveyCss: this.surveyCss, language: this.language, cspNonce: this.cspNonce },\n );\n }\n\n /** Hide the popup */\n private hidePopup(): void {\n this.renderer.hide();\n }\n\n /** Emit an event */\n private emitEvent(type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>): void {\n const event: DeepdotsEvent = { type, surveyId, timestamp: Date.now(), data };\n this.log('Event emitted', event);\n\n const isPartialClick = type === 'popup_clicked' && data?.action === 'partial';\n if (isPartialClick) {\n this.markSurveyProgress(surveyId, 'PARTIAL');\n }\n if (type === 'survey_completed') {\n this.markSurveyAnswered(surveyId);\n }\n if (type === 'popup_shown' || type === 'survey_completed' || isPartialClick) {\n const popupIdFromData = data?.popupId as string | undefined;\n const popupId = popupIdFromData || this.surveyToPopupId.get(surveyId);\n if (popupId) {\n const userIdFromData = data?.userId as string | undefined;\n const status = type === 'popup_shown'\n ? POPUPSESSIONSTATUS.SHOWED\n : type === 'survey_completed'\n ? POPUPSESSIONSTATUS.COMPLETED\n : POPUPSESSIONSTATUS.PARTIAL;\n void this.postPopupEvent(status, popupId, userIdFromData || this.tracking?.getUserId() || this.config?.userId);\n } else {\n this.debug('No popupId available to post event', { type, surveyId });\n }\n }\n\n const listeners = this.listeners.get(type);\n if (listeners) {\n listeners.forEach((listener) => {\n try {\n listener(event);\n } catch (error) {\n if (this.logger.error) this.logger.error('Error in event listener:', error);\n else this.logger.log('Error in event listener:', error);\n }\n });\n }\n }\n\n /** Log debug messages */\n private log(...args: unknown[]): void {\n if (this.config?.debug) {\n this.logger.log('[DeepdotsPopups]', ...args);\n }\n }\n\n /** External debug method for triggers */\n public debug(...args: unknown[]): void {\n this.log(...args);\n }\n\n /** Set a custom renderer */\n public setRenderer(renderer: PopupRenderer): void {\n this.renderer = renderer;\n if (this.initialized && this.renderer.init) {\n this.renderer.init();\n }\n }\n\n private normalizeUrl(value: string): string {\n if (!value) return '';\n const withoutIndex = value.replace(/\\/index\\.html(?=($|[?#]))/i, '');\n return withoutIndex.length > 1 && withoutIndex.endsWith('/') ? withoutIndex.slice(0, -1) : withoutIndex;\n }\n\n private safeParseUrl(url: string): URL | null {\n if (!url) return null;\n try {\n return new URL(url, typeof window !== 'undefined' ? window.location.href : undefined);\n } catch {\n return null;\n }\n }\n\n private mapPopupTriggerType(type: PopupTrigger['type']): TriggerConfig['type'] | null {\n switch (type) {\n case 'time_on_page':\n return 'time';\n case 'scroll':\n return 'scroll';\n case 'exit':\n return 'exit';\n case 'click':\n return 'click';\n case 'event':\n return 'event';\n default:\n return null;\n }\n }\n\n private markSurveyProgress(surveyId: string, status: PopupTriggerConditionStatus): void {\n const current = this.surveyProgress.get(surveyId);\n if (current?.status === 'COMPLETED' && status !== 'COMPLETED') {\n return;\n }\n this.surveyProgress.set(surveyId, { status, timestamp: Date.now() });\n this.persistPopupState();\n }\n\n /**\n * Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana\n * la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias\n * pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión\n * de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las\n * entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.\n */\n private loadPopupState(storage: KeyValueStorage): void {\n const state = readPopupState(storage);\n Object.entries(state.lastShown).forEach(([popupId, timestamp]) => {\n if (timestamp > (this.lastShown.get(popupId) ?? -Infinity)) this.lastShown.set(popupId, timestamp);\n });\n Object.entries(state.progress).forEach(([surveyId, entry]) => {\n const current = this.surveyProgress.get(surveyId);\n if (current && current.timestamp >= entry.timestamp) return;\n this.surveyProgress.set(surveyId, { status: entry.status, timestamp: entry.timestamp });\n if (entry.status === 'COMPLETED') this.answeredSurveys.add(surveyId);\n });\n }\n\n /**\n * Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.\n * Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña\n * hubiese guardado desde nuestro `init()`.\n */\n private persistPopupState(): void {\n if (!this.storage) return;\n this.loadPopupState(this.storage);\n writePopupState(this.storage, {\n lastShown: Object.fromEntries(this.lastShown),\n progress: Object.fromEntries(this.surveyProgress),\n });\n }\n\n private isPopupDefinition(value: unknown): value is NormalizedPopupDefinition {\n if (typeof value !== 'object' || value === null) return false;\n const popup = value as Partial<NormalizedPopupDefinition>;\n\n return typeof popup.id === 'string'\n && typeof popup.title === 'string'\n && typeof popup.message === 'string'\n && typeof popup.surveyId === 'string'\n && typeof popup.productId === 'string'\n && Array.isArray(popup.triggers)\n && popup.triggers.length > 0\n && popup.triggers.every((trigger) => this.isPopupTrigger(trigger))\n && (!popup.cooldown || popup.cooldown.every((condition) => this.isPopupTriggerCondition(condition)));\n }\n\n private isPopupTrigger(value: unknown): value is PopupTrigger {\n if (typeof value !== 'object' || value === null) return false;\n const trigger = value as Partial<PopupTrigger>;\n return this.isPopupTriggerType(trigger.type)\n && (typeof trigger.value === 'number' || typeof trigger.value === 'string');\n }\n\n private isPopupTriggerType(value: unknown): value is PopupTrigger['type'] {\n return value === 'time_on_page'\n || value === 'scroll'\n || value === 'exit'\n || value === 'click'\n || value === 'event';\n }\n\n private isPopupTriggerCondition(value: unknown): value is PopupTriggerCondition {\n if (typeof value !== 'object' || value === null) return false;\n const condition = value as Partial<PopupTriggerCondition>;\n return POPUP_TRIGGER_CONDITION_STATUSES.includes(condition.answered as PopupTriggerConditionStatus)\n && typeof condition.cooldownDays === 'number'\n && Number.isFinite(condition.cooldownDays)\n && condition.cooldownDays >= 0;\n }\n\n private isLegacyPopupTriggerCondition(value: unknown): value is LegacyPopupTriggerCondition {\n if (typeof value !== 'object' || value === null) return false;\n const condition = value as Partial<LegacyPopupTriggerCondition>;\n if (condition.answered !== undefined && typeof condition.answered !== 'boolean') {\n return false;\n }\n if (condition.cooldownDays !== undefined) {\n return typeof condition.cooldownDays === 'number'\n && Number.isFinite(condition.cooldownDays)\n && condition.cooldownDays >= 0;\n }\n return true;\n }\n\n private normalizeTriggers(value: unknown): PopupTrigger[] {\n const items = Array.isArray(value) ? value : value ? [value] : [];\n return items\n .filter((trigger): trigger is PopupTrigger => this.isPopupTrigger(trigger))\n .map((trigger) => ({\n type: trigger.type,\n value: trigger.value,\n }));\n }\n\n private normalizeCooldown(value: unknown): PopupTriggerCondition[] {\n if (!Array.isArray(value)) return [];\n return value.filter((condition): condition is PopupTriggerCondition => this.isPopupTriggerCondition(condition));\n }\n\n private normalizeLegacyConditions(value: unknown): LegacyPopupTriggerCondition[] {\n if (!Array.isArray(value)) return [];\n return value.filter((condition): condition is LegacyPopupTriggerCondition => this.isLegacyPopupTriggerCondition(condition));\n }\n\n private normalizePopupDefinition(def: unknown): NormalizedPopupDefinition | null {\n if (typeof def !== 'object' || def === null) return null;\n const raw = def as Partial<PopupDefinition> & {\n trigger?: PopupTrigger | PopupTrigger[];\n triggers?: PopupTrigger | PopupTrigger[];\n conditions?: LegacyPopupTriggerCondition[];\n cooldown?: PopupTriggerCondition[];\n };\n\n const rawTriggers = raw.triggers ?? raw.trigger;\n const triggers = this.normalizeTriggers(rawTriggers);\n if (!triggers.length) return null;\n\n const rawTriggerList = Array.isArray(rawTriggers) ? rawTriggers : rawTriggers ? [rawTriggers] : [];\n const legacyConditions = [\n ...this.normalizeLegacyConditions(raw.conditions),\n ...rawTriggerList.flatMap((trigger) => this.normalizeLegacyConditions((trigger as { condition?: unknown }).condition)),\n ];\n const cooldown = this.normalizeCooldown(raw.cooldown);\n\n return {\n ...(raw as PopupDefinition),\n triggers,\n cooldown,\n legacyConditions,\n };\n }\n\n private validatePopupDefinitions(defs: unknown[]): NormalizedPopupDefinition[] {\n return defs\n .map((def) => this.normalizePopupDefinition(def))\n .filter((def): def is NormalizedPopupDefinition => !!def && this.isPopupDefinition(def));\n }\n}\n","/**\n * Auto-wiring de React Native: concentra TODA la configuración en el SDK para que el\n * host solo tenga que envolver su app en un `<DeepdotsProvider config={...}>`.\n *\n * No importa React ni librerías nativas: el host (o el Provider) le pasa los módulos\n * nativos que tenga instalados (MMKV, device-info, AppState). Todo es opcional y degrada\n * con elegancia (sin MMKV → in-memory; sin device-info → sin device info).\n */\nimport type { DeepdotsPopups } from '../core/deepdots-popups';\nimport type { DeepdotsInitParams } from '../types';\nimport type { KeyValueStorage } from '../tracking/tracking-manager';\nimport type { DeviceInfo } from '../analytics/device-info';\nimport type { PopupRenderer } from '../platform/renderer';\nimport type { ReactNativeErrorUtils } from '../analytics/crash-reporter';\n\n/** Forma mínima de `react-native-mmkv` (instancia MMKV). */\nexport interface MmkvLike {\n getString(key: string): string | undefined | null;\n set(key: string, value: string): void;\n delete(key: string): void;\n}\n\n/** Forma mínima de `react-native-device-info`. */\nexport interface DeviceInfoLike {\n isTablet?: () => boolean;\n getSystemVersion?: () => string;\n getDeviceId?: () => string;\n getVersion?: () => string;\n}\n\n/** Forma mínima de `AppState` de react-native. */\nexport interface AppStateLike {\n addEventListener(type: 'change', listener: (state: string) => void): { remove: () => void } | void;\n}\n\nexport interface ReactNativeSetupDeps {\n mmkv?: MmkvLike | null;\n deviceInfo?: DeviceInfoLike | null;\n appState?: AppStateLike | null;\n platform?: 'web' | 'android' | 'ios';\n renderer?: PopupRenderer;\n /** `global.ErrorUtils` de RN (para capturar errores JS no manejados). Default: `globalThis.ErrorUtils`. */\n errorUtils?: ReactNativeErrorUtils | null;\n}\n\n/** Adaptador KeyValueStorage (síncrono) sobre una instancia de MMKV. */\nexport function mmkvStorage(mmkv: MmkvLike): KeyValueStorage {\n return {\n getItem: (k) => mmkv.getString(k) ?? null,\n setItem: (k, v) => mmkv.set(k, v),\n removeItem: (k) => mmkv.delete(k),\n };\n}\n\n/** Recoge device info desde react-native-device-info (Technology #11–13). */\nexport function collectRnDevice(d: DeviceInfoLike): DeviceInfo {\n return {\n device_type: d.isTablet?.() ? 'tablet' : 'mobile',\n os_version: d.getSystemVersion?.(),\n device_model: d.getDeviceId?.(),\n app_version: d.getVersion?.(),\n };\n}\n\n/**\n * Configura el SDK para React Native en una sola llamada: storage persistente (MMKV),\n * device info, platform, init y lifecycle (AppState → onForeground/onBackground).\n * Devuelve una función de limpieza (quita el listener de AppState).\n */\nexport function setupReactNative(\n sdk: DeepdotsPopups,\n config: DeepdotsInitParams,\n deps: ReactNativeSetupDeps = {},\n): () => void {\n if (deps.renderer) sdk.setRenderer(deps.renderer);\n\n const storage = deps.mmkv ? mmkvStorage(deps.mmkv) : config.storage;\n const device = deps.deviceInfo ? collectRnDevice(deps.deviceInfo) : config.device;\n const platform = deps.platform ?? config.platform;\n\n sdk.init({ ...config, storage, device, platform });\n\n // Captura de errores JS no manejados en RN (no hay `window`): usa el ErrorUtils inyectado\n // o el global del runtime RN. Degrada si no existe.\n const errorUtils =\n deps.errorUtils ?? (globalThis as { ErrorUtils?: ReactNativeErrorUtils }).ErrorUtils;\n if (errorUtils && typeof errorUtils.setGlobalHandler === 'function') {\n sdk.installReactNativeCrashHandler(errorUtils);\n }\n\n let subscription: { remove: () => void } | void;\n if (deps.appState?.addEventListener) {\n subscription = deps.appState.addEventListener('change', (state) => {\n if (state === 'active') sdk.onForeground();\n // Solo 'background' es fin de sesión. En iOS 'inactive' es transitorio (llamada\n // entrante, app switcher) y cerrar sesión ahí partiría la sesión en dos: se envía\n // lo acumulado, por si el SO mata la app, pero sin cerrar el registro.\n else if (state === 'background') sdk.onBackground();\n else sdk.flushAnalytics();\n });\n }\n\n return () => {\n if (subscription && typeof subscription.remove === 'function') subscription.remove();\n };\n}\n"],"mappings":"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","names":["pending","pending"]}