Skip to main content

Arquitectura

Estructura del proyecto​

widget/
├── build.mjs # build con esbuild (--watch para dev)
├── serve.mjs # servidor estático local de dist/
├── package.json
├── demo/
│ ├── index.html # sitio de prueba del widget
│ └── frame-crm.html # demo de SSO por iframe del CRM (otro mecanismo)
└── src/
├── widget-loader.js # entrada: todo el widget (IIFE)
├── attachments.js # lógica pura de adjuntos
├── presence.js # lógica pura de "leave intent"
├── attachments.test.js
├── presence.test.js
└── assets/notification.mp3

Módulos de src/​

ArchivoResponsabilidad
widget-loader.jsPunto de entrada. Envuelve todo en una IIFE: configuración, identidad, UI (botón, panel, mensajes), socket, subida de archivos, badge y API pública
attachments.jsFunciones puras: límites de tamaño (MAX_DOCUMENT_SIZE_BYTES 10 MB, MAX_IMAGE_SIZE_BYTES 5 MB), duración de audio (1 a 120 s), buildUploadUrl y formatDuration
presence.jsFunción pura shouldEmitLeaveIntent y constantes LEAVE_INTENT_EVENT (contact-leaving) y LEAVE_INTENT_SOURCE
assets/notification.mp3Sonido de notificación; se incrusta como data URI al compilar

attachments.js y presence.js se extrajeron como módulos hermanos para poder testearlos con vitest sin DOM ni socket; esbuild los incluye en el mismo bundle.

Estructura de widget-loader.js​

El archivo (unas 2.600 líneas) se organiza en secciones numeradas por comentarios:

SecciónContenido
1. Anti-duplicadoSi window.__WhaticketWidgetLoaded__ ya existe, el script termina sin hacer nada
2. ConfiguraciónLee window.WhaticketWidgetConfig; sin token registra un error y aborta
3. Origin del CRMConstantes fijas CRM_ORIGIN, API_BASE (/api/v1), SOCKET_NS (/chat) y SOCKET_CLIENT_SRC (/socket.io/socket.io.js)
3.b IdentidadresolveVisitor(): usuario declarado por el host o visitante anónimo según requiresAuth
Tema y SETTINGSColores (THEME) y opciones efectivas (SETTINGS) con sus valores por defecto
4. EstadoVariables de socket, timers de reintento, DOM (els), no leídos, ticket activo
5. Socket.IOensureSocketIo (carga dinámica) y connect (handshake y eventos)
6. Pre-checkpreValidate: GET /widget/validate, no bloqueante
7. MontajemountWidget y destroyWidget
8. Auto-initboot() al cargar el script

API pública​

Se expone antes de resolver la identidad:

window.WhaticketWidget = {
init: mountWidget,
destroy: destroyWidget,
open: openPanel,
close: closePanel,
};

Detalle de uso en Integración y configuración.

Ciclo de vida​

boot() ──► fetchWidgetConfig() ──► resolveVisitor()
│
requiresAuth === false ─────────┤► visitante anónimo (localStorage)
requiresAuth true ──────────────┤► config.user (si trae externalId y username)
sin config / error ─────────────┘► no monta nada
▼
mountWidget(visitor)
buildPanel() · toggle · badge · connect() · preValidate()
  • mountWidget es idempotente: con el mismo externalId ya montado no hace nada; con otro externalId llama a destroyWidget() y remonta.
  • destroyWidget emite el aviso de salida, desconecta el socket (lo que frena la reconexión), cancela timers, aborta grabación y subida en curso, quita listeners (resize, orientationchange, pagehide, click del documento) y elimina del DOM panel, botón y badge. window.WhaticketWidget permanece intacto.
  • El panel se construye de forma anticipada (oculto) al montar, para recibir historial y mensajes aunque esté cerrado.

Aislamiento visual​

El panel y el botón usan estilos inline con un reset por elemento (all: initial; box-sizing: border-box; y una pila de fuentes del sistema), de modo que no heredan fuentes ni colores del sitio anfitrión. No usa Shadow DOM ni iframe.

Persistencia en el navegador (localStorage)​

ClaveContenido
__WhaticketWidget_visitor_<token>Id del visitante anónimo (user- más un UUID)
__WhaticketWidget_unread_<token>_<externalId>Contador de no leídos (o anon si no hay visitante)
__WhaticketWidget_pos_<token>_<externalId>Posición del botón (left, top) cuando draggable es true

Todos los accesos están en try/catch; si el storage está bloqueado, la identidad anónima vale solo para la sesión.