Guía del SDK de iOS

El SDK para dispositivos móviles de Contact Center AI Platform (CCAI Platform) para el sistema operativo iOS de Apple permite incorporar la experiencia para dispositivos móviles de CCAI Platform en aplicaciones para dispositivos móviles de iOS.

Requisitos

El SDK para iOS tiene los siguientes requisitos:

  • iOS 12.0 y versiones posteriores

Recupera las credenciales de la empresa

  1. Accede al portal de Contact Center AI Platform (CCAI Platform) con credenciales de administrador.

  2. Ve a Configuración > Configuración para desarrolladores.

  3. En Clave de la empresa y Código secreto, anota la clave y el código secreto de la empresa.

Cómo comenzar

A continuación, se proporciona una guía para comenzar a usar el SDK para iOS de la plataforma de CCAI.

Instalación

Para comenzar, debes instalar el SDK de iOS.

Descarga la app de ejemplo

  1. Descarga la app de ejemplo para iOS.

  2. Navega a la carpeta y, luego, instala las dependencias con CocoaPods:

    $ pod install --project-directory=ExampleApp
    
  3. Para configurar rápidamente los parámetros del proyecto, ejecuta una secuencia de comandos de shell:

    $ ./setup.sh
    

    Como alternativa, puedes editar manualmente la configuración del proyecto siguiendo estos pasos:

    1. Abre ExampleApp.xcworkspace.

    2. Reemplaza los valores UJETCompanyKey y UJETCompanySecret en Info.plist por los valores de Company Key y Company Secret Code de la página Settings > Developer Settings en el portal de CCAI Platform.

    3. Reemplaza el valor de UJETSubdomain en Info.plist por el subdominio de la URL de tu portal de CCAI Platform. El subdominio precede directamente a .ujet.com en la URL, por ejemplo, your-subdomain en https://your-subdomain.ujet.com/settings/developer-setting.

Integración en tu proyecto

La integración del SDK para iOS con tu aplicación depende de tu entorno de desarrollo.

Administrador de paquetes de Swift

  1. Agrega el paquete de Swift para el SDK de iOS.

  2. En la configuración de compilación, coloca -ObjC en Other Linker Flags.

  3. En la versión más reciente de Xcode (actualmente, 13.2), hay un problema conocido con el consumo de frameworks binarios distribuidos con Swift Package Manager. La solución alternativa actual para este problema es agregar una fase de Run Script a las fases de compilación de tu proyecto de Xcode. Esta fase de Run Script debe aparecer después de la fase de compilación de Embed Frameworks. Esta nueva fase Run Script debería contener el siguiente código:

    find "${CODESIGNING_FOLDER_PATH}" -name '*.framework' -print0 | while read -d $'0' framework
    do
    codesign --force --deep --sign "${EXPANDED_CODE_SIGN_IDENTITY}" --preserve-metadata=identifier,entitlements --timestamp=none "${framework}"
    done
    
CocoaPods
  1. Agrega la siguiente línea al Podfile:

    pod 'UJET', :podspec =>
    'https://sdk.ujet.co/ios/x.y.z/ujet.podspec' #specific version
    x.y.z
    
  2. Ejecuta pod install. Si ya se integró el SDK de iOS, ejecuta pod update CCAI Platform.

Carthage

Google Cloud recomienda usar un administrador de dependencias o una integración manual, ya que una dependencia de la CCAI Platform no admite Carthage. Para ello, agrega las siguientes líneas:

binary "https://sdk.ujet.co/ios/UJETKit.json

binary "https://sdk.ujet.co/ios/UJETFoundationKit.json

binary https://raw.githubusercontent.com/twilio/twilio-voice-ios/Releases/twilio-voice-ios.json

Integración manual

No se admite: https://github.com/twilio/conversations-ios/issues/12.

binario https://raw.githubusercontent.com/twilio/conversations-ios/master/twilio-convo-ios.json

  1. Ejecuta carthage bootstrap --use-xcframeworks (o carthage update --use-xcframeworks si actualizas las dependencias).

  2. Descarga UJETKit.xcframework, UJETFoundationKit.xcframework, UJETChatRedKit.xcframework, UJETChatBlueKit.xcframework, UJETTwilioCallKit.xcframework y todas las dependencias TwilioVoice.xcframework y TwilioConversationsClient.xcframework.

  3. Agrega UJETKit.xcframework a tu destino arrastrándolo a la sección Frameworks, Libraries, and Embedded Content.

  4. Repite los pasos 2 y 3 en todas las dependencias del paso 1.

  5. En Build Settings, coloca -ObjC en Other Linker Flags.

  6. Agrega libc++.tbd como una dependencia en la sección Linked Frameworks del destino.

Si vas a compilar el SDK de forma manual con el proyecto de ejemplo, sigue los pasos que se indican en la siguiente sección.

Compila el SDK de forma manual con el proyecto de ejemplo

Sigue estos pasos en orden:

  1. Descarga todos los frameworks, incluidos UJETKit.xcframework y otras dependencias.

  2. Crea la carpeta CCAI Platform en la raíz del proyecto y extrae todos los frameworks.

  3. Selecciona el destino Objc-Manual o Swift-Manual y compila.

Importar framework

En las siguientes secciones, se proporcionan instrucciones para importar el framework.

Proyecto de Objective-C

@import UJETKit;

Proyecto de Swift

swiftimport
UJETimport UJETKit

Inicializa el SDK

Inicializa CCAI Platform con UJET_COMPANY_KEY y UJET_SUBDOMAIN.

In application:didFinishLaunchingWithOptions: method:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    // Initialize CCAI Platform
    [UJET.initialize:UJET_COMPANY_KEY subdomain:UJET_SUBDOMAIN delegate:self];

    // YOUR CODE

    return YES;
}

Puedes cambiar el nivel de registro de verbose a error. El nivel de registro predeterminado es UjetLogLevelInfo.

[UJET.setLogLevel:UjetLogLevelVerbose];

Autenticación del usuario final

Accede al SDK de iOS a través de la app para iOS.

Para asegurarnos de que el usuario final esté autenticado, presentamos el mecanismo de firma de JWT.

El SDK para iOS solicitará que se firme la carga útil cuando se necesite autenticación. Si la firma se realiza correctamente, la aplicación intercambia el JWT firmado por el token de autenticación del usuario final. Se debe llamar al bloque de éxito o falla antes de que devuelva el delegado.

Para el usuario anónimo (identificador = nil), la aplicación creará un UUID para el usuario. Si, en un momento posterior, el usuario se autentica con un identificador, la aplicación intentará combinar los dos usuarios según el UUID.

En UJETObject.h del proyecto de ejemplo, haz lo siguiente:

@import UJETKit;

@interface UJETObject : NSObject <UJETDelegate>

Implementa signPayload: payloadType: success: failure: método delegado.

- (void)signPayload:(NSDictionary *)payload payloadType:(UjetPayloadType)payloadType success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure {
  if (payloadType == UjetPayloadAuthToken) {
    [self signAuthTokenInLocal:payload success:success failure:failure];
  }
}

- (void)signAuthTokenInLocal:(NSDictionary *)payload success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure {
    NSMutableDictionary *payloadData = [payload mutableCopy];

    NSDictionary *userData = [[NSUserDefaults standardUserDefaults] objectForKey:@"user-data"];
    [payloadData addEntriesFromDictionary:userData];
    payloadData[@"iat"] = [NSNumber numberWithDouble:[[NSDate date] timeIntervalSince1970]]; // required
    payloadData[@"exp"] = [NSNumber numberWithDouble:([[NSDate date] timeIntervalSince1970] + 600)]; // required

    NSString *signedToken = [self encodeJWT:payloadData];

    if (signedToken.length > 0) {
        success(signedToken);

    } else {
        NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Failed to sign token" };
        NSError *error = [NSError errorWithDomain:@"ExampleApp" code:0 userInfo:userInfo];
        failure(error);
    }
}

- (NSString *)encodeJWT:(NSDictionary *)payload {
    id<JWTAlgorithm> algorithm = [JWTAlgorithmHSBase algorithm384];
    NSString *secret = NSBundle.mainBundle.infoDictionary[@"UJETCompanySecret"];
    return [JWTBuilder encodePayload:payload].secret().algorithm(algorithm).encode;
}

