@ocularsolution/react-native
v0.2.0
Published
Adaptador React Native para el SDK de Ocular Solution.
Readme
@ocularsolution/react-native
SDK oficial de Ocular Solution para integrar videollamadas en aplicaciones React Native / Expo.
Con solo envolver la app en <OcularProvider>, el SDK renderiza un botón flotante nativo (equivalente a la burbuja del widget web) y un drawer con la videollamada embebida al 100%. La llamada sobrevive a la navegación y al minimizado: la WebView interna nunca se desmonta. Los permisos de cámara/micrófono los gestiona el SDK.
Compatibilidad
| Componente | Versión |
|---|---|
| React Native | ≥ 0.73 (validado con 0.81) |
| React | ≥ 18 (validado con 19.1) |
| Expo SDK | 52+ (validado con 54) |
| react-native-webview | ≥ 13 |
| react-native-gesture-handler / reanimated | ≥ 2 / ≥ 3.16 — drag del drawer en el hilo de UI |
| TypeScript (opcional) | ≥ 5.0 — tipos .d.ts incluidos |
1. Antes de empezar
Solicite a Ocular Solution sus datos de aprovisionamiento:
widgetCode— código de organización de su widget. Configúrelo por variable de entorno; no lo incruste en el repositorio.- Autorización de su
appOrigin— el origin https desde el que su app usará el widget debe estar dado de alta por Ocular Solution. Sin este registro el widget no se inicializa (veráOCULAR_E_WIDGET_TIMEOUT).
La URL del script del widget viene fija en el SDK (producción); solo se sobreescribe con widgetUrl si Ocular Solution le indica un entorno especial.
2. Instalación
npx expo install @ocularsolution/react-native react-native-webview react-native-gesture-handler react-native-reanimated react-native-safe-area-context expo-audioUn solo comando: instala el SDK y alinea las dependencias nativas con la versión de tu Expo SDK. La mayoría de plantillas de Expo ya incluyen varias de ellas — en ese caso expo install solo verifica versiones. expo-audio se usa para una línea obligatoria en iOS (ver §4).
pnpm add @ocularsolution/react-native react-native-webview react-native-gesture-handler react-native-reanimated react-native-safe-area-context
npx pod-installLos gestores modernos (npm ≥7, pnpm ≥8) instalan las peers automáticamente; verifica que las versiones nativas correspondan a tu versión de React Native. La sesión de audio de iOS (§4) puede configurarse con cualquier mecanismo que ponga la AVAudioSession en modo grabación.
3. Permisos (app.json)
{
"expo": {
"android": {
"permissions": ["CAMERA", "RECORD_AUDIO", "MODIFY_AUDIO_SETTINGS"]
},
"ios": {
"infoPlist": {
"NSCameraUsageDescription": "La cámara se usa para la videollamada.",
"NSMicrophoneUsageDescription": "El micrófono se usa para la videollamada."
}
}
}
}En runtime no necesita pedirlos: el SDK los solicita solo (Android) o deja que iOS muestre sus diálogos al capturar. Si quedan denegados, el drawer muestra un panel nativo con botón de conceder y acceso a Ajustes.
4. Uso — integración mínima
// app/_layout.tsx (ejemplo con Expo Router)
import { useEffect } from 'react';
import { Stack } from 'expo-router';
import { setAudioModeAsync } from 'expo-audio';
import { OcularProvider } from '@ocularsolution/react-native';
export default function RootLayout() {
// iOS: habilitar grabación en la sesión de audio (imprescindible para el micrófono).
useEffect(() => {
setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true }).catch(() => {});
}, []);
return (
<OcularProvider
widgetCode={process.env.EXPO_PUBLIC_OCULAR_WIDGET_CODE!}
appOrigin={process.env.EXPO_PUBLIC_OCULAR_APP_ORIGIN}
>
<Stack />
</OcularProvider>
);
}Eso es todo: cuando el widget termina de inicializarse aparece el botón flotante; al tocarlo sube el drawer con la videollamada. El asa (tap o arrastre que sigue al dedo), el fondo y el botón back de Android minimizan sin cortar la llamada.
5. Flujos de negocio
El comportamiento por defecto (botón flotante siempre disponible) es solo uno de los flujos posibles.
CTA propio, sin botón flotante
<OcularProvider {...config} floatingProps={{ showButton: false }}>// En cualquier pantalla: su propio botón/banner abre la videollamada
const { openDrawer } = useOcularUI();
<Button title="Hablar con un asesor" onPress={() => openDrawer()} />Botón solo en ciertas pantallas
const { setButtonVisible } = useOcularUI();
useEffect(() => {
setButtonVisible(false); // ocultar en esta pantalla
return () => setButtonVisible(null); // restaurar el automático al salir
}, [setButtonVisible]);Apertura programática (deep link, post-checkout, evento de negocio)
const { openDrawer, closeDrawer, toggleDrawer, drawerOpen, widgetReady } = useOcularUI();Personalización del botón y el drawer
<OcularProvider
{...config}
floatingProps={{
buttonColor: '#156B35', // color de la burbuja (default)
buttonIconSource: { uri: '...' }, // o require('./icono.png'); default: ícono oficial embebido
position: 'left', // lado del botón
drawerHeight: '90%',
bottomInset: 34, // espacio bajo la llamada (home indicator)
}}
>Control total (sin UI del SDK)
<OcularProvider {...config} floating={false}>Con floating={false} el SDK no renderiza UI; monte <OcularCall style={...} /> donde quiera (pantalla dedicada, Modal propio). No combine floating con <OcularCall>: comparten el transport.
6. API
<OcularProvider>
| Prop | Tipo | Req. | Descripción |
|---|---|---|---|
| widgetCode | string | Sí | Código de organización |
| appOrigin | string | No* | Origin https autorizado (identidad ante el backend). *Requerido en la práctica |
| widgetUrl | string | No | Override de la URL del script del widget. Default: la oficial de producción |
| floating | boolean | No | UI automática (botón + drawer). Default true |
| floatingProps | OcularFloatingWidgetProps | No | Personalización del botón/drawer (ver §5) |
| locale | 'en'\|'es'\|'pt'\|'tr' | No | Idioma inicial del widget |
| debug | boolean | No | Diagnóstico: emite eventos boot/fetch/xhr como widget:message. Solo desarrollo |
| widgetAppOrigin | string | No | Override del filtro de mensajes del iframe (por defecto se resuelve en runtime) |
| widgetModule | string | No | data-modules del loader. Default 'call' |
| dataset | Record<string,string> | No | Atributos data-* extra del loader |
| autoLoad | boolean | No | Inicia load() al montar (con floating la carga ya es automática) |
| readyTimeoutMs | number | No | Timeout de inicialización (default 20000) |
| logger | Logger | No | Logger opcional (debug/info/warn/error) |
floatingProps — diccionario completo
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| position | 'left'\|'right' | 'right' | Lado del botón flotante |
| bottomOffset | number | 24 | Separación inferior del botón, adicional al safe area |
| sideOffset | number | 16 | Separación lateral del botón |
| buttonSize | number | 72 | Diámetro del botón (como la burbuja web) |
| buttonColor | string | '#156B35' | Color del botón, del halo y de los paneles |
| buttonIconSource | ImageSourcePropType | ícono oficial embebido | Imagen del ícono ({uri} o require(...)) |
| buttonIcon | ReactNode | — | Ícono totalmente personalizado; prioridad sobre buttonIconSource |
| showButton | boolean | true | false = flujo "CTA propio" (sin burbuja) |
| highlightInCall | boolean | true | Halo pulsante + badge durante la llamada activa |
| drawerHeight | number\|'N%' | '85%' | Alto del drawer |
| bottomInset | number | 12 | Espacio bajo la llamada, adicional al safe area |
| zoom | number | 1 | Escala visual de la UI de la llamada (0.5–2), sin desbordes |
| ensurePermissions | () => Promise<boolean> | gestor interno | Override de permisos: pídelos y resuelve true si quedaron concedidos; con false el drawer muestra el panel de permisos. Úsalo para integrar expo-camera o lógica propia |
useOcularUI() — control de la UI nativa
const {
widgetReady, // el widget terminó de inicializarse
drawerOpen, // drawer visible
inCall, // llamada activa (entre call-started y call-ended)
openDrawer, closeDrawer, toggleDrawer,
buttonVisible, // null = automático · false = oculto
setButtonVisible, // control dinámico del botón flotante
} = useOcularUI();Durante una llamada activa el botón flotante se destaca solo (halo pulsante + badge); se desactiva con floatingProps={{ highlightInCall: false }}. inCall permite además CTAs propios ("Volver a la llamada") o bloquear flujos de negocio mientras hay llamada.
useOcularCall() — estado y comandos del widget
const { state, load, show, hide, toggle, setLocale, setCustomer, showPopUp, align } = useOcularCall();
// state: 'idle' | 'loading' | 'ready' | 'visible' | 'hidden' | 'error'show()/hide()/toggle() equivalen a abrir/cerrar el drawer (misma máquina de estados que useOcularUI). setLocale() y setCustomer() hablan con el widget; showPopUp() y align() solo aplican al modo floating={false} con <OcularCall> (en modo embebido el widget no tiene burbuja propia y son no-ops). Los comandos emitidos antes de que el widget esté listo se encolan y ejecutan en orden.
useOcular() — cliente y eventos
Devuelve el OcularClient de @ocularsolution/core:
const client = useOcular();
useEffect(() => client.on('widget:message', ({ data }) => {
if (data?.type === 'ocular-widget-event' && data.msg === 'call-started') { /* ... */ }
}), [client]);Diccionario de eventos del cliente:
| Evento | Payload | Cuándo |
|---|---|---|
| state:changed | { from, to } | Cada transición de la máquina de estados |
| widget:ready | — | El widget terminó de inicializarse |
| widget:message | { data } | Mensajes del widget/iframe y señales de diagnóstico |
| call:joined / call:left | { callId, ... } | Al abrir/cerrar el drawer (show/hide) |
| error | { code, message, cause } | Fallo de carga/transport |
Eventos de ciclo de vida del widget — llegan como widget:message con data.type === 'ocular-widget-event' y data.msg ∈ is-ready · button-displayed · opened · minimized · hidden · closed · call-started · call-ended · scheduled-success. (call-ended se captura también del callback oficial ocularCallEnded() del widget y se normaliza al mismo formato.)
useOcularDebug()
requestSnapshot() pide a la página embebida una radiografía de su estado real (flags de ready, scripts, iframes, últimas peticiones si debug). Llega como widget:message con data.ocularDebug === 'snapshot'. No requiere que el widget haya señalado ready.
Exports de bajo nivel (integraciones avanzadas)
| Export | Uso |
|---|---|
| <OcularFloatingWidget {...props} /> | Montar el botón + drawer manualmente (con floating={false} en el provider); una sola instancia |
| <OcularCall style /> | WebView del widget en modo flotante clásico, para pantalla/Modal propios |
| buildEmbeddedHtml(options) | Documento HTML del modo embebido (#ocular-service-container al 100%) |
| buildEmbedHtml(options) | Documento HTML del modo flotante clásico |
| WebViewTransport | Transport RN del OcularClient (attach/detach/receiveFromWebView, cola pre-ready) |
7. Errores
Todos los errores llevan code; llegan por el evento error del cliente o como widget:message de tipo ocular:error.
| Código | Causa | Solución |
|---|---|---|
| OCULAR_E_WIDGET_TIMEOUT | Origin no autorizado, widgetUrl/widgetCode incorrectos o sin red | Verifique el aprovisionamiento con Ocular Solution |
| OCULAR_E_INVALID_CONFIG | widgetUrl no https, widgetCode/zoom/origins con formato inválido | Corrija la configuración (el mensaje indica el campo) |
| OCULAR_E_SCRIPT_LOAD | La WebView no pudo descargar el script del widget | Verifique la URL y la conectividad |
| OCULAR_E_NETWORK | Fallo de red/DNS al cargar la página (el drawer muestra el panel "Sin conexión" con reintento) | Verifique la conexión del dispositivo |
| OCULAR_E_TRANSPORT | load() falló (envuelve la causa, p. ej. el timeout) | Revise cause.code |
| OCULAR_E_INTERNAL | Error dentro de la página embebida (método no permitido/no disponible, excepción JS) | Revise el mensaje; use debug y Snapshot |
| Micrófono no inicia (iOS) | Falta setAudioModeAsync({ allowsRecording: true }) | Aplique el snippet del §4 |
| Cámara/micrófono denegados | Permisos revocados por el usuario | El drawer muestra el panel de permisos con acceso a Ajustes |
Para depurar la WebView: Android chrome://inspect · iOS Safari → menú Desarrollo (el SDK habilita la inspección en desarrollo).
Protección de datos
setCustomer() permite asociar datos del usuario a la sesión. No envíe datos personales sin base legal y consentimiento. Las videollamadas pueden ser grabadas según la configuración de su organización: informe al usuario conforme a la normativa aplicable.
© Ocular Solution. Soporte: a través de su canal de cuenta. Incidentes de seguridad: [email protected]
