Saltar al contenido principal

Integración del SDK de React Native

Envoltorio de React Native que conecta los SDK nativos de iOS y Android y ofrece todas las capacidades de la plataforma mediante una única API de TypeScript.

Mínimo iOS 14.0

El SDK nativo requiere iOS 14.0, mientras que React Native 0.75 sigue usando 13.4 por defecto. CocoaPods rechaza la diferencia con un error engañoso - "Specs satisfying the dependency were found, but they required a higher minimum deployment target" - que parece un problema de resolución de versiones y no de plataforma.

Súbelo en tu ios/Podfile:

platform :ios, [min_ios_version_supported, '14.0'].max

Tomar el mayor de los dos sigue funcionando cuando una versión futura de React Native suba su propio mínimo por encima del nuestro.

Después sube también el deployment target de la propia app a 14.0 o superior: en Xcode selecciona el target de la app y luego General → Minimum Deployments → iOS (el ajuste de compilación IPHONEOS_DEPLOYMENT_TARGET en ios/<YourApp>.xcodeproj). Una app nueva de React Native 0.75 se queda ahí en 13.4 incluso después del cambio en el Podfile, y el linker avisa entonces de que los objetos de Joryio se compilaron para una versión de iOS más reciente que la de la app.

Funciones​

  • Puente nativo: envuelve los SDK nativos de iOS (Swift) y Android (Kotlin).
  • Rastreo de eventos: registra eventos personalizados y vistas de pantalla.
  • Identidad de usuario: identifica, añade alias con etiqueta y gestiona atributos de usuario.
  • Notificaciones push: registra tokens y rastrea clics (FCM y APNs).
  • Mensajería in-app: recibe y muestra mensajes in-app.
  • E-commerce: rastrea compras, eventos del carrito y checkout.
  • Compatibilidad sin conexión: los eventos se encolan localmente y se sincronizan al volver a estar en línea.
  • Reintento automático: espera exponencial ante fallos de red.

Requisitos​

  • React Native 0.75+. En iOS el SDK nativo se enlaza como paquete Swift mediante spm_dependency de React Native, que se introdujo en la 0.75. Con Expo, SDK 52 o posterior.
  • Arquitectura antigua y Nueva Arquitectura. En la Nueva Arquitectura el módulo funciona a través de la capa de interoperabilidad (interop layer) de React Native para módulos heredados, activada por defecto.
  • iOS 14.0+ / Android API 23+ (Android 6.0)
  • TypeScript 5.0+ (recomendado)

Instalación​

npm install @joryio/react-native-sdk

Configuración de iOS​

cd ios && pod install

