Saltar al contenido principal

Integración del SDK de iOS

SDK nativo de iOS para rastrear eventos, gestionar sesiones de usuario, enviar notificaciones push y mostrar mensajes in-app.

Funciones​

  • Ligero: ocupa poco espacio.
  • Rápido: optimizado para el rendimiento.
  • Compatibilidad sin conexión: cola de eventos basada en SQLite.
  • Reintento automático: espera exponencial ante fallos.
  • Procesamiento por lotes: agrupación eficiente de eventos (50 eventos / 5 s).
  • Mensajería in-app: mensajes nativos y HTML, 5 tipos, con límite de frecuencia.
  • Notificaciones push: Apple Push Notification Service (APNS).
  • Privacidad primero: cumple el RGPD y respeta el consentimiento de los usuarios.
  • iOS 14+: compatible con versiones modernas de iOS.

Requisitos​

  • iOS 14.0+
  • Xcode 15.0+
  • Swift 5.9+
Llamar desde Objective-C

La API del SDK es solo Swift, por lo que no se puede llamar directamente desde un archivo .m o .mm. Una app de Objective-C lo integra añadiendo un único archivo Swift que importa Joryio y expone las llamadas que necesita, y luego lo llama desde Objective-C con normalidad. Xcode configura el bridging header la primera vez que añades un archivo Swift a un objetivo Objective-C.

Instalación​

Swift Package Manager (recomendado)​

El SDK se distribuye como paquete Swift desde https://github.com/joryio/joryio-ios-sdk. Es la forma recomendada de instalarlo; CocoaPods está obsoleto (consulta CocoaPods). Añade lo siguiente a Package.swift:

dependencies: [
.package(url: "https://github.com/joryio/joryio-ios-sdk.git", from: "1.3.3")
]

El paquete ofrece tres products. Enlaza los dos primeros en tu app según los necesites; el tercero es solo para una Notification Service Extension (consulta Imágenes en rich push):

.target(
name: "YourApp",
dependencies: [
.product(name: "Joryio", package: "joryio-ios-sdk"), // tracking, identidad, push
.product(name: "JoryioUI", package: "joryio-ios-sdk"), // + visualización de mensajes in-app
]
)

O desde Xcode:

  1. Archivo → Añadir dependencias de paquetes.
  2. Escribe https://github.com/joryio/joryio-ios-sdk.git.
  3. Selecciona la versión y añádela al target.

CocoaPods (obsoleto)​

Obsoleto - usa Swift Package Manager

CocoaPods está obsoleto para el SDK de iOS de Joryio en favor de Swift Package Manager. CocoaPods trunk, el registro del que descarga un pod 'Joryio' simple, está cerrando (solo lectura desde el 2 de diciembre de 2026), así que no se garantiza que las versiones nuevas estén allí. Si tu app tiene que seguir en CocoaPods, instala desde la etiqueta git del SDK como abajo: no usa trunk, y toda versión desde la 1.3.0 lo admite, betas incluidas. Fija la versión con :tag y cambia la etiqueta para actualizar - pod update no la mueve.

Para pasar a Swift Package Manager: borra las líneas pod 'Joryio...' de tu Podfile, ejecuta pod install y añade el paquete como se describe arriba. Nada más cambia en tu código - salvo que una Notification Service Extension escribe import JoryioNotificationServiceExtension en lugar de import Joryio.

pod 'Joryio/UI', :git => 'https://github.com/joryio/joryio-ios-sdk.git', :tag => '1.3.3' # todo, incluida la visualización in-app
# pod 'Joryio', :git => 'https://github.com/joryio/joryio-ios-sdk.git', :tag => '1.3.3' # solo tracking, identidad y push: sin WebKit enlazado

# Solo en un target de Notification Service Extension:
# pod 'Joryio/NotificationServiceExtension', :git => 'https://github.com/joryio/joryio-ios-sdk.git', :tag => '1.3.3'
¿Cuál elegir?

JoryioUI depende de Joryio, así que enlazar el producto de UI te da ambos.

Usa Joryio solo cuando tu app no necesite mensajes in-app, o cuando una revisión de seguridad se oponga a enlazar un web view. Ojo: el valor por defecto en CocoaPods es Joryio sin UI, así que pod 'Joryio' no mostrará mensajes in-app hasta que lo cambies por pod 'Joryio/UI'.

Manifiesto de privacidad​

El SDK incluye el manifiesto de privacidad de Apple (PrivacyInfo.xcprivacy) en el paquete de Swift y en el pod de CocoaPods. Declara la única API de motivo obligatorio (required-reason) que usa el SDK - UserDefaults, motivo CA92.1 - y los datos que recoge el SDK: ID de usuario, ID de dispositivo, interacción con el producto e historial de compras, cada uno vinculado al usuario y no usado para tracking. Todos se declaran para App Functionality, Analytics y Developer's Advertising or Marketing, y todos salvo el ID de dispositivo también para Product Personalization. No se declara ninguna ubicación: el SDK nunca lee la ubicación del dispositivo, y el país que el backend de Joryio deduce de la dirección IP no es un dato que recoja el SDK. Xcode lo incluye en el informe de privacidad de tu app. Los atributos y las propiedades de eventos que decidas enviar los declaras tú en el manifiesto de tu app.

Inicio rápido​

1. Inicializa el SDK​

En AppDelegate.swift:

import Joryio

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {

// Inicializa con tu clave de SDK
Joryio.shared.initialize(
sdkKey: "jry_sdk_ios_YOUR_SDK_KEY",
apiHost: "https://api-eu1.joryio.com/api"
)

return true
}
Encontrar tu clave de SDK

Encuentra tu clave de SDK en el dashboard de Joryio: Configuración → Apps → [Tu app] → Claves de SDK.

Todos los métodos públicos se pueden llamar con seguridad antes de initialize(). optOut(), optIn(), wipeData(), updateConsent(_:), setSdkAuthenticationToken(_:), setSdkAuthenticationErrorHandler(_:) y setInAppMessageCallback(_:capabilities:) se recuerdan y se aplican cuando el SDK arranca. Las llamadas que recopilan o envían - track, identify, atributos, addAlias, suscripciones, reset, flush, las llamadas de token push, recibo de entrega, clic y baja, y los informes in-app - se ponen en cola (como máximo 1000, se descarta primero la más antigua, el mismo límite en todos los SDK de Joryio) y se ejecutan en orden en cuanto initialize() tiene éxito. Hasta entonces, los getters devuelven valores vacíos. Una llamada que recopila datos - un evento, un atributo, un alias, una suscripción, un recibo o clic de push, un informe in-app - hecha mientras el usuario está dado de baja del seguimiento o el consentimiento está denegado se descarta en el acto: un optIn() o .granted posterior nunca la envía. identify se sigue guardando, para que las retiradas lleven el usuario correcto. Las decisiones tomadas antes de initialize() - optOut(), optIn(), updateConsent(_:), wipeData() - se aplican antes de cualquier cosa que el SDK haga al arrancar (Session Start, sincronización in-app, nuevo registro de push). Un optOut() / optIn() hecho antes de initialize() prevalece sobre optOut en la configuración. isUserOptedOut() y getTrackingConsent() responden con lo guardado incluso antes de initialize(). Cuando todavía no se ha decidido nada, getTrackingConsent() responde .granted - el consentimiento con el que arranca una app sin banner de consentimiento. Si initialize() rechaza la SDK key o el host, registerPushToken(_:onResult:) responde success: false de inmediato en lugar de esperar.