Te recomendamos que firmes la carga útil desde el servidor de aplicaciones, no desde el cliente.

En este ejemplo, se usa la firma local con fines de prueba. Consulta signDataInRemote: success: failure: en el archivo UJETObject.m.

Para obtener más información, consulta Autenticación de usuarios finales del SDK.

Cómo configurar las notificaciones push

La aplicación envía notificaciones push para solicitar Smart Actions, como la verificación y la foto, además de informar sobre una llamada entrante. La aplicación requiere que se guarden dos tipos diferentes de certificados (VoIP y APNs) en el portal del administrador.

Prepara el certificado de servicios de VoIP

La documentación de referencia está disponible para la notificación push de VoIP de Apple.

  1. Crea y descarga el certificado de VoIP desde el sitio para desarrolladores de Apple.

  2. Haz doble clic en el certificado para agregarlo al Llavero.

  3. Inicia la aplicación Acceso a Llavero en tu Mac.

  4. Elige la categoría Mis certificados en la barra lateral izquierda.

  5. Haz clic con el botón derecho en el certificado de VoIP Services: your.app.id.

  6. En el menú emergente, elige Exportar.

  7. Guárdalo como cert.p12 sin protegerlo con una contraseña. Para ello, deja el campo de contraseña en blanco.

  8. Ejecuta el siguiente comando en la terminal.

    openssl s_client -connect gateway.push.apple.com:2195 -cert cert.pem -debug -showcert
    
  9. La parte superior de cert.pem es el certificado y la parte inferior es la clave privada.

  10. Verifica que tu certificado funcione con el servidor de notificaciones push de Apple.

    openssl s_client -connect gateway.push.apple.com:2195 -cert cert.pem -debug -showcerts
    

    Si se ejecuta correctamente, debería mostrar lo siguiente:

    ---
    New, TLSv1/SSLv3, Cipher is AES256-SHA
    Server public key is 2048 bit
    Secure Renegotiation IS supported
    Compression: NONE
    Expansion: NONE
    SSL-Session:
        Protocol  : TLSv1
        Cipher    : AES256-SHA
        Session-ID:
        Session-ID-ctx:
        Master-Key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
        Key-Arg   : None
        Start Time: 1475785489
        Timeout   : 300 (sec)
        Verify return code: 0 (ok)
    ---
    
  11. Accede al portal de la Plataforma de CCAI con credenciales de administrador y ve a Configuración > Configuración para desarrolladores > App para dispositivos móviles.

  12. Completa el certificado en la sección "Certificado de servicios de VoIP" y guárdalo. Asegúrate de incluir límites (-----BEGIN----- y -----END-----) para el certificado y la clave privada.

  13. Marca la casilla de verificación Sandbox si ejecutas una app con un perfil de aprovisionamiento de desarrollo, como la depuración en Xcode. Si tu app se archivó para Ad hoc o App Store y usa un perfil de aprovisionamiento de distribución, desmarca la casilla de verificación Sandbox.

Prepara el certificado SSL del servicio de notificaciones push de Apple

El proceso para esto es similar al de los certificados de servicio de VoIP. En este caso, se usa el certificado SSL del servicio de notificaciones push de Apple (zona de pruebas y producción). Puedes consultar la documentación del servidor de notificaciones remotas de Apple para obtener orientación sobre cómo crear el certificado.

Integración de notificaciones push

En AppDelegate.m:

@import PushKit;

@interface AppDelegate() <PKPushRegistryDelegate>
In application:didFinishLaunchingWithOptions: method:
// Initialize CCAI Platform
[UJET] initialize:UJET_COMPANY_KEY subdomain:UJET_SUBDOMAIN delegate:self];

//  Register for VoIP notifications on launch.
PKPushRegistry *voipRegistry = [[PKPushRegistry alloc] initWithQueue: dispatch_get_main_queue()];
voipRegistry.delegate = self;
voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];

Agrega los siguientes métodos de delegado en el archivo de implementación del protocolo UIApplicationDelegate:

Imprime tu token del dispositivo para probar las notificaciones push.

// PKPushRegistryDelegate

- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type {
  [UJET updatePushToken:credentials.token type:UjetPushTypeVoIP];
}

- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion {
  if (payload.dictionaryPayload[@"ujet"]) {
    [UjetreceivedNotification:payload.dictionaryPayload completion:completion];
  } else {
    completion();
  }
}

// UIApplicationDelegate

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
  [UjetupdatePushToken:deviceToken type:UjetPushTypeAPN];
}

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
  if (userInfo[@"ujet"]) {
    [UJET receivedNotification:userInfo completion:nil];
  }
}

// UserNotificationsDelegate overrides [UIApplicationDelegate didReceiveRemoteNotification:]

- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
    NSDictionary *userInfo = notification.request.content.userInfo;

    if (userInfo[@"ujet"] != nil) {
        [UJET receivedNotification:userInfo completion:nil];
    }
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
    NSDictionary *userInfo = response.notification.request.content.userInfo;

    if (userInfo[@"ujet"] != nil) {
        [UJET receivedNotification:userInfo completion:nil];
    }
}

Habilita las notificaciones push

  1. Selecciona tu destino y abre la pestaña Capabilities.

  2. Activa el interruptor de Notificaciones push.

Prueba las notificaciones push

En las siguientes secciones, se proporciona orientación para probar las notificaciones push.

Sección de depuración de notificaciones push

En el portal del administrador, navega a Configuración > Configuración para desarrolladores. En esta página, busca la sección titulada Push Notification Debug:

Copia y pega el token del dispositivo en el área de texto correcta y selecciona la app para dispositivos móviles correcta.

Obtén el token del dispositivo

Un ejemplo de cadena de token de dispositivo se ve de la siguiente manera:

7db0bc0044c8a203ed87cdab86a597a2c43bf16d82dae70e8d560e88253364b7

Por lo general, las notificaciones push se configuran en la clase que se ajusta al protocolo UIApplicationDelegate o PKPushRegistryDelegate. En algún momento, el token del dispositivo estará disponible para ti. Puedes imprimirlo antes de pasarlo al SDK para iOS. Para obtener el token del dispositivo, usa el fragmento de código.

Swift
func tokenFromData(data: Data) -> String {
  return data.map { String(format: "%02x", $0) }.joined()
}

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
  print("apns token: ", tokenFromData(data: deviceToken))
  ...
}

func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
  print("voip token: ", tokenFromData(data: credentials.token))
  ...
}
Obj-C
- (NSString *)tokenFromData:(NSData *)data {
  const char *d = data.bytes;
  NSMutableString *token = [NSMutableString string];

  for (NSUInteger i = 0; i < data.length; i++) {
    [token appendFormat:@"%02.2hhX", d[i]];
  }

  return [[token copy] lowercaseString];
}

- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(PKPushType)type {
  NSLog(@"voip token: %@", [self tokenFromData:credentials.token]);
  ...
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
  NSLog(@"apns token: %@", [self tokenFromData:deviceToken]);
}
Resultado

Una vez que hayas ingresado el archivo PEM del certificado y el token del dispositivo, haz clic en el botón.

El resultado mostrará el mensaje Push notification successfully configured si la notificación push de prueba se entregó correctamente.

No se garantiza la entrega de notificaciones push al 100%, ya que depende de la conexión de red del dispositivo.

Configuraciones del proyecto

En las siguientes secciones, se describen los cambios necesarios para configurar el proyecto.

Funciones

En la configuración de destino, activa las siguientes funciones:

  • Notificaciones de aplicación

  • Modos en segundo plano (verifica estos elementos)

  • Audio y AirPlay

  • Voz por IP

Info.plist

Para proteger la privacidad del usuario, cualquier app para iOS vinculada en iOS 10.0 o versiones posteriores que acceda a cualquiera de los micrófonos, la biblioteca de fotos y la cámara del dispositivo debe declarar su intención de hacerlo. Incluye las siguientes claves con un valor de cadena en el archivo Info.plist de tu app y proporciona una cadena de propósito para esta clave. Si tu app intenta acceder a cualquiera de los micrófonos, la biblioteca de fotos y la cámara del dispositivo sin una cadena de propósito correspondiente, se cerrará.

  • NSMicrophoneUsageDescription: Permite el acceso al micrófono para llamar y hablar con los equipos de asistencia o solución de problemas, y para enviar videos con sonido relacionados con consultas sobre productos.

  • NSCameraUsageDescription: Permite que el cliente acceda a la cámara para tomar y enviar fotos relacionadas con su consulta de asistencia al cliente.

  • NSPhotoLibraryUsageDescription: Permite que el cliente envíe fotos relacionadas con su consulta de asistencia al cliente.

  • NSFaceIDUsageDescription: Permite el acceso a la verificación con Face ID.