pod install añade el Joryio iOS SDK al proyecto de Pods como paquete Swift (https://github.com/joryio/joryio-ios-sdk, products Joryio y JoryioUI, exactamente en la versión de este paquete), y Xcode lo descarga la primera vez que compila la app, así que esa compilación necesita acceso de red a github.com. Nada viene de CocoaPods trunk, que deja de aceptar versiones nuevas el 2 de diciembre de 2026.

React Native imprime durante pod install dos líneas [SPM] WARNING por cada librería que usa paquetes Swift: una pide reabrir el workspace si usas Xcode 15 o anterior, la otra indica que el paquete se enlaza de forma estática. Si después tu app no enlaza, instala con frameworks dinámicos: USE_FRAMEWORKS=dynamic pod install.

Actualizar desde una versión anterior

Las versiones anteriores tomaban el SDK de iOS de CocoaPods (el pod Joryio/UI). Si añadiste tú mismo una línea pod 'Joryio...' a tu Podfile - por ejemplo un pod con :path que apunta a una copia local -, bórrala y vuelve a ejecutar pod install.

Configuración de Android​

No hay nada que añadir. El paquete se enlaza automáticamente (autolinking - incluye un react-native.config.js), así que PackageList ya registra JoryioPackage; no lo añadas también a mano en MainApplication, porque eso registra el módulo dos veces. El puente depende de io.joryio:joryio-android-ui desde Maven Central, en la misma versión que el paquete.

Expo​

El SDK es un módulo nativo, así que no funciona en Expo Go: usa un development build (npx expo prebuild o EAS Build) con Expo SDK 52 o posterior. El deployment target de iOS que Expo usa por defecto puede ser inferior al 14.0 que necesita el SDK; súbelo con expo-build-properties en app.json:

{
"expo": {
"plugins": [["expo-build-properties", { "ios": { "deploymentTarget": "14.0" } }]]
}
}

Inicio rápido​

import { Platform } from 'react-native';
import Joryio from '@joryio/react-native-sdk';

// Inicializa una vez en App.tsx
await Joryio.initialize(
Platform.select({
ios: 'jry_sdk_ios_YOUR_IOS_KEY', // clave de iOS del panel
android: 'jry_sdk_android_YOUR_ANDROID_KEY', // clave de Android del panel
})!,
'api-eu1.joryio.com', // host de la API (solo el nombre de host, sin esquema)
{
enableDebug: __DEV__,
trackSessionStart: true,
}
);

Cada plataforma tiene su propia clave de SDK: las claves de iOS empiezan por jry_sdk_ios_ y las de Android por jry_sdk_android_. Con una clave que la plataforma rechaza, initialize() registra un aviso y el SDK queda apagado hasta que lo vuelvas a llamar con la clave correcta.

Rastreo de eventos​

Rastrear eventos personalizados​

// Evento básico
Joryio.track('Button Clicked');

// Evento con propiedades
Joryio.track('Product Added', {
productId: 'SKU-123',
productName: 'Blue T-Shirt',
price: 29.99,
currency: 'USD',
});

// E-commerce: usa Joryio.ecommerce (ver E-commerce más abajo) - envía los nombres y las claves que leen los informes de ingresos

Rastrear vistas de pantalla​

// En los componentes de tus pantallas
Joryio.trackScreen('ProductDetail', { productId: 'SKU-123' });
Joryio.trackScreen('Cart');
Joryio.trackScreen('Checkout');

Integración con React Navigation​

import { useRef } from 'react';
import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';

function App() {
const navigationRef = useNavigationContainerRef();
const routeNameRef = useRef<string>();

return (
<NavigationContainer
ref={navigationRef}
onStateChange={() => {
const currentRouteName = navigationRef.current?.getCurrentRoute()?.name;
if (currentRouteName && currentRouteName !== routeNameRef.current) {
Joryio.trackScreen(currentRouteName);
routeNameRef.current = currentRouteName;
}
}}
>
{/* ... */}
</NavigationContainer>
);
}

Identidad de usuario​

Identificar usuarios​

Envía los números de teléfono con + y el código de país

Envía siempre los números de teléfono en formato internacional completo (E.164): +, el código de país y luego el número - por ejemplo +972501234567. Un número sin + se lee con el país predeterminado para números de teléfono del espacio de trabajo; si no hay ninguno, o los dígitos se pueden leer de dos formas, se guarda sin verificar y no se usa para mensajes. Cómo se tratan los números de teléfono

Llama a identify después de iniciar sesión o cuando sepas quién es el usuario:

// Después de iniciar sesión
Joryio.identify('user-123');

// Con atributos
Joryio.identify('user-123');
Joryio.setAttributes({
firstName: 'John',
plan: 'premium',
});

Alias​

Asocia un identificador de otro sistema - un id de CRM, un número de fidelización - a la persona actual. Funciona también con visitantes anónimos: el alias se guarda con el id anónimo y sigue con la persona cuando se identifica.

Joryio.addAlias('crm_id', 'C-1042');

No existe un alias(userId) sin etiqueta; para nombrar a la persona actual, llama a identify(userId).

Restablecer (cerrar sesión)​

Borra la identidad del usuario e inicia una sesión anónima nueva:

Joryio.reset();

Atributos de usuario​

// Establece varios atributos
Joryio.setAttributes({
firstName: 'John',
lastName: 'Doe',
plan: 'premium',
age: 28,
isVIP: true,
});

// Establece un atributo
Joryio.setAttribute('favoriteColor', 'blue');

// Incrementa un atributo numérico
Joryio.incrementAttribute('loginCount', 1);
Joryio.incrementAttribute('totalSpent', 29.99);

// Elimina un atributo
Joryio.unsetAttribute('temporaryFlag');

Un valor null elimina el atributo en ambas plataformas: setAttribute('nickname', null) y setAttributes({ nickname: null }) equivalen a unsetAttribute('nickname').

E-commerce​

Rastrea las compras y los pasos previos con Joryio.ecommerce. Envía los nombres de evento y las claves de propiedad estándar que leen los informes de ingresos de Joryio, así que no escribes ninguno a mano:

Joryio.ecommerce.viewProduct({ productId: 'SKU-123', name: 'Blue T-Shirt', price: 29.99 });
Joryio.ecommerce.addToCart({ productId: 'SKU-123', name: 'Blue T-Shirt', price: 29.99, quantity: 2 });
Joryio.ecommerce.startCheckout(
[{ productId: 'SKU-123', name: 'Blue T-Shirt', price: 29.99, quantity: 2 }],
59.98, // valor del carrito
);

// se envía como "Order Completed" con orderId y total - las claves que leen los informes de ingresos
Joryio.ecommerce.purchase({
orderId: 'ORD-789',
items: [{ productId: 'SKU-123', name: 'Blue T-Shirt', price: 29.99, quantity: 2 }],
value: 59.98,
currency: 'USD',
});

purchase envía un evento Order Completed con orderId y total (el value del pedido): las dos claves que leen los ingresos y la atribución. Un track('Order Completed', { order_id, value }) escrito a mano se guarda tal cual y cuenta como un pedido de 0 sin id de pedido: a propósito no hay una capa de alias que traduzca otras grafías. Si rastreas una compra por tu cuenta, usa orderId y total.

El resto de llamadas: viewCategory(categoryId, categoryName), search(query, resultCount?), removeFromCart(product, quantity?), cartViewed(items, cartValue), updateCart(items, cartValue), addPaymentInfo(paymentMethod), orderFulfilled(orderId, trackingNumber?, carrier?), orderDelivered(orderId), orderCancelled(orderId, reason?), orderRefunded(orderId, refundAmount?, reason?), addToWishlist(product), removeFromWishlist(product), shareProduct(product, shareMethod), applyCoupon(couponCode, discountAmount?, discountType?), removeCoupon(couponCode), submitReview(productId, rating, reviewText?). ecommerce también es una exportación con nombre: import { ecommerce } from '@joryio/react-native-sdk'.

Una llamada a la que le falta un argumento obligatorio - un purchase sin orderId, items o un value numérico, un producto sin productId, un carrito sin items - registra un aviso y no envía nada, así que nunca queda registrada como un pedido de 0.

Notificaciones push​

Cuándo pedirlo

Pide el permiso cuando la persona tenga un motivo para aceptar. Una solicitud en los primeros segundos de una sesión es la que más se rechaza, y revertir un rechazo es mucho más difícil que retrasar la petición: la solicitud del sistema es de una sola vez, así que un "no" ahí es prácticamente permanente, mientras que pedirlo más tarde no te cuesta nada.

Esto aplica sea cual sea el componente que pida el permiso. Si lo pide Firebase Messaging, como abajo, el momento lo eliges tú; si lo pide el SDK de Joryio, no preguntará por ti salvo que actives requestPushPermissionAtLaunch, que está desactivada por defecto, no ausente.

Configuración con Firebase (React Native Firebase)​

import { Platform } from 'react-native';
import messaging from '@react-native-firebase/messaging';

// Solicita permiso
const authStatus = await messaging().requestPermission();

// Obtén y registra el token
// Joryio envía el push de iOS a través de APNs, así que en iOS registra el token de APNs, no el de FCM.
const token = Platform.OS === 'ios'
? await messaging().getAPNSToken()
: await messaging().getToken();
if (token) {
const result = await Joryio.registerPushToken(token);
if (!result.success && result.isRetryable) {
// transitorio (sin conexión, DNS, 5xx): vuelve a registrar cuando vuelva la red
}
}

// Escucha la actualización del token
messaging().onTokenRefresh((newToken) => {
// onTokenRefresh entrega el token de FCM - también en iOS, en cada inicio de la app.
// Regístralo solo en Android; en iOS el correcto es el token de APNs de arriba.
if (Platform.OS === 'android') Joryio.registerPushToken(newToken);
});

En iOS registra solo el token de dispositivo de APNs. onTokenRefresh de React Native Firebase entrega el token de FCM también en iOS, en cada inicio de la app, y registrarlo reemplazaría el token de APNs válido del dispositivo por uno que APNs rechaza - por eso regístralo solo en Android, como arriba. En iOS, registerPushToken rechaza un token que no sea de APNs (hexadecimal) con success: false e isRetryable: false.

Si el usuario desactivó el push y llamaste a unregisterPush(), no registres tokens desde callbacks automáticos (onTokenRefresh, un getToken() al arrancar) hasta que lo vuelva a activar: registerPushToken() es el "push activado" explícito y anula el unregister. Comprueba tu propio ajuste de push antes de registrar.

registerPushToken resuelve con el resultado (success, message, isUnauthorized, isRetryable). Nunca rechaza, así que ignorar la promesa mantiene el comportamiento anterior - pero un fallo transitorio deja el dispositivo sin registrar hasta el siguiente arranque, así que comprueba isRetryable y vuelve a registrar. getDiagnostics().lastError lleva la misma marca isRetryable.

Gestionar clics en notificaciones push​

import messaging from '@react-native-firebase/messaging';

// Cuando la aplicación está en segundo plano y se toca la notificación
messaging().onNotificationOpenedApp((remoteMessage) => {
const trackingId = remoteMessage.data?.trackingId;
if (trackingId) {
Joryio.trackPushClick(trackingId);
}
});

// Cuando la aplicación estaba cerrada y se abre desde una notificación
messaging()
.getInitialNotification()
.then((remoteMessage) => {
if (remoteMessage?.data?.trackingId) {
Joryio.trackPushClick(remoteMessage.data.trackingId);
}
});

Confirmaciones de entrega​

Cuando tu propia librería de mensajería recibe el push (React Native Firebase, Expo notifications), el SDK nativo no lo ve, así que informa tú de la entrega con el trackingId de los datos del mensaje:

messaging().onMessage(async (msg) => {
if (msg.data?.trackingId) Joryio.reportPushDelivered(String(msg.data.trackingId));
});
messaging().setBackgroundMessageHandler(async (msg) => {
if (msg.data?.trackingId) Joryio.reportPushDelivered(String(msg.data.trackingId));
});

Anular el registro​

Joryio.unregisterPush();

unregisterPush() detiene los push a este dispositivo. Sigue vigente tras reiniciar la app hasta que vuelvas a llamar a registerPushToken().

Comprobar el estado de push​

const enabled = await Joryio.isPushEnabled();
console.log('Push enabled:', enabled);

Pedir el permiso​

if ((await Joryio.getPushPermissionStatus()) === 'notDetermined') {
// muestra primero tu propia explicación y después:
const status = await Joryio.requestPushPermission(); // 'granted' | 'denied' | 'notDetermined' | 'unavailable'
}

requestPushPermission() se comporta igual en iOS y Android:

  • Siempre se resuelve. En Android, si la Activity se recrea mientras el aviso está en pantalla (una rotación, un cambio de tema), se resuelve con el estado actual cuando la app vuelve al primer plano.
  • Si el usuario ha rechazado el seguimiento, el aviso se muestra igualmente y se devuelve el resultado - que la app pueda enviar notificaciones es decisión del usuario en el sistema operativo - pero no se registra ni se comunica nada a Joryio mientras siga rechazado.

Mensajería in-app​

Los mensajes se muestran solos. Instala el SDK, envía una campaña y aparece, dibujada por las vistas nativas con las fuentes, los colores y el modo oscuro de tu app. No hay nada que conectar.

Solo necesitas el resto de esta sección si prefieres renderizar los mensajes en React; consulta Encargarte tú del renderizado.

Tokens de entrega (delivery tokens)​

Cuando el backend sirve una campaña elegible, emite también un token de entrega firmado y de corta duración. El SDK lo devuelve al informar una impresión, un clic o un cierre, y el servidor verifica la firma antes de registrar nada.

No tienes que hacer nada: el SDK se encarga por ti. Se documenta porque el servidor puede configurarse para exigirlo. Por defecto, un informe sin token sigue aceptándose - el servidor lo registra como un cliente anterior a los tokens - y solo cuando el backend se ejecuta con IN_APP_REQUIRE_DELIVERY_TOKEN=true ese mismo informe se rechaza:

POST /v1/in-app/track (no deliveryToken)
{ "success": false, "error": "A delivery token is required" }

Un token presente pero que no se verifica - falsificado, manipulado o caducado - se rechaza siempre, sea cual sea la configuración.

El token es lo que hace que una impresión sea fiable: sin él, cualquiera que tenga la SDK key - que viaja dentro de cada app y cada página - podría informar impresiones y clics de una campaña que nunca se mostró, y tus informes los contarían.

Si dejan de registrarse impresiones in-app en un backend que exige tokens, comprueba que la app esté compilada contra una versión actual del SDK: una compilación anterior a los tokens de entrega no envía token.

Dos tipos de contenido​

Cada mensaje lleva un kind que indica su forma. Ramifica según él:

kindQué llevaCómo renderizarlo
'native'title, body, imageUrl, buttonsComponentes de React Native: <Text>, <Image>, <Pressable>
'html'html, cssUn WebView

Encargarte tú del renderizado​

Suscribirte con onInAppMessage detiene el dibujado del SDK y entrega cada mensaje a tu código, para que lo renderices con componentes de React Native. No recibirás dos copias.

Declara lo que tu renderizador puede dibujar:

Joryio.onInAppMessage(handleMessage, {
capabilities: ['content.native'], // añade 'content.html' solo si tienes un web view
});

Las campañas se segmentan según lo que informa el dispositivo, y una capacidad no declarada se interpreta como "no puede", no como "desconocido". Si omites la opción se mantiene el comportamiento anterior, en el que el valor venía de un ajuste nativo que describe las vistas del propio SDK nativo, que no se ejecutan en cuanto te suscribes.

Hazlo si quieres que los mensajes in-app encajen con el resto de tu interfaz. El propio puente depende de los artefactos de UI nativos (io.joryio:joryio-android-ui en Android, el product JoryioUI del paquete Swift en iOS), así que siguen enlazados; suscribirte solo hace que dejen de dibujar.

Al eliminar tu última suscripción, el renderizado vuelve al SDK nativo, de modo que los mensajes que lleguen después se muestran de forma nativa en lugar de perderse. Una lista capabilities vacía equivale a omitirla (no declarada) en ambas plataformas.

Las acciones de los botones son dismiss, url, deep_link y request_push_permission - el «sí» de un mensaje previo al permiso de push: cierra el mensaje y llama a Joryio.requestPushPermission().

Cuándo suscribirte​

Desde la versión 1.3.2. Suscríbete antes de que initialize() haya terminado - a nivel de módulo, o en un effect que se ejecute al arrancar - y ningún mensaje se dibuja de forma nativa, ni siquiera el primero: el SDK retiene los mensajes in-app hasta que initialize() ha terminado y tu suscripción ha llegado al SDK nativo, entrega el mensaje retenido a tu listener y solo devuelve el renderizado al SDK nativo si nadie se suscribió.

Si te suscribes más tarde - en una pantalla que se monta cuando initialize() ya ha terminado - pasa holdInAppMessagesForHandler: true a initialize(). Los mensajes que estén listos antes de que te suscribas se retienen (no se dibujan, no se informan y no cuentan para ningún límite de frecuencia) y se entregan a tu listener cuando se suscribe. Si en su lugar llamas a disableInAppMessages(), la retención termina y el SDK nativo dibuja el mensaje retenido.

En Android el puente entrega al SDK nativo la Activity actual en initialize(), así que los mensajes in-app pueden mostrarse ya en la primera sesión.

Escuchar mensajes​

import { useEffect, useState } from 'react';
import Joryio, { type InAppMessage } from '@joryio/react-native-sdk';

function App() {
const [message, setMessage] = useState<InAppMessage | null>(null);

useEffect(() => Joryio.onInAppMessage(setMessage), []);

return <>{message && <InAppMessageHost message={message} onClose={() => setMessage(null)} />}</>;
}

Renderizar un mensaje nativo​

El contenido nativo es texto, no marcado. Ponlo en un <Text>: pasarlo a un WebView o a dangerouslySetInnerHTML reintroduciría exactamente el riesgo de inyección que lo nativo evita.

function InAppMessageHost({ message, onClose }: { message: InAppMessage; onClose: () => void }) {
// Informa de 'displayed' una vez, cuando aparece el mensaje.
useEffect(() => {
Joryio.trackInAppImpression(message.id, 'displayed');
}, [message.id]);

// Informa de 'dismissed' siempre que el mensaje sale de la pantalla - también tras un clic.
const close = () => {
Joryio.trackInAppImpression(message.id, 'dismissed');
onClose();
};

if (message.kind === 'native') {
return (
<View>
{message.imageUrl && <Image source={{ uri: message.imageUrl }} />}
{message.title && <Text style={styles.title}>{message.title}</Text>}
<Text>{message.body}</Text>

{message.buttons.map((button) => (
<Pressable
key={button.id}
onPress={() => {
Joryio.trackInAppImpression(message.id, 'clicked', button.id);
if (button.action === 'url' && button.url) Linking.openURL(button.url);
if (button.action === 'request_push_permission') void Joryio.requestPushPermission();
close();
}}
>
<Text>{button.text}</Text>
</Pressable>
))}
</View>
);
}

// Escapa `</` para que el CSS de la campaña no pueda cerrar la etiqueta <style>.
const css = message.css.replace(/<\//g, '<\\/');
return <WebView source={{ html: `<style>${css}</style>${message.html}` }} />;
}

Aplica el estilo de la campaña​

message.style lleva las anulaciones que fijó quien creó la campaña. Todos los campos son opcionales y uno ausente significa heredar el aspecto de tu propia aplicación: aplica solo lo que venga.

const st = message.style;
const text = {
// 'auto' alinea según el idioma del MENSAJE, no el del dispositivo.
writingDirection: 'auto' as const,
textAlign: st?.textAlign === 'center' ? 'center' : 'auto',
...(st?.fontFamily ? { fontFamily: st.fontFamily } : {}),
};

Aplica fontFamily solo si tu aplicación incluye esa fuente. Si la campaña fija un color de botón y ninguno de etiqueta, elige negro o blanco por contraste: el blanco por defecto hace invisible un relleno claro.

Permitir mensajes HTML​

Los mensajes HTML están desactivados por defecto. El SDK nativo muestra un mensaje HTML en un web view dentro de tu app. Los scripts escritos en el mensaje no se ejecutan ahí - solo se ejecuta el puente del SDK, que gestiona botones y formularios - pero activar un web view sigue siendo una decisión del equipo de la app:

await Joryio.initialize(sdkKey, apiHost, {
allowHtmlJsInAppMessages: true, // por defecto: false
});

Dejarlo desactivado no desactiva la mensajería in-app: los mensajes nativos siguen llegando. Solo se omiten las campañas HTML.

Eso vale mientras sea el SDK quien dibuja. En cuanto llames a enableInAppMessages(['content.native', 'content.html']), tu JS renderiza cada mensaje y este ajuste ya no bloquea el HTML: lo que declares en esa llamada es el consentimiento.

Rastrear impresiones​

// Cuando se muestra el mensaje
Joryio.trackInAppImpression(message.id, 'displayed');

// Cuando el usuario hace clic
Joryio.trackInAppImpression(message.id, 'clicked', button.id);

// Cuando el usuario lo descarta
Joryio.trackInAppImpression(message.id, 'dismissed');

Informa de 'displayed' una vez, cuando aparece el mensaje. Ese único informe registra la impresión y es lo que cuenta para los límites de frecuencia de la campaña: entregar el mensaje a tu código no hace ninguna de las dos cosas, y el SDK no lo informa por ti. En un clic, pasa el id del botón o enlace como tercer argumento, elementId, para que los informes digan qué botón se pulsó; omítelo en displayed y dismissed.

Informa de 'dismissed' siempre que el mensaje sale de la pantalla, también después de un clic. Hasta entonces el mensaje ocupa el hueco de visualización: en iOS no se muestra ningún otro mensaje in-app, y en Android la misma campaña no se vuelve a mostrar. Si 'dismissed' no llega nunca, el SDK libera el hueco a los 5 minutos.

Formularios en mensajes HTML​

Cuando renderizas tú un mensaje HTML, envía el <form data-jry-form> que contiene a través del SDK, para que los valores viajen con la identidad actual y se validen contra el formulario que lleva la campaña:

const result = await Joryio.submitInAppForm(message.id, { email: '[email protected]' });
// { ok, message?, redirect?, errors? } - nunca rechaza; un fallo es ok: false

Sincronización y pruebas​

Joryio.syncInAppCampaigns(); // descarga campañas ahora y muestra las que sean elegibles
Joryio.resetDisplayedCampaigns(); // solo para pruebas: olvida lo que este dispositivo ya mostró

El SDK ya sincroniza al iniciar la sesión y cuando la app vuelve al primer plano; llama a syncInAppCampaigns() para los momentos intermedios. resetDisplayedCampaigns() solo se ejecuta con enableDebug activado.

enableInAppMessages(capabilities?) y disableInAppMessages() son el par de bajo nivel que onInAppMessage llama por ti; úsalos solo si escuchas el evento JoryioInAppMessage de otra forma. Cuando JavaScript se recarga, el puente devuelve el renderizado al SDK nativo hasta que tu código vuelva a suscribirse.

Cuándo un mensaje cuenta como mostrado​

Un mensaje cuenta como mostrado - se envía in_app.displayed y se consume el límite de frecuencia de la campaña - solo cuando la persona pudo verlo de verdad. El tiempo que debe estar en pantalla es IN_APP_DISPLAY_DWELL_MS (1000 ms), la misma constante en todos los SDK de Joryio.

  • Los mensajes que dibuja el SDK nativo siguen la regla de iOS y Android: se muestran solo en una pantalla estable en primer plano y cuentan después de 1 segundo en pantalla con la app en primer plano. Un toque o un cierre antes cuenta como visto; un mensaje que sale de la pantalla antes no cuenta y sigue siendo elegible.
  • Si dibujas los mensajes tú mismo (onInAppMessage), envía 'displayed' con la misma regla - IN_APP_DISPLAY_DWELL_MS se exporta: cuando el mensaje lleve visible ese tiempo, o justo antes de un clic o un descarte que llegue antes. No lo envíes para un mensaje que salió antes de la pantalla.

Consentimiento de seguimiento​

Dos interruptores independientes deciden si el SDK recopila algo, y cualquier "no" gana:

  • Consentimiento - la respuesta de tu banner de consentimiento: 'granted', 'denied' o 'pending'. Se fija con trackingConsent en initialize() o con updateConsent().
  • Rechazo (opt-out) - la decisión de la propia persona. Se fija con optOut() y solo se levanta con optIn().

El SDK solo recopila mientras el consentimiento es granted y la persona no ha rechazado el seguimiento. Conceder el consentimiento nunca cancela un rechazo, y rechazar nunca cambia el consentimiento guardado.

Detener y reanudar la recogida. Es consentimiento de seguimiento, no una suscripción de marketing: optOut() no da de baja a nadie del correo ni del SMS. Eso vive en el estado de suscripción del contacto.

// El usuario rechazó el seguimiento
Joryio.optOut();

// ...y más tarde aceptó
Joryio.optIn();

const optedOut = await Joryio.isUserOptedOut(); // solo el interruptor de rechazo
const consent = await Joryio.getTrackingConsent(); // 'granted' | 'denied' | 'pending'

wipeData() borra todo lo que el dispositivo guarda de la persona actual - identidad, eventos en cola, escrituras pendientes, estado de los mensajes in-app, sesión, id de dispositivo y token de push - y anula el registro del token en el servidor. La exclusión (opt-out) se conserva. El push sigue desactivado hasta registerPushToken() o una concesión de permiso.

optOut() registra la decisión en el perfil como tracking_opted_out antes de dejar de enviar, de modo que una audiencia pueda excluir a quien la rechazó. optIn() lo limpia. Es una señal para la segmentación, no una restricción.

Los eventos ya en cola y no enviados se descartan con optOut(), no se guardan para después.

Para decidir antes de que se envíe nada, pasa el consentimiento a initialize() y actualízalo desde tu aviso de consentimiento:

await Joryio.initialize(sdkKey, apiHost, { trackingConsent: 'pending' });

// más tarde
Joryio.updateConsent('granted'); // 'granted' | 'denied' | 'pending'
  • Deja trackingConsent sin definir salvo que tu banner tenga una respuesta para este arranque. Sin definir y sin nada guardado, se comporta como granted; un denied guardado en un arranque anterior sigue vigente.
  • denied se guarda en el dispositivo y se mantiene tras reiniciar. Solo updateConsent('granted') o un trackingConsent: 'granted' explícito lo levantan.
  • pending vale solo para este arranque y nunca sustituye a un denied guardado. Mientras el consentimiento sea pending no se envía nada: los eventos y los cambios de atributos esperan en el dispositivo (hasta maxQueueSize), y no hay inicio de sesión, sincronización in-app ni registro de push. updateConsent('granted') envía lo que esperaba; updateConsent('denied') lo descarta.
  • Pasa optOut: true para empezar con el seguimiento rechazado. El rechazo se guarda en el dispositivo y se mantiene tras reiniciar; optOut: false no deshace un optOut() anterior - llama a optIn().
  • Si tu app ya conoce la respuesta al arrancar - por ejemplo, una elección del banner que guardó ella misma - pásala como trackingConsent a initialize() en lugar de llamar a updateConsent() después, para que el SDK arranque con esa respuesta en vigor. Las mayúsculas no importan ('Denied' se lee como 'denied'); un valor desconocido se ignora con un aviso y el consentimiento queda sin definir.

optOut(), optIn(), updateConsent() y wipeData() se pueden llamar antes de initialize(), y se aplican primero: ningún inicio de sesión, sincronización in-app ni registro de push sale antes que ellas. Las llamadas anteriores que recopilan datos - eventos, atributos, alias, cambios de suscripción - se descartan con un optOut(), updateConsent('denied') o wipeData() posterior, en lugar de enviarse cuando arranca el SDK. Las llamadas de ese tipo hechas mientras hay opt-out o denegación - también antes de initialize(), tanto si la decisión se tomó en este arranque como si quedó guardada de uno anterior - se descartan igualmente, y un optIn() o updateConsent('granted') posterior no las envía. identify solo lo descarta wipeData(). Un setSdkAuthenticationToken() anterior nunca pasa al siguiente visitante: en iOS y en Android por igual el borrado lo conserva solo para las retiradas de la propia persona borrada, o lo descarta. Tras un optOut() o una denegación el id de usuario se guarda en el dispositivo, sin enviarse, solo para que una baja llegue a la persona correcta. Las retiradas son la excepción: setSubscription(channel, 'unsubscribed'), removeFromSubscriptionGroup y unregisterPush() hechos antes se conservan y se envían.

Una decisión tomada mientras se ejecuta un initialize() que luego falla se aplica en el momento del fallo, antes del siguiente initialize(): el reintento empieza con ella en vigor.

isUserOptedOut() y getTrackingConsent() también se pueden llamar antes de initialize(). Mientras initialize() se está ejecutando lo esperan; en cualquier otro caso - antes de initialize() y después de uno que falló - responden al momento con lo guardado en el dispositivo, así que un rechazo o un denied de un arranque anterior se ven desde el principio. Sin ninguna decisión tomada ni guardada, getTrackingConsent() responde 'granted', el estado en que funciona el SDK, en ambas plataformas.

Opciones de configuración​

OpciónTipoPredeterminadoDescripción
enableDebugbooleanfalseActiva el registro de depuración
logLevelcadenaderivado de enableDebugNivel de registro: debug, info, warn, error. Si no se define, ambos puentes nativos usan 'debug' cuando enableDebug es true y 'error' en caso contrario - no hay un valor predeterminado fijo. Defínelo solo para anular eso.
batchSizenúmero50Eventos por lote antes del vaciado automático (predeterminado nativo)
flushIntervalnúmero5000Intervalo de vaciado automático en ms (predeterminado nativo)
sendImmediatelybooleanfalseEnvía cada evento en cuanto se registra, sin lotes (no recomendado en producción)
maxQueueSizenúmero1000Máximo de eventos que retiene la cola local mientras fallan los envíos; por encima se descartan los más antiguos (predeterminado nativo)
requestTimeoutnúmero10000Tiempo de espera HTTP por petición en ms (predeterminado nativo)
sessionTimeoutnúmero1800000Tiempo de espera de sesión en ms (predeterminado nativo: 30 min)
trackSessionStartbooleantrueRastrea automáticamente los eventos de inicio de sesión
userIdcadenanullID de usuario predefinido al inicializar
anonymousIdcadenageneradoTu propio id anónimo para este dispositivo. Se usa en esta ejecución y se guarda solo cuando aún no hay nada guardado, así un id definido una vez sobrevive aunque la app deje de pasar la opción
requestPushPermissionAtLaunchbooleanofalsePide el permiso de notificaciones durante initialize. Mejor pedirlo en un momento en que el usuario entienda por qué
allowHtmlJsInAppMessagesbooleanofalsePermite que el SDK nativo dibuje mensajes in-app HTML (ver Permitir mensajes HTML)
holdInAppMessagesForHandlerbooleanofalseDesde la versión 1.3.2. Tu app dibuja los mensajes in-app y se suscribe cuando initialize() ya ha terminado: retiene los mensajes hasta entonces (ver Cuándo suscribirte)
enableSdkAuthenticationbooleanofalseAdjunta un JWT emitido por ti a cada petición de envío de datos
sdkAuthenticationTokencadena-El JWT inicial cuando enableSdkAuthentication está activado
trackingConsent'granted' | 'denied' | 'pending'sin definirConsentimiento de seguimiento al iniciar. Déjalo sin definir salvo que tengas una respuesta de tu banner de consentimiento - ver Consentimiento al iniciar
optOutbooleanfalseEmpezar con el seguimiento rechazado
notificationChannelNamestring"Notifications"Solo Android: el nombre del canal de notificaciones que la gente ve en los ajustes del sistema (Android 8+). Pásalo traducido. Se ignora en iOS

Utilidades​

// Vacía los eventos de inmediato (antes de cerrar la aplicación, cerrar sesión, etc.)
Joryio.flush();

// Obtén los ID
const anonymousId = await Joryio.getAnonymousId();
const userId = await Joryio.getUserId(); // null si no se identificó
const sessionId = await Joryio.getSessionId();

Compatibilidad con TypeScript​

El SDK está completamente tipado. Importa los tipos que necesites:

import Joryio, {
type JoryioConfig,
type UserAttributes,
type InAppMessage,
} from '@joryio/react-native-sdk';

Suscripción de marketing​

Si se puede enviar mensajes a esta persona en un canal. Es un consentimiento distinto de optOut(), que trata de si se la puede observar: ninguno implica el otro.

Esto es lo que escribe un centro de preferencias dentro de la app.

Joryio.setSubscription('email', 'unsubscribed');

Los grupos de suscripción son listas a las que una persona puede unirse o de las que puede salir en cada canal:

Joryio.addToSubscriptionGroup('list_abc123', 'email');
Joryio.removeFromSubscriptionGroup('list_abc123', 'email');

Canales: email, sms, whatsapp, push, viber. Estados: optedIn, subscribed, unsubscribed.

Se requiere un identificador: la llamada se omite, con un error registrado, si no hay ni user id ni anonymous id.

Autenticación del SDK​

Opcional. Cuando está activa, una petición que lleva un id de usuario debe llevar también un JWT que tu backend emitió para ese usuario; las llamadas de visitantes anónimos se aceptan sin él.

await Joryio.initialize(sdkKey, apiHost, {
enableSdkAuthentication: true,
sdkAuthenticationToken: initialToken,
});

const stop = Joryio.onSdkAuthenticationError(async (error) => {
// error.reason: 'missing' | 'invalid' | 'expired' | 'sub_mismatch' | 'unknown'
// error.refreshExhausted: true cuando el SDK ha dejado de reintentar esta petición
Joryio.setSdkAuthenticationToken(await fetchFreshToken());
});

Llama a setSdkAuthenticationToken(token) tras el inicio de sesión y siempre que tu handler tenga un token nuevo; se adjunta a cada petición posterior. onSdkAuthenticationError devuelve una función para cancelar la suscripción.

Diagnóstico​

const { apiEndpoint, lastError } = await Joryio.getDiagnostics();
// lastError: { message, isUnauthorized, isRetryable, at } - vuelve a null cuando una llamada tiene éxito

Úsalo cuando una integración no se comporta como esperas: una clave de SDK incorrecta parece inicializada mientras cada petición falla con isUnauthorized: true.

Seguro ante null y llamadas tempranas​

  • Toda llamada es segura antes de que initialize() termine: se retienen hasta 1.000 llamadas y se reproducen en orden, y los getters esperan hasta 5 segundos antes de resolver un valor por defecto. Pasadas las 1.000 se descartan las más antiguas, primero las de datos: una baja, unregisterPush() o una decisión de privacidad solo se descarta cuando no queda nada más.
  • null o undefined nunca hacen fallar tu app. Un valor de configuración null significa el valor por defecto; una llamada a la que le falta un argumento obligatorio (un nombre de evento, un id de usuario, un token) registra un aviso y no hace nada; un valor de atributo null elimina el atributo.
  • Si initialize() falla, las llamadas se retienen igual y se envían cuando un initialize() posterior tiene éxito: puedes volver a llamarlo con valores corregidos. Los listeners que suscribiste (onInAppMessage, onSdkAuthenticationError) y las bajas hechas mientras tanto se conservan, no se pierden. Una decisión de privacidad tomada mientras ese initialize() seguía en marcha se aplica en el momento del fallo, así que el reintento empieza con ella.
  • Dos llamadas a initialize() superpuestas - React StrictMode ejecuta los efectos dos veces - comparten un único arranque. Una llamada posterior se ignora y el SDK sigue funcionando; para aplicar ajustes cambiados hay que reiniciar la app.
  • NaN e Infinity nunca se envían: un valor de atributo o una propiedad de evento que contenga uno se omite con un aviso, y el resto de la llamada sigue adelante. setAttribute(key, NaN) se ignora en lugar de eliminar el atributo. También se revisan los valores dentro de instancias de tus propias clases - por ejemplo, una línea de carrito creada a partir de una clase.
  • Un callback que pasas a onInAppMessage o onSdkAuthenticationError y que lanza un error (o devuelve una promesa rechazada) se captura y se registra. Nunca hace fallar tu app ni detiene a los demás listeners.

Referencia de la API​

ÁreaMétodos
Configuracióninitialize(sdkKey, apiHost, config?)
Eventostrack(eventName, properties?), trackScreen(screenName, properties?), flush()
Identidadidentify(userId), addAlias(aliasLabel, aliasName), reset(), getUserId(), getAnonymousId(), getSessionId()
AtributossetAttributes(attributes), setAttribute(key, value), incrementAttribute(key, by?), unsetAttribute(key)
PushregisterPushToken(token), unregisterPush(), reportPushDelivered(trackingId), trackPushClick(trackingId), getPushPermissionStatus(), requestPushPermission(), isPushEnabled()
In-apponInAppMessage(callback, options?), trackInAppImpression(campaignId, action, elementId?), submitInAppForm(campaignId, values), syncInAppCampaigns(), resetDisplayedCampaigns(), enableInAppMessages(capabilities?), disableInAppMessages()
ConsentimientooptOut(), optIn(), isUserOptedOut(), getTrackingConsent(), updateConsent(consent), wipeData()
SuscripcionessetSubscription(channel, status), addToSubscriptionGroup(groupId, channel), removeFromSubscriptionGroup(groupId, channel)
Autenticación del SDKsetSdkAuthenticationToken(token), onSdkAuthenticationError(callback)
E-commerceJoryio.ecommerce.* - ver E-commerce
DiagnósticogetDiagnostics()

Pruebas con Jest​

El main del paquete es JavaScript compilado, así que tu configuración de Jest no necesita una entrada en transformIgnorePatterns para él. Sin un mock de NativeModules.JoryioModule, cada llamada no hace nada y los getters devuelven sus valores por defecto; ninguna llamada lanza errores.