Arranque antes del primer desbloqueo. Solo espera un arranque anterior al primer desbloqueo del dispositivo desde que se encendió (un push en segundo plano o una activación por ubicación justo después de reiniciar): hasta entonces el almacenamiento del SDK no se puede leer. initialize() termina por sí solo en ese primer desbloqueo, las llamadas hechas mientras tanto siguen en cola, e isInitializationPending es true mientras espera - no es un fallo. Un dispositivo que simplemente está bloqueado, y que ya se desbloqueó una vez desde que se encendió, arranca de inmediato: un push silencioso en un teléfono bloqueado se procesa con normalidad.

2. Rastrear eventos​

// Evento básico
Joryio.shared.track("Button Tapped")

// Evento con propiedades
Joryio.shared.track("Product Viewed", properties: [
"product_id": "abc123",
"product_name": "Wireless Headphones",
"price": 99.99,
"category": "Electronics"
])

// Vista de pantalla
Joryio.shared.trackScreen("ProductDetail", properties: [
"product_id": "abc123"
])

3. 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

// Identifica un usuario
Joryio.shared.identify("user-123")
Joryio.shared.setAttributes([
"email": "[email protected]",
"name": "John Doe",
"plan": "premium"
])

// Establece atributos más adelante
Joryio.shared.setAttribute("last_purchase", value: Date())
Joryio.shared.incrementAttribute("lifetime_value", by: 99.99)

// Un segundo identificador para la misma persona - funciona también antes de identify()
Joryio.shared.addAlias("loyalty_id", name: "L-12345")

// Al cerrar sesión
Joryio.shared.reset()

identify une en el servidor el historial anónimo del dispositivo con el usuario, vuelve a registrar el token de push a su nombre y vuelve a sincronizar los mensajes in-app.

Identificar a otro usuario sin reset() de por medio se trata como un cierre de sesión y un inicio de sesión para lo que dejó el usuario anterior: sus escrituras de atributos sin enviar salen con sus propios ID, nunca con los del nuevo usuario, y con autenticación del SDK su token sigue ligado a sus elementos en cola. Mientras la persona haya hecho opt-out o el consentimiento esté denegado, identify solo guarda el ID de usuario en el dispositivo, para que una baja, el registro de opt-out o la baja de push lleven el usuario correcto - no se envía, rastrea ni escribe nada en ningún perfil. El identify normal se ejecuta cuando el seguimiento vuelve a estar permitido; reset() y wipeData() olvidan el ID guardado.

reset inicia de inmediato una nueva identidad anónima: un nuevo ID anónimo, sin ID de usuario, sin escrituras de atributos pendientes y sin estado in-app, y una nueva sesión anunciada con un evento Session Start. Un identify en la línea siguiente ya usa la nueva identidad. El token de push se vuelve a registrar con ella y los mensajes in-app se vuelven a sincronizar. Los eventos en cola antes del reset se siguen enviando, con los IDs con los que se registraron.

Una llamada a la que le falta un valor obligatorio - identify(""), track(""), una clave de atributo vacía - registra una advertencia y no hace nada. Un número que no es finito (NaN o infinito) en las propiedades de un evento o en los valores de atributos se descarta con una advertencia en el registro, y el resto de la llamada sigue adelante.

Las etiquetas de addAlias usan letras minúsculas, dígitos y guiones bajos. Un alias añadido a un visitante anónimo se mantiene en su perfil cuando se le identifica.

Dónde se almacenan los identificadores​

ValorSe almacena enTras eliminar la app
ID anónimo, ID de dispositivo, sesiónUserDefaultsSe pierde
Opt-out, una denegación de consentimiento guardada, decisiones tomadas antes de initialize, historial de mensajes in-app mostradosUserDefaultsSe pierde
ID de usuario, token de dispositivo de APNs, escrituras de atributos sin enviar, bajas de push pendientes (llevan el token), un ID de usuario guardado durante el opt-out, el token de autenticación del SDK de un usuario que cerró sesión (hasta que sus elementos en cola se envían o se descartan)Keychain (solo este dispositivo, tras el primer desbloqueo)Se pierde - el SDK detecta la reinstalación y borra sus elementos del Keychain
Eventos en cola; escrituras de identify, alias, suscripción y opt-out que esperan al servidor; escrituras de atributos que dejó un usuario anterior cuando la app identificó a otra persona sin reset()SQLite en Application Support, excluido de las copias de seguridadSe pierde

Toda clave que el SDK escribe en UserDefaults empieza por jry_ o joryio_.

Datos automáticos de sesión​

El SDK de iOS enriquece los eventos Session Start con datos de dispositivo y entorno:

  • $device_id
  • $platform (ios)
  • $model
  • $os_name / $os_version
  • $app_version / $build_number
  • $bundle_id
  • $screen_width / $screen_height
  • $locale
  • $language / $languages
  • $timezone
  • country (ISO-3166-1 alfa-2, derivado de la IP al iniciar la sesión)

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.

El SDK nunca muestra la solicitud por ti. Si has decidido que la quieres al arrancar, activa requestPushPermissionAtLaunch: está desactivada por defecto, no ausente.

Activa las notificaciones push para enviar mensajes segmentados mediante Apple Push Notification Service (APNS).

1. Configuración en AppDelegate​

import Joryio

class AppDelegate: UIResponder, UIApplicationDelegate {

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Inicializa el SDK
Joryio.shared.initialize(
sdkKey: "jry_sdk_ios_YOUR_KEY",
apiHost: "https://api-eu1.joryio.com/api"
)

// Solicita permisos de push
Task {
let granted = await Joryio.shared.requestPushPermission()
if granted {
print("Notificaciones push activadas")
}
}

return true
}