Inicia el SDK de iOS

Agrega la siguiente línea donde quieras iniciar el SDK de iOS:

[UJET startWithOptions:nil];

También puedes iniciar el SDK de iOS desde un punto específico del menú con esta clave a través de un punto de acceso directo:

UJETStartOptions *option = [[UJETStartOptions alloc] initWithMenuKey:@"MENU_KEY"];
[UJET startWithOptions:option];

El menuKey se puede crear con un punto de acceso directo (PAD). En los siguientes pasos, se indica cómo crear una DAP:

  1. Accede al portal de CCAI Platform con credenciales de administrador.

  2. Ve a Configuración > Cola.

  3. Selecciona cualquier fila de la estructura del menú.

  4. Selecciona Create direct access point.

  5. Ingresa la clave en el formulario de texto.

  6. Haz clic en Guardar.

Borra la caché local si se actualizaron los datos del usuario

Almacenamos en caché el token de autorización en el llavero para reutilizarlo y realizar solicitudes menos frecuentes para firmar la carga útil desde la app del host. El SDK lo usará hasta que venza o se revoque a través de la llamada clearUserData. La app host es la encargada de revocar esta caché cada vez que se actualizan o cambian los datos relacionados con el usuario, como un evento de cierre de sesión.

[UJET clearUserData];

Verifica si hay una sesión existente antes de iniciar Contact Center AI Platform

Antes de iniciar una sesión, verifica si no hay una sesión actual. Esto es especialmente importante cuando cambió el userId.

[UJET getStatus];

Si hay una sesión existente, debemos pedirle al usuario que la reanude o cancele la acción:

if ([UJET getStatus] != UjetStatusNone) {
  // Display alert to cancel login or resume existing session
}

Personalizar

En UJETGlobalTheme.h, se enumeran varias opciones para el tema del SDK.

Establece tu tema después de [UJET initialize], por ejemplo:

UJETGlobalTheme *theme = [UJETGlobalTheme new];

theme.font = [UIFont fontWithName:@"OpenSans" size: 16.0f];
theme.lightFont = [UIFont fontWithName:@"OpenSans-Light" size: 16.0f];
theme.boldFont = [UIFont fontWithName:@"OpenSans-Bold" size: 16.0f];
theme.tintColor = [UIColor colorWithRed:0.243 green:0.663 blue:0.965 alpha:1.00];

[Ujet setGlobalTheme:theme];

El nombre de la empresa se recupera de Admin Portal > Settings > Support Center Details > Display Name.

Puedes establecer la imagen del logotipo en lugar del nombre de la empresa de la siguiente manera:

theme.companyImage = [UIImage imageNamed:@"logo"];

Si la imagen es demasiado grande, se cambiará su tamaño para que quepa en el área.

Strings

También puedes personalizar cadenas anulando el valor. Por ejemplo, coloca este par clave-valor en tu archivo Localizable.strings:

"ujet_greeting_title" = "Title";

"ujet_greeting_description" = "Description";

Las cadenas personalizables disponibles se enumeran en el archivo ujet.strings.

Modo oscuro

Puedes especificar un tono del color que desees para el modo oscuro y mejorar la legibilidad de las fuentes.

@property (nonatomic, strong) UIColor \*tintColorForDarkMode;

Si no configuras la propiedad, se usará UJETGlobalTheme.tintColor para el modo oscuro. Te recomendamos que establezcas esta propiedad si tu app admite el modo oscuro. Consulta los siguientes artículos de Apple para elegir el color de tinte adecuado para el modo oscuro:

Tema del chat

Para personalizar la pantalla de chat, puedes usar una cadena JSON o cada clase de tema.

Para obtener más información, consulta la app de ejemplo y quita la marca de comentario del método customizeChatTheme.

func customizeChatTheme() throws {
  guard let file = Bundle.main.path(forResource: "chat-theme-custom", ofType: "json") else { return }
  let json = try String.init(contentsOfFile: file, encoding: .utf8)

  let chatTheme = UJETChatTheme.init(jsonString: json)

  let quickReplyTheme = UJETChatQuickReplyButtonTheme()
  quickReplyTheme.style = .individual
  quickReplyTheme.alignment = .right
  quickReplyTheme.backgroundColor = UJETColorRef(assetName: "white_color")
  quickReplyTheme.backgroundColorForHighlightedState = UJETColorRef(assetName: "quick_reply_color")
  quickReplyTheme.textColor = UJETColorRef(assetName: "quick_reply_color")
  quickReplyTheme.textColorForHighlightedState = UJETColorRef(assetName: "white_color")

  let fontTheme = UJETFontTheme()
  fontTheme.family = "Arial Rounded MT Bold"
  fontTheme.size = 14
  quickReplyTheme.font = fontTheme

  chatTheme?.quickReplyButtonTheme = quickReplyTheme

  let globalTheme = UJETGlobalTheme()
  globalTheme.chatTheme = chatTheme
  globalTheme.defaultAgentImage = UIImage(named: "agent_avatar_image")
  globalTheme.font = UIFont(name: "Arial Rounded MT Bold", size: 14)

  UJET.setGlobalTheme(globalTheme)
}

Se muestra un ejemplo de un tema de chat personalizado.

Tema de las tarjetas de contenido

Puedes agregar personalización para las tarjetas de contenido junto con la personalización del chat. Puedes hacerlo con el archivo JSON (consulta la propiedad content_card) o con la clase UJETChatContentCardTheme.

func customizeChatTheme() throws {
  guard let file = Bundle.main.path(forResource: "chat-theme-custom", ofType: "json") else { return }
  let json = try String.init(contentsOfFile: file, encoding: .utf8)

  let chatTheme = UJETChatTheme.init(jsonString: json)

  let contentCardTheme = UJETChatContentCardTheme()
  contentCardTheme.backgroundColor = UJETColorRef(assetName: "agent_message_background_color")
  contentCardTheme.cornerRadius = 16

  let contentCardFontTheme = UJETFontTheme()
  contentCardFontTheme.family = "Arial Rounded MT Bold"
  contentCardFontTheme.size = 18
  contentCardTheme.font = contentCardFontTheme

  let contentCardBorder = UJETBorderTheme()
  contentCardBorder.width =  1
  contentCardBorder.color = UJETColorRef(assetName: "agent_message_border_color")
  contentCardTheme.border = contentCardBorder

  let contentCardFontTheme = UJETFontTheme()
  contentCardFontTheme.family = "Arial Rounded MT Bold"
  contentCardFontTheme.size = 18
  contentCardTheme.font = contentCardFontTheme

  // The font family is inherited from the contentCardFontTheme
  let subtitle = UJETFontTheme()
  subtitle.size = 12
  contentCardTheme.subtitle = subtitle

  // The font family is inherited from the contentCardFontTheme
  let bodyFont = UJETFontTheme()
  bodyFont.size = 10
  contentCardTheme.body = bodyFont

  theme.chatTheme?.contentCard = contentCardTheme

  let globalTheme = UJETGlobalTheme()
  globalTheme.chatTheme = chatTheme
  globalTheme.defaultAgentImage = UIImage(named: "agent_avatar_image")
  globalTheme.font = UIFont(name: "Arial Rounded MT Bold", size: 14)

  UJET.setGlobalTheme(globalTheme)
}

Se muestra un ejemplo de una tarjeta de contenido.

Tema de la tarjeta de formulario

Puedes agregar personalización para las tarjetas de formularios junto con la personalización del chat. Para ello, usa el archivo JSON (consulta form_card property) o la clase UJETChatFormCardTheme.

func customizeChatTheme() throws {
    guard let file = Bundle.main.path(forResource: "chat-theme-custom", ofType: "json") else { return }
    let json = try String.init(contentsOfFile: file, encoding: .utf8)

    let chatTheme = UJETChatTheme.init(jsonString: json)

    let formCardTheme = UJETChatFormCardTheme()
    formCardTheme.backgroundColor = UJETColorRef(assetName: "agent_message_background_color")
    formCardTheme.cornerRadius = 16

    let formCardFontTheme = UJETFontTheme()
    formCardFontTheme.family = "Arial Rounded MT Bold"
    formCardFontTheme.size = 18
    formCardTheme.font = formCardFontTheme

    let formCardBorder = UJETBorderTheme()
    formCardBorder.width =  1
    formCardBorder.color = UJETColorRef(assetName: "agent_message_border_color")
    formCardTheme.border = formCardBorder

    let titleFontTheme = UJETFontTheme()
    titleFontTheme.family = "Arial Rounded MT Bold"
    titleFontTheme.size = 18
    formCardTheme.title = titleFontTheme

    // The font family is inherited from the formCardFontTheme
    let subtitleFontTheme = UJETFontTheme()
    subtitleFontTheme.size = 12
    formCardTheme.subtitle = subtitleFontTheme

    chatTheme?.formCard = formCardTheme

    let globalTheme = UJETGlobalTheme()
    globalTheme.chatTheme = chatTheme
    globalTheme.defaultAgentImage = UIImage(named: "agent_avatar_image")
    globalTheme.font = UIFont(name: "Arial Rounded MT Bold", size: 14)

    UJET.setGlobalTheme(globalTheme)
}

Configuración del formulario web

Para configurar la capacidad del formulario web, implementa el método ujetWebFormDidReceive del protocolo UJETDelegate. Este método recibe un evento (un diccionario FormMessageReceivedEvent) como parámetro, que contiene información relacionada con el formulario. El diccionario de eventos (FormMessageReceivedEvent) incluye la siguiente estructura JSON:

  {
    "type": "form_message_received",
    "smart_action_id": 1,
    "external_form_id": "external_foobar"
    "signature": "4868a7e1dcb5..."
  }

Para controlar el evento, haz lo siguiente:

  1. Extrae la información pertinente del diccionario de eventos (smart_action_id, external_form_id y signature).

  2. Genera un URI del formulario y una firma para los datos del formulario.

  3. Pasa los datos del formulario al SDK como un diccionario FormDataEvent con completion closure.

  4. Si se produce algún error durante la generación de la URI o la firma, invoca la devolución de llamada con callback.onError() y el Error.

El diccionario (FormDataEvent) que se pasa al SDK debe tener la siguiente estructura:

 {
    "type": "form_data",
    "signature": "4868a7e1dcb5...",
    "data": {
       "smart_action_id":1,
       "external_form_id": "form_id",
       "uri":"foobar"
    }
  }

La firma (HMAC-SHA256) se debe generar con data y firmar con la clave secreta compartida. Las claves de objeto de los datos deben ordenarse alfabéticamente antes de generar firmas, y el mismo data debe enviarse al SDK.

Transferencia posterior a la sesión

Puedes agregar personalización para la VA posterior a la sesión junto con la personalización del chat. Esto se puede lograr con el archivo JSON (consulta la propiedad post_session) o con la clase UJETChatPostSessionVaTheme. El ancho del borde solo puede ser 0 o 1, y, si no deseas diferenciar la experiencia del VA posterior a la sesión, puedes establecer containerColor en blanco y el borde en 0.

func customizeChatTheme() throws {
    guard let file = Bundle.main.path(forResource: "chat-theme-custom", ofType: "json") else { return }
    let json = try String.init(contentsOfFile: file, encoding: .utf8)

    let chatTheme = UJETChatTheme.init(jsonString: json)

    let postSessionVaTheme = UJETChatPostSessionVaTheme()
    postSessionVaTheme.containerColor = UJETColorRef(assetName: "white_color")

    let postSessionVaBorder = UJETBorderTheme()
    postSessionVaBorder.width =  0
    postSessionVaBorder.color = UJETColorRef(assetName: "white_color")
    containerColor.border = postSessionVaBorder

    chatTheme?.postSessionVaTheme = postSessionVaTheme

    let globalTheme = UJETGlobalTheme()
    globalTheme.chatTheme = chatTheme

    UJET.setGlobalTheme(globalTheme)
}

Menú de acciones de chat

Puedes agregar personalización para el menú de acciones del chat junto con la personalización del chat. Esto se puede lograr con el archivo JSON (consulta la propiedad form_card) o con la clase UJETChatActionMenuTheme.

func customizeChatTheme() throws {
    guard let file = Bundle.main.path(forResource: "chat-theme-custom", ofType: "json") else { return }
    let json = try String.init(contentsOfFile: file, encoding: .utf8)

    let chatTheme = UJETChatTheme.init(jsonString: json)

    let actionMenuTheme = UJETChatActionMenuTheme()

    let photoLibraryIcon = UJETChatUserInputIconTheme()
    photoLibraryIcon.visible = true
    photoLibraryIcon.image = UJETImageRef(assetName: "library_button_asset")

    let cameraIcon = UJETChatUserInputIconTheme()
    cameraIcon.visible = true
    cameraIcon.image = UJETImageRef(assetName: "camera_button_asset")

    let cobrowseIcon = UJETChatUserInputIconTheme()
    cobrowseIcon.visible = true
    cobrowseIcon.image = UJETImageRef(assetName: "cobrowse_button_asset")

    actionMenuTheme.libraryIcon = photoLibraryIcon
    actionMenuTheme.cameraIcon = cameraIcon
    actionMenuTheme.cobrowseIcon = cobrowseIcon

    chatTheme?.actionMenu = actionMenuTheme

    let globalTheme = UJETGlobalTheme()
    globalTheme.chatTheme = chatTheme

    UJET.setGlobalTheme(globalTheme)
}

Otras apariciones

Puedes personalizar otros aspectos, como el tamaño de la fuente y el color de fondo.

theme.supportTitleLabelFontSize = 30;
theme.supportDescriptionLabelFontSize = 20;
theme.supportPickerViewFontSize = 30;
theme.staticFontSizeInSupportPickerView = YES;

theme.backgroundColor = UIColor.darkGrayColor;
theme.backgroundColorForDarkMode = UIColor.lightGrayColor;

CallKit

En iOS 10.0 y versiones posteriores, CallKit está habilitado para todas las llamadas.

Con CallKit, se muestra una llamada entrante en la app con la pantalla de llamada y se muestra la llamada en el historial de llamadas del teléfono.

Para iniciar una nueva sesión de asistencia de CCAI Platform desde el historial de llamadas, agrega el siguiente bloque a tu AppDelegate.m:

AppDelegate.m:
- (BOOL)application:(UIApplication *)app continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray * _Nullable))restorationHandler {
    if ([userActivity.activityType isEqualToString:@"INStartAudioCallIntent"]) {
        // Open app from Call history
        [UJET startWithOptions:nil];
    }

    return YES;
}

CallKit permite que se muestre un ícono de 40 x 40 en la pantalla de bloqueo cuando se recibe una llamada mientras el dispositivo está bloqueado. Coloca una imagen en tu Xcassets llamada "icon-call-kit".

Configura el SDK

Puedes establecer varias opciones antes de iniciar el SDK.

Consulta la clase UJETGlobalOptions para obtener más detalles.

UJETGlobalOptions *options = [UJETGlobalOptions new];
options.fallbackPhoneNumber = @"+18001112222";
options.preferredLanguage = @"en";

[UJET setGlobalOptions:options];

Cómo mostrar u ocultar el botón para descargar la transcripción

Puedes configurar el SDK para mostrar u ocultar el botón de descarga de la transcripción en el menú de opciones del chat y en la pantalla posterior al chat.

El siguiente código muestra cómo configurar el botón de descarga de la transcripción:

typedef NS_OPTIONS(NSUInteger, UJETChatDownloadTranscriptVisibilityOptions) {
    UJETChatDownloadTranscriptVisibilityOptionsShowAll = 0,
    UJETChatDownloadTranscriptVisibilityOptionsHideFromOptionsMenu = 1 << 0,
    UJETChatDownloadTranscriptVisibilityOptionsHideFromPostChatScreen = 1 << 1,
    UJETChatDownloadTranscriptVisibilityOptionsHideAll = UJETChatDownloadTranscriptVisibilityOptionsHideFromOptionsMenu | UJETChatDownloadTranscriptVisibilityOptionsHideFromPostChatScreen
};

@property (nonatomic, assign) UJETChatDownloadTranscriptVisibilityOptions transcriptVisibilityOptions;