// Gestiona el registro del token de dispositivo
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
Joryio.shared.didRegisterForRemoteNotifications(deviceToken: deviceToken)
}

// Gestiona el fallo de registro
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
Joryio.shared.didFailToRegisterForRemoteNotifications(error: error)
}

// Gestiona la notificación push recibida
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
Joryio.shared.didReceiveRemoteNotification(userInfo, completionHandler: completionHandler)
}
}

El SDK se convierte en el delegate de UNUserNotificationCenter en initialize() y conserva el delegate que tu app había establecido antes. Solo actúa sobre las notificaciones de Joryio (clave joryio en el payload) - las muestra en primer plano y gestiona sus toques - y reenvía todas las demás notificaciones, y el callback de ajustes, a tu delegate sin cambios. Si estableces tu propio delegate después de initialize(), informa tú mismo de los toques en notificaciones de Joryio con trackPushClick(trackingId:).

didRegisterForRemoteNotifications(deviceToken:) es también el punto de entrada para un motor que recibe él mismo el token de APNs: el bridge de Unity le pasa el token de kUnityDidRegisterForRemoteNotificationsWithDeviceToken. Un token que llega antes de initialize() se conserva y se registra cuando el SDK arranca.

El token se registra en Joryio solo mientras el usuario tenga el opt-in activo y el consentimiento de seguimiento esté concedido. En caso contrario, se queda en el dispositivo y se registra cuando eso cambie. Los recibos de entrega y el seguimiento de clics siguen la misma regla.

2. Configurar APNS en el dashboard​

Para enviar notificaciones push, introduce tus credenciales de APNs en la app iOS de Joryio:

  1. Ve a Configuración → Apps y haz clic en el icono Notificaciones push (campana) de la fila de tu app iOS.
  2. Elige el Environment: Desarrollo (Sandbox) para compilaciones que ejecutas desde Xcode y Producción para compilaciones de App Store y TestFlight.
  3. Elige Basada en token (clave .p8) e introduce el Key ID, el Team ID y el contenido de tu clave de autenticación .p8.
  4. Haz clic en Save configuration.

Para ver instrucciones paso a paso sobre cómo crear la clave en tu cuenta de Apple Developer, consulta Obtener tus credenciales de APNs.

3. Funciones de push​

// Comprueba si push está activado - true exactamente cuando el estado es "granted"
let isEnabled = await Joryio.shared.isPushEnabled()
let status = await Joryio.shared.getPushPermissionStatus() // "granted", "denied" o "notDetermined"

// Obtén el token de dispositivo
if let token = Joryio.shared.getDeviceToken() {
print("Device token: \(token)")
}

// Gestión de insignias
Joryio.shared.updateBadgeCount(5)
Joryio.shared.clearBadge()

// Anula el registro de push - elimina el token en el servidor de Joryio y en iOS.
// Se mantiene entre arranques hasta registerPushToken(_:) o requestPushPermission().
Joryio.shared.unregisterPush()

// Tu propia pila de push: entrega el token e informa de lo que pasó
Joryio.shared.registerPushToken(hexToken) { result in
// result.success, result.message, result.isUnauthorized, result.isRetryable
}
Joryio.shared.reportPushDelivered(trackingId)
Joryio.shared.trackPushClick(trackingId: trackingId)

La autorización provisional y la efímera cuentan como "granted". El atributo de usuario push_permission usa los mismos valores, con not_determined para "notDetermined".

El token debe ser el token de dispositivo de APNs como cadena hex. Cualquier otra cosa - por ejemplo un token de registro de Firebase (FCM) - se rechaza: onResult informa success: false y el SDK registra el motivo. unregisterPush() se recuerda entre arranques: el token que iOS entrega en un arranque posterior se guarda en el dispositivo pero no se registra, hasta que la app vuelva a llamar a registerPushToken(_:onResult:) o a requestPushPermission(). registerPushToken(_:onResult:) llama a onResult una vez con el resultado; una respuesta 200 con success: false se informa como fallo, nunca como éxito. reportPushDelivered(_:) y trackPushClick(trackingId:) son para una app que gestiona las notificaciones por su cuenta, de modo que ninguna llega a los handlers del SDK.

Imágenes en rich push​

APNs no puede adjuntar una imagen por sí solo; tiene que hacerlo una Notification Service Extension en tu app. Añade una (File → New → Target → Notification Service Extension) y enlaza el producto de la extensión solo en ese target:

  • Swift Package Manager: JoryioNotificationServiceExtension
  • CocoaPods: pod 'Joryio/NotificationServiceExtension' en el bloque del target de la extensión

No enlaces Joryio en la extensión: llama a UIApplication.shared, que las extensiones de app no pueden usar, y añade SQLite a un proceso limitado a 24 MB. El producto de la extensión no tiene dependencias. Sustituye la clase generada por:

import JoryioNotificationServiceExtension // CocoaPods: import Joryio

class NotificationService: JoryioNotificationService {}

Descarga la URL de la clave jry_image_url (o attachment-url) del payload y la adjunta.

Mensajería in-app​

Muestra mensajes in-app segmentados a los usuarios según su comportamiento y atributos.

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.

Sincronización automática de campañas​

El SDK sincroniza automáticamente las campañas desde el servidor:

  • Al arrancar y cuando la app pasa a primer plano, como máximo una vez por syncInterval (300 segundos por defecto).
  • De inmediato cuando el usuario se identifica o se restablece, cuando se toca un push, cuando llega un push silencioso de sincronización y cuando cambia un atributo que vigila una campaña.

syncInAppCampaigns() sincroniza bajo demanda y no tiene límite de frecuencia.

Los mensajes activados por un evento, un toque en un push o un umbral de atributo pasan por los mismos límites de frecuencia y la misma regla de un mensaje a la vez que cualquier otro mensaje.

Control manual de campañas​

// Sincroniza manualmente las campañas desde el servidor
await Joryio.shared.syncInAppCampaigns()

// Activa manualmente la evaluación de campañas
await Joryio.shared.evaluateInAppCampaigns()

// Restablece las campañas mostradas (para pruebas)
Joryio.shared.resetDisplayedCampaigns()
Las llamadas de prueba requieren enableDebug

evaluateInAppCampaigns() y resetDisplayedCampaigns() cambian estado real - una puede forzar un mensaje en pantalla y la otra borra límites de frecuencia reales - por lo que se IGNORAN salvo que el SDK se haya inicializado con enableDebug: true; una compilación release registra una advertencia y no hace nada. Los diagnósticos de solo lectura (getQueueSize(), getSessionInfo(), getIdentity(), getDeviceToken()) no están restringidos. syncInAppCampaigns() es API normal y se ejecuta siempre.

Dos tipos de contenido​

Cada campaña llega en una de dos formas de contenido, y el SDK renderiza cada una de manera distinta:

ContenidoQué esCómo se renderiza
NativoDatos estructurados: titular, texto, imagen, botonesVistas UIKit reales, con el color de tinte, Dynamic Type, el modo oscuro y VoiceOver de tu app. Sin web view.
HTMLMarcado y CSS escritos por el autorUn WKWebView dentro del mensaje, bajo una Content-Security-Policy estricta.

Permitir mensajes HTML​

Los mensajes HTML están desactivados por defecto. Un mensaje HTML renderiza marcado escrito por el autor en una web view dentro de tu app, así que activarlo es una decisión del equipo de la app, no algo que se enciende desde una plataforma de marketing:

let config = JoryioConfig(
inApp: InAppConfig(allowHtmlJsInAppMessages: true) // por defecto: false
)

Joryio.shared.initialize(
sdkKey: "jry_sdk_ios_YOUR_KEY",
apiHost: "https://api-eu1.joryio.com/api",
config: config
)

Dejarlo desactivado no desactiva la mensajería in-app. Los mensajes nativos siguen mostrándose, porque son datos que dibujan las propias vistas del SDK, sin ningún intérprete. Las campañas HTML se omiten y se registran.

Esto controla el renderizador del propio SDK. Si tu app dibuja los mensajes in-app por su cuenta con setInAppMessageCallback(_:capabilities:), la web view del SDK nunca se ejecuta, así que el ajuste no se aplica a esa ruta: declara content.html en capabilities y las campañas HTML llegan a tu callback esté activado o no. Tu renderizador, tu decisión.

El documento del mensaje se muestra bajo una Content-Security-Policy que bloquea todo script, los frames, el acceso a la red y los envíos de formularios: las mismas reglas que el SDK web. Solo se ejecuta el joryioBridge del propio SDK, así que los enlaces, los botones, los atributos data-joryio-action, las llamadas al bridge y los formularios data-jry-form funcionan, pero los <script> propios del autor, los manejadores inline on* y los <iframe> incrustados no se ejecutan en iOS (ni en web).

Si tu política de seguridad prohíbe renderizar HTML de terceros en el proceso, déjalo desactivado y crea tus campañas como mensajes nativos.

Renderizar los mensajes tú mismo​

El SDK dibuja los mensajes nativos por ti, pero puedes tomar el control por completo: la misma vía de escape que otros proveedores llaman custom view factory. Implementa InAppMessagePresenter y asígnalo:

Joryio.shared.inAppPresenter = MyPresenter()

El descubrimiento automático solo se ejecuta cuando inAppPresenter es nil, así que el tuyo sustituye al renderizador integrado en lugar de competir con él. Asígnalo antes o después de initialize.

Tu presenter recibe la campaña con content ya resuelto - el Liquid se renderiza en el servidor y los campos nativos llegan como texto plano. Devuelve true desde present(campaign:in:delegate:) solo cuando el mensaje está en pantalla: entonces el SDK informa "displayed" por sí mismo y cuenta la visualización en el límite de frecuencia de la campaña. Informa de los toques y del cierre a través del delegate que recibes - messageViewClicked(_:action:url:) con el id del botón o enlace, y messageViewDidDismiss(_:) cuando el mensaje se cierra. No llames a trackInAppImpression desde un presenter: el mensaje se informaría dos veces.

Para renderizar con tu propia UI sin presenter, usa un callback:

Joryio.shared.setInAppMessageCallback({ campaign in
showMyMessage(campaign)
Joryio.shared.trackInAppImpression(campaignId: campaign.id, action: "displayed")
}, capabilities: ["content.native"])

// Cuando se pulsa un botón y cuando el mensaje se cierra:
Joryio.shared.trackInAppImpression(campaignId: campaign.id, action: "clicked", elementId: "buy_now")
Joryio.shared.trackInAppImpression(campaignId: campaign.id, action: "dismissed")

// Hand display back to the SDK's own renderer:
Joryio.shared.clearInAppMessageCallback()

action es "displayed", "clicked", "dismissed" o "converted"; cualquier otro valor se rechaza con un mensaje en el registro. elementId nombra opcionalmente el botón o enlace de un clic. Tu informe "displayed" es el único que se envía para ese mensaje, y es lo que cuenta el mensaje en el límite de frecuencia de la campaña - un mensaje entregado a tu callback que nunca se muestra no consume nada. "dismissed" libera al SDK para entregar el siguiente mensaje: un mensaje del que informaste "displayed" ocupa la pantalla hasta que informes "dismissed", para que no se dibuje nada encima. Sin ningún informe, el SDK se libera solo a los 30 segundos; un mensaje informado como "displayed" y nunca como "dismissed" ocupa la pantalla como máximo 5 minutos. Informa "dismissed" siempre que el mensaje salga de la pantalla, también después de un clic. Un mensaje que espera su retraso de visualización no se entrega si el usuario se identifica como otra persona, hace opt-out o cambia el consentimiento.

Desde la versión 1.3.2. Un callback configurado antes de initialize() se instala antes de la primera sincronización. Si lo configuras después de initialize(), la primera sincronización puede terminar antes y el SDK dibuja ese mensaje por su cuenta; pasa InAppConfig(holdInAppMessagesForHandler: true) para evitarlo. Entonces un mensaje que esté listo antes de configurar tu callback se retiene - no se dibuja, no se informa y no cuenta para ningún límite de frecuencia - y se entrega al callback en el momento en que lo configuras. Si en su lugar llamas a clearInAppMessageCallback(), la retención termina y el SDK dibuja el mensaje retenido.

El nombre de la clase cambia según el gestor de paquetes

No es algo que configures, pero conviene saberlo cuando algo no se muestra. El SDK encuentra su renderizador integrado por nombre en tiempo de ejecución, y el módulo en el que vive ese nombre depende de cómo lo hayas integrado:

cómo se empaquetaclase que busca el SDK
SwiftPMJoryioUI es su propio targetJoryioUI.DefaultInAppMessagePresenter
CocoaPodsJoryio/UI es un subspec, y los subspecs comparten el módulo del podJoryio.DefaultInAppMessagePresenter

Se prueban ambos nombres. Si nunca aparece ningún mensaje in-app, busca el aviso "No in-app presenter found": significa que el producto de UI no está enlazado (pod 'Joryio/UI', o el producto JoryioUI en SwiftPM), no que la campaña no haya llegado. Desde fuera, ambos casos se ven igual.

Tu propio presenter no necesita nada de esto: asígnalo y el descubrimiento no llega a ejecutarse.

Tipos de mensaje​