Respaldo de PSTN

Proporcionamos una alternativa de PSTN para varias situaciones:

  • La red móvil está sin conexión.

  • No se puede acceder al backend de la aplicación.

  • VoIP no está disponible

    • La condición de la red no es lo suficientemente buena para conectarse. Consulta la propiedad UJETGlobalOptions.pstnFallbackSensitivity para obtener más detalles.

    • Se produjo un error durante la conexión debido a la configuración del firewall o a un problema del proveedor.

Te recomendamos que configures el número de IVR de tu empresa en UJETGlobalOptions.fallbackPhoneNumber. El formato recomendado es el signo + seguido del código de país y el número de teléfono, p. ej., +18001112222.

Sensibilidad de resguardo de la RTC

Puedes ajustar el nivel de sensibilidad de la verificación de la condición de la red para la conmutación por recuperación a la RTCP.

@property (nonatomic, assign) float pstnFallbackSensitivity;

El valor debe estar en el rango de 0.0 a 1.0. Si se establece en 1, la llamada siempre se conectará a través de la RTPC en lugar de VoIP. La latencia máxima y el umbral de ancho de banda mínimo son de 10,000 ms y 10 KB/s, respectivamente, para el valor de 0. Por ejemplo, un valor de 0.5 significa que la latencia y el ancho de banda mínimos son de 5,000 ms y 15 KB/s, respectivamente.

Para configurar este valor, sigue estos pasos:

  1. Accede al portal de CCAI Platform como administrador.

  2. Ve a Configuración > Configuración para desarrolladores > Apps para dispositivos móviles.

  3. Busca la sección Umbral de número de teléfono de resguardo. El valor predeterminado es 0.85.

  4. Especifica el nuevo valor del umbral.

  5. Haz clic en Guardar.

Cómo desactivar las notificaciones push a nivel global

Puedes desactivar las notificaciones push a nivel global. Si se configura la siguiente propiedad en false, se omiten todas las dependencias de las notificaciones push y se evita que estas lleguen a los usuarios finales:

@property (nonatomic, assign) BOOL allowsPushNotifications;

Ignorar el modo oscuro

Puedes ignorar el modo oscuro en el SDK de la plataforma de CCAI específicamente con esta propiedad:

@property (nonatomic, assign) BOOL ignoreDarkMode;

Ocultar la barra de estado

Puedes controlar la visibilidad de la barra de estado con esta propiedad:

  @property (nonatomic, assign) BOOL hideStatusBar;

De forma predeterminada, hideStatusBar se establece en false y visible.

Omitir la encuesta de CSAT

Puedes agregar un botón que le permita al usuario omitir la encuesta de CSAT. En el siguiente ejemplo de código, se muestra cómo agregar el botón:

let options = UJETGlobalOptions()
options.skipCsat = true

Cómo personalizar el indicador de actividad

Puedes agregar tu propia animación de cargador (dentro de un UIView) al SDK y anular el UIActivityIndicatorView predeterminado. Implementa el método ujet_activityIndicator desde UJETDelegate y devuelve la vista personalizada.

public func ujet_activityIndicator() -> UIView! {
    let loader = UIView.init()
    let animation = CABasicAnimation()
    loader.backgroundColor = .blue
    loader.layer.cornerRadius = 15
    animation.timingFunction = CAMediaTimingFunction.init(name: CAMediaTimingFunctionName.easeOut)
    animation.keyPath = "transform.scale"
    animation.duration = 1.0
    animation.fromValue = 0.0
    animation.toValue = 1.0
    animation.repeatCount = Float.infinity
    animation.isRemovedOnCompletion = false
    loader.layer.add(animation, forKey: "Load")

    return loader
}

Si ya configuraste UIUserInterfaceStyle como Light en el archivo Info.plist de tu app para inhabilitar por completo el modo oscuro, puedes ignorar esta propiedad.

Idioma preferido

El SDK de CCAI Platform usará el siguiente orden de prioridad para determinar el idioma preferido.

  1. Idioma seleccionado en la pantalla de presentación de la app.

  2. Se seleccionó el idioma predeterminado de UJETGlobalOptions. Puedes establecer el idioma predeterminado con la propiedad preferredLanguage. Los códigos de idioma admitidos se pueden encontrar en el archivo UJETGlobalOptions.h.

  3. Se usará el idioma del dispositivo seleccionado en el dispositivo (desde Configuración > General > Idioma y región) cuando la app lo admita.

  4. Se usará el dialecto más cercano al idioma del dispositivo cuando la aplicación no admita el idioma del dispositivo, pero sí admita su dialecto principal más cercano. Por ejemplo, si el usuario seleccionó español (Cuba) como idioma en el dispositivo y la app no admite español (Cuba), pero sí admite el dialecto principal español, se usará el idioma español.

  5. Se usará el inglés si la app no admite el idioma del dispositivo.

Configura los íconos de vínculos de desviación externos

Puedes personalizar el ícono en el canal de vínculos de redireccionamiento externo. Para ello, sube el ícono al catálogo de recursos de tu app y asegúrate de usar el mismo nombre de ícono cuando crees el vínculo de redireccionamiento externo en Configuración > Chat > Vínculos de redireccionamiento externo > Ver vínculos > Agregar vínculo de redireccionamiento en el Portal del administrador. Si el nombre del ícono en el Portal de administración no coincide con el ícono subido a la app, el SDK usará el ícono predeterminado. Puedes consultar este vínculo sobre cómo agregar imágenes al catálogo de recursos.

Resguardo

Puedes usar la función didHandleUjetError para la resiliencia ante errores inesperados. Si no usas esta función o si muestra false, el SDK para iOS controla el error.

En la siguiente tabla, se muestran los errores que detecta la función didHandleUjetError:

Tipo de error Código de error Descripción
networkError 1 La red no está disponible.

Nota: Este error no se activa cuando la red no está disponible durante una sesión de chat o llamada, o en una pantalla de calificación.

authenticationError 100 Se produjo un error inesperado durante la autenticación.
authenticationJwtError 101 Se produjo un error inesperado durante la validación del JWT, por ejemplo, un error de análisis.
voipConnectionError 1000 No se pudo establecer una conexión con el proveedor de VoIP. Una devolución de llamada del SDK de VoIP controla esto.
voipLibraryNotFound 1001 El sistema espera que la llamada se conecte a través de un proveedor de VoIP, pero no puede encontrar uno. Esto puede ocurrir si integras el SDK incorrecto o no agregas una biblioteca de proveedores de VoIP a tus dependencias.
chatLibraryNotFound 1100 Se produce cuando el sistema no puede encontrar la biblioteca de chat. Esto puede ocurrir cuando integras el SDK incorrecto o no agregaste una biblioteca de chat de Twilio a tus dependencias.

En la siguiente muestra de código, se muestra cómo usar la función didHandleUjetError:

public func didHandleUjetError(_ errorCode: Int32) -> Bool {
    guard let ujetError = UjetErrorCode(rawValue: Int(errorCode)) else {
        return false // Let the SDK handle unknown integer codes.
    }

    switch ujetError {
    case .networkError:
        // Example for if you have a custom UI for network errors. You can
        // handle the error and prevent the SDK from showing its own alert.
        showCustomNetworkAlert() // Your custom UI for this type of error.
        return true

    case .authenticationError, .voipConnectionError:
        // For all other errors, use the default SDK behavior.
        return false

    @unknown default:
        // Let the SDK handle future errors.
        return false
    }
}

Envía datos personalizados a tu CRM

Puedes enviar datos personalizados al ticket de CRM.

Existen dos métodos para enviar datos personalizados:

  1. Método seguro: Firma de datos predefinida con JWT.

  2. Método no seguro: Datos predefinidos con JSON sin formato (no recomendado).

Cómo usar el método seguro para enviar datos personalizados

Debes implementar el método de firma. Primero, puedes colocar tus datos personalizados en el cliente y enviarlos a tu servidor para firmarlos. En tu servidor, puedes agregar datos adicionales con un formulario definido y firmar con tu company.secret, y devolverlos con JWT.

- (void)signPayload:(NSDictionary *)payload payloadType:(UjetPayloadType)payloadType success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure
{
    if (payloadType == UjetPayloadCustomData) {
      // sign custom data using UJET_COMPANY_SECRET on your server.

      NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
      NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];

      NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] init];
      mutableRequest.URL = [NSURL URLWithString:@"https://your.company.com/api/ujet/sign/custom_data"];
      mutableRequest.HTTPMethod = @"POST";
      NSError *error;

      // Make client's custom data
      UJETCustomData *customData = [[UJETCustomData alloc] init];
      [customData set:@"name" label:@"Name" stringValue:@"USER_NAME"];
      [customData set:@"os_version" label:@"OS Version" stringValue:[[UIDevice currentDevice] systemVersion]];
      [customData set:@"model" label:@"Model number" numberValue:[NSNumber numberWithInteger:1234]];
      [customData set:@"temperature" label:@"Temperature" numberValue:[NSNumber numberWithFloat:70.5]];
      [customData set:@"purchase_date" label:@"Purchase Date" dateValue:[NSDate date]];
      [customData set:@"dashboard_url" label:@"Dashboard" urlValue:[NSURL URLWithString:@"http://internal.dashboard.com/1234"]];

      NSDictionary *data = @{@"custom_data": [customData getData]};
      mutableRequest.HTTPBody = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error];
      NSURLSessionDataTask *task = [session dataTaskWithRequest:mutableRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
          if(error) {
              failure(error);
          }
          else {
              NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
              success(json[@"jwt"]);
          }
      }];

      [task resume];
    }
}

Usa un método no seguro para enviar datos personalizados

No se recomienda este método, ya que crea una posible vulnerabilidad que podría exponer tu aplicación a un ataque de intermediario. Si eliges usar este método, no nos hacemos responsables de la exposición a la seguridad ni de los posibles daños que puedan ocurrir. Te recomendamos que uses el método seguro que se describió anteriormente para enviar datos personalizados en tu aplicación. También puedes iniciar el SDK para iOS con la instancia UJETCustomData. En este caso, el delegado signPayload para UJETPayloadCustomData solo debe llamar a success(nil);.

- (void)signPayload:(NSDictionary *)payload payloadType:(UjetPayloadType)payloadType success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure {
    if (payloadType == UjetPayloadCustomData) {
      success(nil);
    }
}
UJETStartOptions *options = [UJETStartOptions new];
options.unsignedCustomData = customData;

[UJET startWithOptions:options];

Cómo usar datos personalizados sin firmar para enviar transcripciones de chat externas

Puedes enviar la transcripción del chat al SDK cuando se inicia con datos personalizados sin firmar llamando al método setExternalChatTransfer: o setExternalChatTransferWithDictionary: para establecer datos JSON con NSString o NSDictionary, respectivamente.

UJETCustomData *customData = [UJETCustomData new];
[customData setExternalChatTransfer:jsonString];

UJETStartOptions *options = [UJETStartOptions new];
options.unsignedCustomData = customData;

[UJET startWithOptions:options];

Formato JSON:

  • greeting_override: string

  • agent: dictionary

    • name: string

    • avatar: cadena [URL del avatar del agente, opcional]

  • transcripción: array

    • sender: string ["end_user" or "agent"]

    • timestamp: string [es decir, "2021-03-15 12:00:00Z"]

    • content: array

      • type: string [uno de text, media]

      • text: string [obligatorio para el tipo de texto]

      • media: diccionario [obligatorio para el tipo de medio]

        • type: string [one of image, video]

        • url: string [URL pública que apunta al archivo multimedia]

Ejemplo de JSON:

{
  "greeting_override": "Please hold while we connect you with a human agent.",
  "agent": {
    "name": "Name",
    "avatar": "avatar url"
  },
  "transcript": [
    {
      "sender": "agent",
      "timestamp": "2021-03-15 12:00:15Z",
      "content": [
        {
          "type": "text",
          "text": "**Suggestions shown:**\n\n* Help with batch or delivery\n* Help with metrics or order feedback\n* Help with Instant Cashout"
        }
      ]
    },
    {
      "sender": "end_user",
      "timestamp": "2021-03-15 12:00:16Z",
      "content": [
        {
          "type": "text",
          "text": "Help with batch or delivery"
        }
      ]
    }
  ]
}

Puedes usar Markdown en el tipo de texto. La sintaxis admitida incluye cursiva, negrita, listas con viñetas, hipervínculos y subrayado (--text--).

Ejemplo de datos personalizados { :#example-of-custom-data }

JSON codificado en JWT

El archivo JSON debe validar el JWT. El objeto de datos personalizados es el valor de la clave custom_data.

{
  "iat" : 1537399656,
  "exp" : 1537400256,
  "custom_data" : {
    "location" : {
      "label" : "Location",
      "value" : "1000 Stockton St, San Francisco, CA, United States",
      "type" : "string"
    },
    "dashboard_url" : {
      "label" : "Dashboard URL",
      "value" : "http://(company_name)/dashboard/device_user_ID",
      "type" : "url"
    },
    "contact_date" : {
      "label" : "Contact Date",
      "value" : 1537399655992,
      "type" : "date"
    },
    "membership_number" : {
      "label" : "Membership Number",
      "value" : 62303,
      "type" : "number"
    },
    "model" : {
      "label" : "Model",
      "value" : "iPhone",
      "type" : "string"
    },
    "os_version" : {
      "label" : "OS Version",
      "value" : "12.0",
      "type" : "string"
    },
    "last_transaction_id" : {
      "label" : "Last Transaction ID",
      "value" : "243324DE-01A1-4F71-BABC-3572B77AC487",
      "type" : "string"
    },
    "battery" : {
      "label" : "Battery",
      "value" : "-100%",
      "type" : "string"
    },
    "bluetooth" : {
      "label" : "Bluetooth",
      "value" : "Bluetooth not supported",
      "type" : "string"
    },
    "wifi" : {
      "label" : "Wi-Fi",
      "value" : "Wi-Fi not connected",
      "type" : "string"
    },
    "ssn" : {
      "invisible_to_agent" : true,
      "label" : "Social Security Number",
      "value" : "102-186-1837",
      "type" : "string"
    }
  }
}

Cada dato es similar al formato de objeto JSON y debe contener la clave, el valor, el tipo y la etiqueta.

La clave es el identificador único de los datos, la etiqueta es el nombre visible en la página del CRM y el tipo es el tipo de valor.

  • cadena

    • String de JSON
  • número

    • número entero, número de punto flotante
  • fecha

    • Es el formato de marca de tiempo de Unix en UTC con 13 dígitos. (contiene milisegundos)
  • URL

    • Formato de URL HTTP
Ejemplo de CRM

Ubicación

Usar el framework de CoreLocation Para obtener más información, consulta AppDelegate.m.

Versión del SO del dispositivo
[customData set:@"os_version" label:@"OS Version" stringValue:[[UIDevice currentDevice] systemVersion]];

Cómo evitar que se muestren datos personalizados

Puedes usar la propiedad invisible_to_agent con un objeto de datos personalizado para evitar que se muestren datos personalizados firmados o sin firmar en el adaptador del agente. En el ejemplo anterior, el número de seguridad social del usuario final no se muestra en el adaptador del agente porque "invisible_to_agent" : true se incluye en el objeto ssn.

Cuando incluyes la propiedad "invisible_to_agent" : true con un objeto de datos personalizado, puedes esperar el siguiente comportamiento:

Para obtener más información, consulta Cómo ver los datos de la sesión en el adaptador del agente.

Propiedades de datos reservadas

Puedes enviar propiedades de datos reservadas a Contact Center AI Platform (CCAI Platform) como datos personalizados firmados cuando comienza una sesión. Para obtener más información, consulta Envía propiedades de datos reservadas.

A continuación, se muestra un ejemplo de las propiedades de datos reservadas en los datos personalizados:

  {
    "custom_data": {
      "reserved_verified_customer": {
        "label": "Verified Customer",
        "value": "VERIFIED_CUSTOMER_BOOLEAN": ,
        "type": "boolean"
      },
      "reserved_bad_actor": {
        "label": "Bad Actor",
        "value": "VERIFIED_BAD_ACTOR_BOOLEAN": ,
        "type": "boolean"
      },
      "reserved_repeat_customer": {
        "label": "Repeat Customer",
        "value": "REPEAT_CUSTOMER_BOOLEAN": ,
        "type": "boolean"
      }
    }
  }
  

Reemplaza lo siguiente:

  • VERIFIED_CUSTOMER_BOOLEAN: Es verdadero si consideras que este usuario final es un cliente legítimo.
  • VERIFIED_BAD_ACTOR_BOOLEAN: Es verdadero si consideras que este usuario final podría ser una persona que actúa de mala fe.
  • REPEAT_CUSTOMER_BOOLEAN: Es verdadero si determinaste que este usuario final se comunicó con tu centro de contacto anteriormente.

Personalizar flujo

Desconecta CCAI Platform para controlar los eventos de la app de Host

// CCAI Platform is connected
...
// An event has come
[UJET disconnect:^{
  // Handle an event
}];

Posponer la llamada o el chat entrante de CCAI Platform

Implementa un método de delegado para controlar los eventos entrantes
- (BOOL)shouldConnectUjetIncoming:(NSString *)identifier forType:(UjetIncomingType)type {
  if (weDoingSomething) {
    // save identifier and type
    return NO; // postpone
  } else {
    return YES;
  }
}
Conectar evento pospuesto
[UJET connect:identifier forType:UjetIncomingTypeCall];

Configura el vínculo directo

Esto permite que los agentes en llamadas RTC usen acciones inteligentes por SMS, tanto cuando un usuario final tiene la app como cuando no la tiene.

Ve a Configuración > Administración de operaciones > Habilita la opción Enviar SMS para descargar la app en el portal de la plataforma de CCAI.

Puedes configurar la URL de la app con tu página web (es decir, https://your-company.com/support) después de configurar el vínculo universal o el esquema de URL personalizado. Puedes seleccionar cualquiera de las dos opciones.

Implementa el método de delegado para controlar el vínculo directo

El vínculo universal y la URL personalizada se ven como https://your-company.com/support?call_id=x&nonce=y y your-company://support?call_id=x&nonce=y, respectivamente. Coloca uno de tus vínculos sin parámetros de búsqueda en la URL de la app en el portal del administrador. Por ejemplo, ingresa your-company://support si usas un esquema de URL personalizado.

En el método de delegado, asegúrate de llamar a [UJET start] solo cuando las rutas y los parámetros de URL en el vínculo universal o la URL personalizada sean específicos para la Plataforma de CCAI.

- (BOOL)application:(UIApplication *)app continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray * _Nullable))restorationHandler {
  ...
  if ([NSUserActivityTypeBrowsingWeb isEqualToString:userActivity.activityType]) {
    NSURL *url = userActivity.webpageURL;

    NSArray *availableSchema = @[
                                  @"your-company",   // custom URL scheme
                                  @"https"           // universal link
                                  ];

    NSArray *availableHostAndPath = @[
                                      @"ujet",                  // custom URL scheme
                                      @"your-comany.com/ujet"   // universal link
                                      ];

    if (![availableSchema containsObject:url.scheme]) {
      return NO;
    }

    NSString *hostAndPath = [NSString stringWithFormat:@"%@%@", url.host, url.path];
    if (![availableHostAndPath containsObject:hostAndPath]) {
      return NO;
    }

    // your-company://ujet?call_id={call_id}&nonce={nonce}
    // https://your-company.com/ujet?call_id={call_id}&nonce={nonce}
    NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url
                                                resolvingAgainstBaseURL:NO];
    NSArray *queryItems = urlComponents.queryItems;
    NSString *callId = [self valueForKey:@"call_id" fromQueryItems:queryItems];
    // validate call id
    if (![self isValidCallId:callId]) {
      return NO;
    }
    NSString *nonce = [self valueForKey:@"nonce" fromQueryItems:queryItems];

    UJETStartOptions *options = [[UJETStartOptions alloc] initWithCallId:callId nonce:nonce];

    [UJET startWithOptions:options];
  }
  ...
}