El SDK admite 5 tipos de mensaje:

  1. Modal - Centro de la pantalla con fondo atenuado
  2. Banner - Parte superior de la pantalla
  3. Slide-Up - Notificación pequeña desde abajo
  4. Full-Screen - Mensaje a pantalla completa
  5. Custom - Tu app decide la ubicación

Los banners y los slide-ups solo reciben toques dentro del mensaje, así que el resto de tu app sigue siendo utilizable detrás de ellos. Los mensajes modales y a pantalla completa bloquean la pantalla.

Límites de frecuencia​

Los mensajes respetan las reglas de contacto del espacio de trabajo y los límites de frecuencia de las campañas:

  • Máximo de impresiones por período.
  • Retraso mínimo entre mensajes.
  • Límites de frecuencia por campaña.

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.

  • El SDK presenta un mensaje solo mientras la app está activa y el view controller superior está estable - no a mitad de presentarse, cerrarse o moverse. Si no, espera y vuelve a intentarlo.
  • El mensaje cuenta como mostrado después de estar 1 segundo en pantalla mientras la app sigue activa. Si la app pasa a segundo plano antes, el segundo vuelve a empezar cuando regresa.
  • Si el mensaje sale de la pantalla antes, no se envía nada y el límite no se consume, así que sigue siendo elegible.
  • Un toque o un cierre antes cuenta como visto: primero se envía displayed y después el clic o el descarte. Un reset, un cambio de usuario o una baja (opt-out) antes no envía nada.
  • Si renderizas los mensajes tú mismo: envía displayed con la misma regla - cuando el mensaje lleve visible InAppDisplay.IN_APP_DISPLAY_DWELL_MS, o justo antes de un clic o un descarte que llegue antes.

Opciones de configuración​

Personaliza el comportamiento del SDK con JoryioConfig:

JoryioConfig(
// Identificación de usuario
userId: String?, // Inicializa con un ID de usuario conocido
anonymousId: String?, // ID anónimo personalizado

// Procesamiento por lotes y rendimiento
batchSize: Int, // Predeterminado: 50 (como máximo 500 por solicitud)
flushInterval: TimeInterval, // Predeterminado: 5,0 segundos (mínimo 1,0)
sendImmediately: Bool, // Predeterminado: false
maxQueueSize: Int, // Predeterminado: 1000

// Gestión de sesión
sessionTimeout: TimeInterval, // Predeterminado: 1800 (30 minutos)
trackSessionStart: Bool, // Predeterminado: true

// Push
requestPushPermissionAtLaunch: Bool, // Predeterminado: false - mejor un primer

// In-App Messaging
inApp: InAppConfig, // Predeterminado: InAppConfig() - ver abajo

// Red y reintentos
maxRetries: Int, // Predeterminado: 3
retryBackoffMs: Double, // Predeterminado: 1000.0
requestTimeout: TimeInterval, // Predeterminado: 10.0

// Autenticación del SDK (opcional)
enableSdkAuthentication: Bool, // Predeterminado: false
sdkAuthenticationToken: String?, // Predeterminado: nil

// Privacidad y RGPD
optOut: Bool, // Predeterminado: false (optOut() persiste igualmente)
trackingConsent: TrackingConsent?, // Predeterminado: nil (sin definir) - ver Consentimiento de seguimiento

// Depuración
enableDebug: Bool, // Predeterminado: false
logLevel: LogLevel? // Predeterminado: nil - .debug con enableDebug, si no .error
)
InAppConfig(
enabled: Bool, // Predeterminado: true - false desactiva in-app
allowHtmlJsInAppMessages: Bool, // Predeterminado: false
syncInterval: TimeInterval, // Predeterminado: 300 - segundos entre sincronizaciones automáticas
onMessageDisplay: ((InAppMessage) -> Void)?, // después de que el SDK muestre un mensaje
onMessageClick: ((InAppMessage, String) -> Void)?, // con el nombre del elemento pulsado
onMessageDismiss: ((InAppMessage) -> Void)?, // después de que se cierre un mensaje
holdInAppMessagesForHandler: Bool // Predeterminado: false - retiene los mensajes hasta setInAppMessageCallback (desde 1.3.2)
)

Los tres callbacks se ejecutan en el hilo principal.

Funciones avanzadas​

Gestión de atributos de usuario​

// Establece varios atributos
Joryio.shared.setAttributes([
"age": 28,
"city": "San Francisco",
"premium": true
])

// Establece un atributo
Joryio.shared.setAttribute("language", value: "en")

// Incrementa un atributo numérico
Joryio.shared.incrementAttribute("page_views", by: 1)
Joryio.shared.incrementAttribute("total_spent", by: 29.99)

// Elimina un atributo
Joryio.shared.unsetAttribute("temporary_flag")

Controles de privacidad​

// Stop collecting. PERSISTED - survives an app restart.
Joryio.shared.optOut()

// Opt back in
Joryio.shared.optIn()

// Check opt-out status
if Joryio.shared.isUserOptedOut() {
print("User has opted out")
}

// Delete everything the SDK stored on this device.
// SEPARATE from optOut(): "stop collecting" and "delete what you have" are
// different requests. This is the one an erasure request needs. It does NOT
// opt the user out - call optOut() as well if that is also intended.
Joryio.shared.wipeData()

// Get identity info
let (userId, anonymousId) = Joryio.shared.getIdentity()
print("User: \(userId ?? "anonymous"), Anonymous ID: \(anonymousId)")

Qué hace exactamente optOut():

detiene la recopilaciónsí - todas las rutas de escritura: track, identify, atributos (incluidos incrementos y unsets), alias, suscribirse, registro push y recibos de entrega, impresiones in-app. Las retiradas se siguen enviando: una baja, el registro de opt-out y la baja de push
sobrevive a un reiniciosí - el indicador se guarda en el dispositivo y se lee antes de recopilar nada. No depende de la SDK key, así que también sobrevive a un cambio de clave. optOut: true en la configuración se guarda de la misma manera
descarta lo que ya está en colasí - los eventos en cola, las escrituras de atributos no confirmadas y las escrituras de identify, alias y suscripción que esperan se descartan, no se entregan después. Las retiradas que esperan se conservan y se entregan
informa al servidorsí - el atributo de perfil tracking_opted_out (true; optIn() envía false). Se envía solo cuando cambia el opt-out - también con optOut: true en la configuración o una llamada antes de initialize() - y se reintenta hasta que el servidor lo guarda. Se envía sea cual sea el consentimiento - también pendiente o denegado
borra datos almacenadosno - usa wipeData()