Si tu app adopta UIWindowSceneDelegate, agrega este fragmento de código:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        //if app is called with universal Link and started from cold
        if connectionOptions.urlContexts.first != nil  {
            self.scene(scene, openURLContexts: connectionOptions.urlContexts)
        }

        guard let _ = (scene as? UIWindowScene) else { return }
    }

    func scene(_ scene: UIScene, willContinueUserActivityWithType userActivityType: String) {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
        let _ = appDelegate.application(UIApplication.shared,
                                        continue: NSUserActivity(activityType: userActivityType)) { _ in

        }
    }

    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        guard let url = URLContexts.first?.url else {
            return
        }

        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
        let _ = appDelegate.application(UIApplication.shared,
                                        open: url,
                                        options: [:])
    }
}

Para obtener más información, consulta los códigos de ejemplo del archivo UJETObject+DeepLink.

Observa el evento de CCAI Platform

Publicamos los siguientes eventos a través de NSNotificationCenter.defaultCenter. Puedes escucharlos y personalizar tu flujo según tu caso de uso, p.ej., diseño de teclado personalizado.

  • UJETEventEmailDidClick

    • Datos del menú de la fila
  • UJETEventEmailDidSubmit

    • Datos del menú de la fila

    • has_attachment: (NSNumber) @YES, @NO

  • UJETEventSessionViewDidAppear

    • type: @"call", @"chat"

    • timestamp: (NSString) ISO 8601

  • UJETEventSessionViewDidDisappear

    • type: @"call", @"chat"

    • timestamp: (NSString) ISO 8601

  • UJETEventSessionDidCreate

    • Datos de la sesión
  • UJETEventSessionDidEnd

    • Datos de la sesión

    • agent_name: (NSString) null si el agente no se unió

    • duration: (NSNumber) solo para llamadas

    • ended_by: (NSString)

      • type=call: @"agent", @"end_user"

      • type=chat: @"agent", @"end_user", @"timeout", @"dismissed"

  • UJETEventSdkDidTerminate

  • UJETEventPostSessionOptInDidSelected

    • opt_in_selected: (NSString) @"Yes", @"No"

Datos de eventos

Metadatos
  • application: @"iOS"

  • app_id: (NSString) identificador del paquete

  • app_version: (NSString)

  • company: (NSString) subdomain

  • device_model: (NSString)

  • device_version: (NSString)

  • sdk_version: (NSString)

  • timestamp: (NSString) ISO 8601

Datos del menú de la fila
  • Metadatos

  • menu_id: NSString

  • menu_key: NSString, nullable

  • menu_name: NSString

  • menu_path : NSString

Datos de la sesión
  • Datos del menú de la fila

  • session_id: NSString

  • type: @"call", @"chat"

  • end_user_identifier: NSString

Cómo configurar Compartir pantalla

Si quieres usar la función de Compartir pantalla, integra UJETCobrowseKit.xcframework.

CocoaPods: Agrega el siguiente subspec al destino de tu app.

    ruby
target 'MyApp' do
  pod 'UJET'
  pod 'UJET/Cobrowse'
end

Carthage: Agrega la siguiente línea en el archivo Cartfile:

binary "https://sdk.ujet.co/ios/UJETKit.json"

SwiftPM: Selecciona los productos UJET y UJETCobrowse, y agrégalos al destino de tu app.

Y configura la propiedad UJETGlobalOptions.cobrowseKey.

swift
let options = UJETGlobal
Options()options.cobrowseKey = cobrowseKey

UJET.setGlobalOptions(options)

Compartir pantalla completa del dispositivo (opcional)

Compartir la pantalla completa del dispositivo permite que los agentes de asistencia vean pantallas de aplicaciones externas. Esto suele ser útil cuando los agentes de asistencia al cliente necesitan verificar el estado de la configuración del sistema o ver cómo el usuario navega entre varias aplicaciones. Si no quieres usar esta función, puedes omitir esta sección.

Para personalizar el cuadro de diálogo de consentimiento de Screen Share, debes implementar el protocolo UJETCobrowseAlertProvider en tu clase de proveedor. En esta implementación, devuelve un UIViewController personalizado o cualquier otro objeto UIViewController heredado a través del método de protocolo respectivo. UIViewController debe tener dos botones, uno para aceptar y otro para rechazar.

Después de obtener el consentimiento, pásalo a nuestro SDK llamando al cierre consentStatus. UIViewController de cobrowseFullDeviceRequestAlert El delegado debe contener el RPSystemBroadcastPickerView con el título (consulta el código de ejemplo a continuación) y debe tener otro botón de rechazo. Invoca el cierre descartado cuando se hace clic en el botón de rechazo.