El atributo tracking_opted_out es un registro, no un mecanismo de aplicación: se guarda en el perfil para que las campañas puedan excluir por él. La supresión en el servidor es un ajuste aparte.

Qué borra wipeData(): el ID de usuario y el ID anónimo, el ID de dispositivo, los eventos en cola, las escrituras de atributos no enviadas, el estado in-app, la sesión y el token de push - que además se da de baja en el servidor de Joryio, para que no se envíe nada más a esta instalación. El opt-out se conserva: borrar los datos de alguien no vuelve a activar su seguimiento. El siguiente evento (o la siguiente vuelta de la app al primer plano) inicia una sesión nueva, anunciada con un Session Start. Después, el push queda desactivado hasta que la app vuelva a llamar a registerPushToken o a pedir el permiso; un token que llegue en un arranque posterior no se registra automáticamente. El registro y la baja se ejecutan de uno en uno: registrar de nuevo un token cancela su baja si aún no llegó al servidor, y una baja sin confirmar de otro token se envía antes de cualquier registro nuevo.

Pasa al arrancar el consentimiento que guarda tu gestor de consentimiento, y actualízalo cuando el usuario decida:

Joryio.shared.initialize(
sdkKey: "jry_sdk_ios_YOUR_KEY",
apiHost: "https://api-eu1.joryio.com/api",
config: JoryioConfig(trackingConsent: .pending)
)

// Más tarde, desde tu banner de consentimiento:
Joryio.shared.updateConsent(.granted) // o .denied
ConsentimientoComportamiento
.grantedNormal
.pendingLos eventos y las escrituras de atributos se guardan en el dispositivo (hasta maxQueueSize) y no se envía nada: ni Session Start, ni sincronización in-app, ni registro de push. .granted envía lo guardado; .denied lo descarta
.deniedNo se recopila nada, y todo lo guardado se borra

Dos interruptores independientes deciden si se recopila algo: el consentimiento que informa tu banner y el opt-out de la propia persona. Solo se recopila cuando el consentimiento es .granted Y la persona no ha hecho opt-out: cualquier "no" gana.

  • .denied se guarda en el dispositivo y se mantiene entre arranques. Solo lo levanta un updateConsent(.granted) explícito, o trackingConsent: .granted pasado en la configuración.
  • trackingConsent está sin definir (nil) por defecto. Sin definir y sin nada guardado se comporta como .granted, así que una app sin banner de consentimiento funciona sin más; sin definir con un .denied guardado sigue denegado.
  • .pending no se guarda: se aplica solo a este arranque y nunca anula un .denied, ya sea guardado o asignado antes de initialize().
  • El consentimiento nunca levanta un opt-out. Solo optIn() lo hace, y optOut() nunca cambia el consentimiento guardado.
  • getTrackingConsent() devuelve el consentimiento vigente; isUserOptedOut() refleja solo el opt-out.
  • Una sesión que empezó mientras la recopilación no estaba permitida se anuncia con un evento Session Start en cuanto se permite - más tarde en el mismo arranque, o en un arranque posterior como una sesión nueva.

Vaciado manual de la cola​

// Vacía de inmediato (por ejemplo, antes de terminar la app)
Joryio.shared.flush()

// Comprueba el tamaño de la cola
let queueSize = Joryio.shared.getQueueSize()
print("Pending events: \(queueSize)")

Los eventos se envían en lotes de como máximo 500 eventos y unos 900 KB. Si el servidor rechaza un lote por no válido (HTTP 400, 413 o 422), el SDK lo divide y reenvía las dos mitades, para que un evento erróneo no pueda retener al resto; un evento rechazado por sí solo se descarta y se registra. Los eventos que el servidor indica como fallidos se conservan y se reintentan. Con HTTP 429, 5xx y fallos de red se conserva todo y se reintenta con espera exponencial, respetando Retry-After. HTTP 402 o 403 detiene los envíos durante el resto de la sesión y conserva los eventos. Los fallos nunca eliminan eventos; solo lo hace maxQueueSize, empezando por los más antiguos. El SDK descarta en track(), con una línea de registro, un evento que sabe que el servidor rechazaría - un nombre vacío o de más de 255 caracteres, o propiedades de más de 50 KB serializadas (medidas como las mide el servidor), más de 200 claves de primer nivel o anidadas a más de 5 niveles; los eventos que el servidor indica en rejectedIndices también se descartan, no se reintentan. flush() se salta la espera propia del SDK, pero nunca un Retry-After del servidor. Con autenticación del SDK un lote nunca mezcla dos usuarios, y los eventos en cola de un usuario que cerró sesión se descartan cuando el servidor los rechaza por cualquier motivo de autenticación del SDK, para que no bloqueen los eventos del siguiente usuario.

Escrituras que deben llegar al servidor​

identify(_:), addAlias(_:name:), las llamadas de suscripción, el registro tracking_opted_out y la parte del servidor de unregisterPush() / wipeData() se guardan en el dispositivo hasta que el servidor los confirma - tras un reinicio y tras reset() (cada uno lleva los ID con los que se hizo). Se envían en orden, de uno en uno, y un cambio más reciente de la misma persona para el mismo canal o grupo sustituye al que aún espera, para que un reintento antiguo nunca llegue después. Una escritura hecha siendo anónimo y enviada después de identify(_:) sale con el ID de usuario, siempre que el ID anónimo con el que se hizo siga siendo el de este dispositivo. Una escritura que el servidor rechaza definitivamente (HTTP 400, 403, 409, 413, 422) se descarta y se registra; un cambio de suscripción respondido con 404 (aún no hay perfil) se reintenta después de enviar los eventos en cola, como máximo 5 veces; success: false y un 401 de autenticación del SDK del usuario actual se reintentan con espera, como máximo 10 veces por escritura. Estar sin conexión, los tiempos de espera agotados, HTTP 402, 429 y 5xx nunca cuentan contra una escritura. Mientras la ingesta de datos esté en pausa para tu workspace (HTTP 402), las retiradas siguen saliendo por delante de las escrituras que deben esperar. Cuando hay 1.000 escrituras esperando, la más antigua que no es una retirada deja sitio. optOut(), el consentimiento .denied y wipeData() descartan las escrituras de identify, alias y suscripción que aún esperan; las retiradas - una baja, el registro tracking_opted_out, la baja de push - se conservan y se entregan.

Autenticación del SDK​

El token demuestra quién es un usuario identificado: las solicitudes que indican un ID de usuario deben llevar uno válido. Los visitantes anónimos no tienen token que mostrar, así que sus solicitudes se aceptan sin él, salvo que el workspace active también la identidad estricta (strict identity).