class CobrowseAlertProvider: NSObject, UJETCobrowseAlertProvider {
    func cobrowseSessionInitializationAlert(consentStatus: @escaping (Bool) -> Void) -> UIViewController? {
        let customAlertViewController = CustomAlertViewController()
        customAlertViewController.consentStatus = consentStatus
        return customAlertViewController
    }

    func cobrowseSessionRequestAlert(consentStatus: @escaping (Bool) -> Void) -> UIViewController? {
        // Same as cobrowseSessionInitializationAlert
    }

    func cobrowseRemoteRequestAlert(consentStatus: @escaping (Bool) -> Void) -> UIViewController? {
        // Same as cobrowseSessionInitializationAlert
    }

    func cobrowseFullDeviceRequestAlert(dismissed: @escaping () -> Void) -> UIViewController? {
        let customAlertViewController = CustomFullDeviceAlertViewController()
        cobrowseSessionAlertViewController.dismissed = dismissed
        return customAlertViewController
    }

    func cobrowseSessionEndAlert(consentStatus: @escaping (Bool) -> Void) -> UIViewController? {
        // Same as cobrowseSessionInitializationAlert
    }
}

El controlador de vistas personalizado debe tener un cierre para pasar el estado de consentimiento al SDK.

class CustomAlertViewController: UIViewController {
    var consentStatus: ((Bool) -> Void)?

    @IBAction func allowButtonClicked(_ sender: Any) {
        dismiss(animated: true) {[weak self] in
            self?.consentStatus?(true)
        }
    }

    @IBAction func denyButtonClicked(_ sender: Any) {
        dismiss(animated: true) {[weak self] in
            self?.consentStatus?(false)
        }
    }
}

El controlador de vistas personalizado para la alerta de solicitud de dispositivo completa debe tener RPSystemBroadcastPickerView y un cierre para pasar el estado dismiss al SDK.

class CustomFullDeviceAlertViewController: UIViewController {
    var broadcastPickerView: RPSystemBroadcastPickerView!
    var dismissed: (() -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()

        let frame = CGRect(x: x, y: y, width: 50, height: 50) // Set your own value
        broadcastPickerView = RPSystemBroadcastPickerView(frame: frame)
        broadcastPickerView.preferredExtension = Bundle.main.object(forInfoDictionaryKey: "CBIOBroadcastExtension") as? String // Should have this value as it is
        view.addSubview(broadcastPickerView)
    }

    @IBAction func denyButtonClicked(_ sender: Any) {
        dismiss(animated: true) {[weak self] in
            self?.dismissed?()
        }
    }
}

No olvides pasar este proveedor a nuestro SDK a través de la siguiente API:

let provider = CobrowseAlertProvider()
UJET.setCobrowseAlertProvider(provider)

Extensión de transmisión

La función requiere que se agregue una extensión de transmisión.

  1. Abre tu proyecto de Xcode.

  2. Navega a File > Target.

  3. Elige Broadcast Upload Extension.

  4. Ingresa un Nombre para el objetivo.

  5. Desmarca Include UI Extension.

  6. Crea el destino y anota su ID de paquete.

  7. Cambia el SDK de destino de tu extensión de transmisión a iOS 12.0 o una versión posterior.

Integra el SDK.

CocoaPods: Agrega el siguiente subspec al destino de tu extensión:

target 'MyApp' do
  pod 'UJET'
  pod 'UJET/Cobrowse'
end
target 'MyAppExtension' do
  pod 'UJET/CobrowseExtension'
end

Si usas SwiftPM, selecciona el producto UJETCobrowseExtension y agrégalo al objetivo de tu extensión.

Cómo configurar el uso compartido de llaveros

Tu app y la extensión de la app que creaste anteriormente deben compartir algunos secretos a través del llavero de iOS. Para ello, usan su propio grupo de llavero, de modo que están aisladas del resto del llavero de tus apps.

En ambos, tu destino de la app y tu destino de la extensión, agrega un derecho de uso compartido de Keychain para el grupo de Keychain de io.cobrowse.

Agrega el ID de paquete a tu plist

Toma el ID de paquete de la extensión que creaste anteriormente y agrega la siguiente entrada en el archivo Info.plist de tu app (nota: no en el archivo Info.plist de las extensiones), reemplazando el siguiente ID de paquete por el tuyo:

xml
<key>CBIOBroadcastExtension</key>
<string>your.app.extension.bundle.ID.here</string>

Implementa la extensión

Xcode habrá agregado los archivos SampleHandler.m y SampleHandler.h (o SampleHander.swift) como parte del destino que creaste anteriormente. Reemplaza el contenido de los archivos por lo siguiente:

Swift: Selecciona el producto UJETCobrowseExtension y agrégalo al destino de tu extensión:

import CobrowseIOAppExtension
class SampleHandler: CobrowseIOReplayKitExtension {
}

ObjC

objc// SampleHandler.h
@import CobrowseIOAppExtension;
@interface SampleHandler : CobrowseIOReplayKitExtension
@end// SampleHandler.m
#import "SampleHandler.h"
@implementation SampleHandler
@end

Compila y ejecuta tu app

Ahora puedes compilar y ejecutar tu app. La capacidad completa del dispositivo solo está disponible en dispositivos físicos y no funcionará en el simulador de iOS.

Cómo minimizar el SDK

El SDK de Contact Center AI Platform se puede minimizar cuando hay una sesión de chat o una llamada en curso. Esto puede ser útil cuando deseas dirigir al usuario de vuelta a tu app después de que se recibió un evento del SDK, como un clic en una tarjeta de contenido. Para minimizar el SDK y dirigir a los usuarios de vuelta a tu app, puedes usar lo siguiente:

UJET.minimize(nil)
 // Or if you want to take some action once the SDK has been minimized:

UJET.minimize {
  // Add the code you want to run once the SDK has been minimized here
}

Soluciona problemas

Rechazo del envío de la app

Se rechazó el envío de la app porque se incluyó el framework de CallKit en el territorio de China.

Si Apple rechaza tu app por este motivo, solo deja un comentario, ya que el sistema está diseñado para desactivar el framework de CallKit para la región de China en las llamadas VoIP. Esta función está disponible a partir de la versión 0.31.1 del SDK.

El tamaño del SDK es demasiado grande

Cuando el tamaño del SDK es demasiado grande y difícil de rastrear en GitHub

En este artículo, ofrecen dos opciones. Se recomienda usar Git LFS.

Si no usas bitcode, quitarlo del objeto binario puede ser otra opción. Ejecuta este comando en la carpeta UJETKit.xcframework.

xcrun bitcode_strip -r UJET -o UJET

dyld: Error de biblioteca no cargada

Agrega @executable_path/Frameworks en las rutas de acceso de búsqueda de Runpath desde Target> Build Settings > Linking.

Envío de la app en iTunes Connect

Apple podría hacer la siguiente pregunta durante el proceso de revisión debido al modo en segundo plano de voz sobre IP habilitado:

¿Los usuarios pueden recibir llamadas VoIP en tu app?

Responde a la pregunta.

La notificación de alerta no está disponible cuando se inicia el SDK

Verifica lo siguiente:

  • Usa un dispositivo real, no un simulador.

  • Habilita las notificaciones push y la capacidad de voz por IP de Background Modes >.

Si eso no funciona, intenta compilar con un perfil de aprovisionamiento de distribución (ad hoc o Apple Store).

Envía una notificación push de prueba a tu app de prueba

Prepara tu certificado de VoIP y el token del dispositivo.

En el portal de la plataforma de CCAI, consulta la sección Push Notification Debug en el menú Configuración > Configuración del desarrollador.

Si ya configuraste el certificado para APNs, no tienes que volver a ingresarlo.

Ingresa tu certificado (opcional) y verifica si es de zona de pruebas o no (opcional), y, luego, ingresa el token del dispositivo de la notificación push de la app de prueba.

El inicio de un chat nuevo tardó más de 30 segundos.

Verifica si estás respondiendo a un método delegado de datos personalizados. Debes devolver datos personalizados válidos cuando se soliciten o simplemente devolver nil en el bloque de éxito.

Usa este fragmento de código como ejemplo de configuración:

public func signPayload(_ payload: [AnyHashable: Any]?, payloadType: UjetPayloadType, success: (String?) -> Void, failure: (Error?) -> Void)
{
    if payloadType == .customData {
        success(nil)
    }
}