Cuando la autenticación del SDK está activada en tu espacio de trabajo, cada solicitud de ingesta debe llevar un JWT emitido por tu backend. Proporcionar un token - en sdkAuthenticationToken o con setSdkAuthenticationToken(_:), antes o después de initialize() - activa la cabecera, con o sin enableSdkAuthentication:

Joryio.shared.setSdkAuthenticationToken(tokenFromYourBackend)
Joryio.shared.setSdkAuthenticationErrorHandler { error in
fetchFreshToken { Joryio.shared.setSdkAuthenticationToken($0) }
}

reset() borra el token. Los eventos y escrituras que el usuario saliente ya había puesto en cola siguen enviándose con su propio token, así que un cierre de sesión normal no pierde nada - el token se guarda en el Keychain, ligado a esos elementos, hasta que se envían o se descartan, de modo que cerrar la app justo después de cerrar sesión tampoco pierde nada. Identificar a otro usuario sin reset() hace lo mismo con el token del usuario anterior, y también fijar el token del siguiente usuario con setSdkAuthenticationToken(_:): antes o después de identify, lo que dejó el usuario anterior conserva su propio token. Cada solicitud lleva el token cuyo sub es el usuario de esa solicitud, nunca uno que nombre a otra persona; un token fijado para el siguiente usuario antes de reset() - también cuando nadie ha iniciado sesión - se guarda para él y se vuelve a usar en su identify, y wipeData() lo borra - salvo que sea el token del usuario cuyos datos se borran (también uno identificado mientras el seguimiento está desactivado), o el token proporcionado para este arranque cuando se ejecuta un wipeData() hecho antes de initialize(). Los eventos y escrituras que dejó un usuario que ya no es el actual se descartan cuando la autenticación del SDK los rechaza, sea cual sea el motivo (missing, sub_mismatch, expired, invalid), para que no bloqueen los del siguiente usuario. El manejador de errores solo recibe las solicitudes del usuario actual, nunca las que dejó un usuario anterior. Aunque la persona haya hecho opt-out o el consentimiento esté denegado, fija el token del usuario al iniciar sesión: el usuario identificado mientras tanto cuenta como el actual para su baja, así que si se rechaza se pide su token al manejador y la baja se conserva.

Referencia de la API​

Todos los miembros públicos de Joryio.shared, con su nombre exacto en Swift.

MétodoQué hace
initialize(sdkKey:apiHost:config:)Arranca el SDK con tu SDK key, el host de la API y un JoryioConfig opcional. Una segunda llamada se ignora
track(_:properties:)Registra un evento con propiedades opcionales
trackScreen(_:properties:)Registra un evento Screen Viewed con screen_name
identify(_:)Une este dispositivo a un ID de usuario; vuelve a registrar el push y sincroniza los mensajes in-app
addAlias(_:name:)Añade un segundo identificador con etiqueta, también para visitantes anónimos
reset()Cierre de sesión: nueva identidad anónima y nueva sesión
setAttributes(_:)Asigna varios atributos; los valores nil / NSNull los eliminan
setAttribute(_:value:)Asigna un atributo; nil lo elimina
incrementAttribute(_:by:)Incremento numérico atómico (por defecto 1)
unsetAttribute(_:)Elimina un atributo
getUserId()El ID de usuario actual, o nil
getAnonymousId()El ID anónimo actual
getIdentity()(userId, anonymousId)
getSessionId()El ID de la sesión actual
getSessionInfo()(sessionId, sessionStart, duration)
optOut()Detiene toda recopilación y envío; se guarda
optIn()Reanuda después de optOut()
isUserOptedOut()Si el usuario ha hecho opt-out; antes de initialize(), según lo guardado
getTrackingConsent()El consentimiento de seguimiento vigente: .granted, .denied o .pending
updateConsent(_:)Asigna el consentimiento de seguimiento: .granted, .pending o .denied
wipeData()Borra todo lo que el SDK guardó en el dispositivo; conserva el opt-out
setSubscription(channel:status:)Estado de suscripción de marketing para un canal
addToSubscriptionGroup(_:channel:)Se une a un grupo de suscripción en un canal
removeFromSubscriptionGroup(_:channel:)Sale de un grupo de suscripción en un canal
setSdkAuthenticationToken(_:)Proporciona o sustituye el JWT de autenticación del SDK
setSdkAuthenticationErrorHandler(_:)Se llama cuando el servidor rechaza el JWT; responde con un token nuevo
clearSdkAuthenticationErrorHandler()Quita ese handler, por ejemplo al cerrar un bridge o una pantalla
flush()Envía ahora los eventos en cola
getQueueSize()Eventos que esperan en el dispositivo
lastTransportErrorEl fallo de petición más reciente (JoryioTransportError), o nil tras un éxito
currentApiEndpointLa URL base de la API a la que envía el SDK
isInitializationPendingtrue mientras initialize() espera el primer desbloqueo desde que se encendió el dispositivo; no es un fallo
ecommerce(config:)El asistente de e-commerce; pasa una configuración para fijar la moneda
inAppPresenterTu propio InAppMessagePresenter, en lugar del renderizador integrado
setInAppMessageCallback(_:capabilities:)Recibe los mensajes in-app y dibújalos tú mismo
clearInAppMessageCallback()Devuelve el dibujo al SDK
trackInAppImpression(campaignId:action:elementId:)Informa de lo que tu propia UI hizo con un mensaje
submitInAppForm(campaignId:values:)Envía un formulario mostrado en un mensaje (async); nunca lanza errores
syncInAppCampaigns()Obtiene ahora las campañas in-app (async)
evaluateInAppCampaigns()Solo para pruebas, requiere enableDebug: evalúa las campañas ahora
resetDisplayedCampaigns()Solo para pruebas, requiere enableDebug: olvida qué mensajes se mostraron
requestPushPermission()Pide permiso de notificaciones (async); anula unregisterPush()
getPushPermissionStatus()"granted", "denied" o "notDetermined" (async)
isPushEnabled()true exactamente cuando el estado es "granted" (async)
didRegisterForRemoteNotifications(deviceToken:)Reenvía el token de APNs desde tu AppDelegate
didFailToRegisterForRemoteNotifications(error:)Reenvía un fallo de registro en APNs
didReceiveRemoteNotification(_:completionHandler:)Reenvía una notificación remota
registerPushToken(_:onResult:)Registra tú mismo un token hex, con un callback de resultado; anula unregisterPush()
getDeviceToken()El token de APNs guardado, o nil
unregisterPush()Quita el token en el servidor y en iOS; se mantiene entre arranques
reportPushDelivered(_:)Informa de que llegó un push, por su trackingId
trackPushClick(trackingId:)Informa de un toque en un push, por su trackingId
updateBadgeCount(_:)Asigna el badge de la app
clearBadge()Borra el badge de la app
Joryio.pushPermissionAttributeEl nombre de atributo push_permission

Prácticas recomendadas​

1. Rastrear eventos significativos​

Céntrate en los eventos que importan para tu negocio:

// Bien: eventos específicos y accionables
Joryio.shared.track("Trial Started", properties: ["plan": "premium"])
Joryio.shared.track("Feature Used", properties: ["feature": "export"])

// Evita: eventos demasiado genéricos
Joryio.shared.track("Button Tapped") // Demasiado genérico

2. Gestionar el ciclo de vida del usuario​

// Al iniciar sesión
func handleLogin(userId: String, userInfo: UserInfo) {
Joryio.shared.identify(userId)
Joryio.shared.setAttributes([
"email": userInfo.email,
"name": userInfo.name
])
}

// Al cerrar sesión
func handleLogout() {
Joryio.shared.reset()
}

// Al registrarse - identify() une el historial anónimo al nuevo usuario
func handleSignup(userId: String, userInfo: UserInfo) {
Joryio.shared.identify(userId)
Joryio.shared.setAttributes(userInfo.attributes)
}

3. Vaciar ante eventos críticos​

override func applicationWillTerminate(_ application: UIApplication) {
Joryio.shared.flush()
}

Solución de problemas​

Los eventos no aparecen​

  1. Comprueba que la clave de SDK sea correcta: jry_sdk_ios_*.
  2. Activa el registro de depuración: enableDebug: true (registra todo salvo que también fijes logLevel).
  3. Comprueba los registros de consola en busca de errores.
  4. Verifica la conectividad de red.
  5. Llama a flush() para enviar de inmediato.

Push no funciona​

  1. Verifica que las credenciales de APNs estén guardadas en la app en el dashboard.
  2. Comprueba que el token de dispositivo esté registrado.
  3. Asegúrate de tener los entitlements correctos en Xcode.
  4. Prueba en el entorno adecuado (desarrollo o producción).
  5. Busca el error de Enviar Push de prueba en Obtener tus credenciales de APNs.

Errores de compilación​

  1. Asegúrate de que el destino de despliegue sea iOS 14.0+.
  2. Limpia la carpeta de compilación: Cmd+Mayús+K.
  3. Actualiza las dependencias de Swift Package.

Rastreo de e-commerce​

El SDK de iOS incluye un rastreador de e-commerce integrado para eventos de producto, carrito, checkout y pedido. Consulta la guía de rastreo de e-commerce común a todos los SDK para ver la API completa con ejemplos en Swift.

let ecommerce = Joryio.shared.ecommerce(config: EcommerceConfig(currency: "USD"))
ecommerce.purchase(order: EcommerceOrder(
orderId: "ORD-1001",
items: [EcommerceCartItem(productId: "SKU123", name: "Blue Shirt", price: 29.99, quantity: 2)],
value: 59.98
))

Una llamada a la que le falta lo que da sentido al evento - un productId, un orderId, un precio o valor finito, una lista items en una compra (puede estar vacía) - registra una advertencia y no envía nada, nunca un pedido vacío o a cero.

purchase envía Order Completed con las claves que leen los informes de ingresos: orderId y total (además de currency y los artículos). Si registras una compra tú mismo con track(), usa las mismas claves - order_id y value no se leen, y el pedido cuenta como cero ingresos.

Siguientes pasos​

Entrega de atributos​

setAttributes, setAttribute, incrementAttribute y unsetAttribute son duraderos. Una escritura queda en cola hasta que el servidor la confirma, así que un atributo asignado sin conexión se entrega cuando vuelve la conexión en lugar de perderse. Un incremento se envía como diferencia atómica, nunca como un total calculado en el dispositivo.

  • Reintentado en la siguiente escritura de atributo, cuando la app vuelve al primer plano y cuando se reanuda el envío (optIn(), consentimiento concedido).
  • Agrupado - una ráfaga de llamadas se convierte en una sola petición (ventana de 800ms). Una escritura suelta sale enseguida. Una escritura hecha mientras una petición está en curso la espera y va después; nunca la cancela, así que un incremento nunca se cuenta dos veces.
  • Solo lo confirma el servidor - una respuesta 200 con success: false conserva las escrituras para el siguiente intento. Una escritura que el servidor rechaza como inválida (HTTP 400, 413 o 422) se divide y se reenvían las mitades, para que una clave errónea no retenga las demás; una escritura suelta que sigue rechazada se descarta y se registra. Como máximo 200 unsets por petición, y el resto va después.
  • nil elimina - setAttribute("nickname", value: nil), o NSNull desde un bridge, elimina el atributo como unsetAttribute.
  • Persistido en el Keychain de iOS, así que una escritura sobrevive a que se cierre el proceso. Se borra en cuanto el servidor confirma, y se purga con optOut(), reset() y wipeData(). Una escritura que el servidor rechaza con HTTP 403 se descarta y se registra, para que no pueda retener las siguientes.

Los atributos también se usan para la segmentación in-app. El perfil del servidor es la referencia: solo las escrituras que el servidor aún no ha confirmado pueden imponerse a él, y eso es lo que evita que dos dispositivos del mismo contacto discrepen sobre quién es ese contacto.

Registro de push y consentimiento​

El SDK se registra para notificaciones remotas tanto si el usuario concede el permiso de notificaciones como si no. En iOS son dos cosas distintas: el registro da un token de dispositivo sin consentimiento, y ese token solo puede entregar push en segundo plano (silenciosos); no puede mostrar nada que el usuario no haya autorizado.

Esto permite que un mensaje in-app llegue a un usuario que rechazó las notificaciones, y hace que ya exista un token si más tarde las activa en Ajustes. Indícalo en tu política de privacidad.

Mensajes in-app​

Muestra mensajes in-app dirigidos a los usuarios según su comportamiento y sus atributos.

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.shared.setSubscription(channel: "email", status: "unsubscribed")
Joryio.shared.addToSubscriptionGroup("list_abc123", channel: "email")

Un cambio de suscripción hecho mientras el consentimiento de seguimiento está pendiente espera en el dispositivo y se envía cuando se concede el consentimiento. Uno que falla se conserva, tras un reinicio y tras reset(), y se vuelve a enviar - ver Escrituras que deben llegar al servidor.

Una baja - status: "unsubscribed", o removeFromSubscriptionGroup - es una retirada: se acepta y se entrega aunque la persona haya hecho opt-out, el consentimiento esté pendiente o denegado, o después de wipeData(). Suscribirse y unirse a un grupo no.

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.