Guia do SDK do iOS

O SDK para dispositivos móveis da plataforma Contact Center AI (plataforma CCAI) para o sistema operacional Apple iOS oferece a capacidade de incorporar a experiência móvel da plataforma CCAI em aplicativos móveis iOS.

Requisitos

O SDK para dispositivos móveis iOS tem os seguintes requisitos:

  • iOS 12.0+

Recuperar credenciais da empresa

  1. Faça login no portal da Contact Center AI Platform (plataforma da CCAI) usando as credenciais de administrador.

  2. Acesse Configurações > Configurações do desenvolvedor.

  3. Em Chave da empresa e Código secreto, anote a chave e o código secreto da empresa.

Primeiros passos

Veja a seguir um guia sobre como começar a usar o SDK para dispositivos móveis iOS da plataforma CCAI.

Instalação

Para começar, instale o SDK para iOS.

Baixar o app de exemplo

  1. Faça o download do app de exemplo para iOS.

  2. Acesse a pasta e instale as dependências usando o CocoaPods:

    $ pod install --project-directory=ExampleApp
    
  3. Para definir as configurações do projeto rapidamente, execute um script de shell:

    $ ./setup.sh
    

    Como alternativa, é possível editar manualmente as configurações do projeto seguindo estas etapas:

    1. Abra ExampleApp.xcworkspace.

    2. Substitua os valores UJETCompanyKey e UJETCompanySecret em Info.plist pelos valores Chave da empresa e Código secreto da empresa da página Configurações > Configurações do desenvolvedor no portal da plataforma CCAI.

    3. Substitua o valor UJETSubdomain em Info.plist pelo subdomínio no URL do portal da plataforma CCAI. O subdomínio precede diretamente .ujet.com no URL, por exemplo, your-subdomain em https://your-subdomain.ujet.com/settings/developer-setting.

Integrar ao seu projeto

A integração do SDK para iOS com o aplicativo depende do seu ambiente de desenvolvimento.

Gerenciador de pacotes do Swift

  1. Adicione o pacote Swift para o SDK para iOS do GitHub.

  2. Nas configurações de build, coloque -ObjC em "Other Linker Flags".

  3. A partir do Xcode 13.2, há um problema conhecido com o consumo de frameworks binários distribuídos usando o Gerenciador de pacotes do Swift. A solução alternativa atual para esse problema é adicionar uma fase de execução de script às fases de build do projeto do Xcode. Essa fase de execução do script precisa vir após a fase de build Embed Frameworks. Essa nova fase de execução de script precisa conter o seguinte 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. Adicione a seguinte linha ao Podfile:

    pod 'UJET', :podspec =>
    'https://sdk.ujet.co/ios/x.y.z/ujet.podspec' #specific version
    x.y.z
    
  2. Execute pod install. Se o SDK do iOS já tiver sido integrado, execute o pod update CCAI Platform.

Carthage

OGoogle Cloud recomenda o uso de um gerenciador de dependências ou integração manual, porque uma dependência da CCAI Platform não é compatível com o Carthage.

Para usar um gerenciador de dependências diferente, siga estas etapas:

  1. Adicione estas linhas:

    binary "https://sdk.ujet.co/ios/UJETKit.json"
    binary "https://sdk.ujet.co/ios/UJETFoundationKit.json"
    binary "https://sdk.ujet.co/ios/UJETCallRedKit.json"
    binary "https://sdk.ujet.co/ios/UJETChatRedKit.json"
    # optional
    # binary "https://sdk.ujet.co/ios/UJETChatBlueKit.json"
    binary "https://raw.githubusercontent.com/twilio/twilio-voice-ios/Releases/twilio-voice-ios.json"
    # this is not supported: https://github.com/twilio/conversations-ios/issues/12
    # binary "https://raw.githubusercontent.com/twilio/conversations-ios/master/twilio-convo-ios.json"
    
  2. Execute carthage bootstrap --use-xcframeworks (ou carthage update --use-xcframeworks se você estiver atualizando dependências).

Integração manual
  1. Faça o download de UJETKit.xcframework, UJETFoundationKit.xcframework, UJETChatRedKit.xcframework, UJETChatBlueKit.xcframework, UJETTwilioCallKit.xcframework e todas as dependências TwilioVoice.xcframework e TwilioConversationsClient.xcframework.

  2. Adicione o UJETKit.xcframework ao destino arrastando-o para a seção Frameworks, Libraries, and Embedded Content.

  3. Repita as etapas 2 e 3 em todas as dependências da etapa 1.

  4. Nas Configurações de build, coloque -ObjC em Other Linker Flags.

  5. Adicione libc++.tbd como uma dependência na seção Linked Frameworks do destino.

Se você for criar o SDK manualmente com o projeto de exemplo, siga as etapas na seção a seguir.

Criar o SDK manualmente com o projeto de exemplo

Siga estas etapas na ordem:

  1. Faça o download de todos os frameworks, incluindo UJETKit.xcframework e outras dependências.

  2. Crie a pasta CCAI Platform na raiz do projeto e extraia todos os frameworks.

  3. Selecione o destino Objc-Manual ou Swift-Manual e crie.

Importar framework

As seções a seguir mostram como importar o framework.

Projeto em Objective-C

@import UJETKit;

Projeto Swift

swiftimport
UJETimport UJETKit

Inicializar o SDK

Inicialize a CCAI Platform com UJET_COMPANY_KEY e 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;
}

É possível mudar o nível de registro de verbose para error. O nível de registro padrão é UjetLogLevelInfo.

[UJET.setLogLevel:UjetLogLevelVerbose];

Autenticação de usuário final

Acesse o SDK do iOS pelo app iOS.

Para garantir que o usuário final seja autenticado, estamos introduzindo o mecanismo de assinatura JWT.

O SDK para iOS vai pedir para assinar o payload quando a autenticação for necessária. Se a assinatura for bem-sucedida, o aplicativo trocará o JWT assinado pelo token de autenticação do usuário final. O bloco de sucesso ou falha precisa ser chamado antes que o delegado retorne.

Para usuários anônimos (identificador = nil), o aplicativo vai criar um UUID para o usuário. Se, posteriormente, o usuário for autenticado com um identificador, o aplicativo tentará mesclar os dois usuários com base no UUID.

Em UJETObject.h do projeto de exemplo:

@import UJETKit;

@interface UJETObject : NSObject <UJETDelegate>

Implemente o método delegado signPayload: payloadType: success: failure:.

-   (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;
}

Recomendamos assinar o payload no servidor do aplicativo, não no cliente.

Este exemplo usa a assinatura local para fins de teste. Consulte signDataInRemote: success: failure: no arquivo UJETObject.m.

Para mais informações, consulte Autenticação do usuário final do SDK.

Configurar notificações push

A CCAI Platform envia notificações push para solicitar ações inteligentes, como verificação e foto, além de informar uma chamada recebida. Se as filas de chat usarem o Check-in (requer o SDK do iOS 2.16.0 ou mais recente), a CCAI Platform vai enviar uma notificação push solicitando que os consumidores em espera retornem ao chat e confirmem que estão prontos. Consulte Check-in para SDK para dispositivos móveis. A CCAI Platform exige que dois tipos diferentes de certificados (VoIP e APNs) sejam salvos no Portal de Administração da CCAI Platform.

Configurar certificados

A CCAI Platform exige que dois tipos diferentes de certificados de push da Apple sejam salvos no portal de administração da CCAI Platform:

  • Um certificado SSL do serviço de notificação push da Apple (APNs, na sigla em inglês), usado para notificações push remotas padrão.

  • Um certificado de serviços de VoIP, usado para notificações de chamadas recebidas e determinadas ações inteligentes relacionadas a chamadas.

Os dois certificados são criados no portal do desenvolvedor da Apple, convertidos para o formato PEM e, em seguida, enviados para o portal de administração da plataforma CCAI.

Criar um certificado SSL do APNs

Siga as próximas etapas com base no guia oficial da Apple Como estabelecer uma conexão baseada em certificado com APNs para criar um certificado SSL de APNs.

  • Faça login em developer.apple.com e acesse Certificates, identifiers & profiles.

  • Clique no botão + para criar um certificado.

  • Em Serviços, selecione SSL do serviço de notificação push da Apple (Sandbox e produção).

  • Escolha o ID do app que corresponde ao identificador do pacote do seu aplicativo e clique em Continue.

  • Faça upload de uma solicitação de assinatura de certificado (CSR) gerada pelo acesso às chaves no Mac:

    • Abra Acesso às Chaves > Assistente de Certificado > Solicitar um certificado de uma autoridade certificadora.

    • Digite seu endereço de e-mail, deixe Endereço de e-mail da CA em branco, selecione Salvo no disco e clique em Continuar.

  • Clique em Continuar e em Fazer o download do arquivo .cer gerado.

  • Clique duas vezes no arquivo .cer para instalá-lo no seu conjunto de chaves.

  • Em Acesso às Chaves, localize o certificado instalado, clique nele com o botão direito do mouse e selecione Exportar… para salvá-lo como um arquivo .p12. Você vai precisar definir uma senha de exportação.

  • Converta o arquivo .p12 para o formato PEM usando openssl:

# Extract the certificate
openssl pkcs12 -in apns_certificate.p12 -out apns_cert.pem -clcerts -nokeys
# Extract the private key
openssl pkcs12 -in apns_certificate.p12 -out apns_key.pem -nocerts -nodes
# (Optional) Combine into a single PEM file
cat apns_cert.pem apns_key.pem > apns_combined.pem

Criar um certificado de serviços de VoIP

Os certificados de serviços de VoIP permitem que seu app receba notificações push de alta prioridade pelo framework PushKit da Apple, que pode ativar o app de um estado encerrado para processar chamadas recebidas. Para ver as etapas oficiais mais recentes, consulte Criar certificados de serviços de VoIP na documentação para desenvolvedores da Apple.

  • Faça login em developer.apple.com e acesse Certificates, Identifiers & Profiles.

  • Clique no botão + para criar um certificado.

  • Em Serviços, selecione Certificado de serviços de VoIP.

  • Escolha o ID do app que corresponde ao identificador do pacote do seu aplicativo e clique em Continue.

  • Faça upload da mesma solicitação de assinatura de certificado (CSR) que você gerou na seção anterior (ou crie uma nova).

  • Clique em Continuar e em Fazer o download do arquivo .cer gerado.

  • Clique duas vezes no arquivo .cer para instalá-lo no Keychain.

  • Em Acesso às Chaves, localize o certificado de serviços de VoIP instalado, clique com o botão direito do mouse nele e selecione Exportar… para salvá-lo como um arquivo .p12.

  • Converta o arquivo .p12 para o formato PEM usando openssl:

# Extract the certificate
openssl pkcs12 -in voip_certificate.p12 -out voip_cert.pem -clcerts -nokeys
# Extract the private key
openssl pkcs12 -in voip_certificate.p12 -out voip_key.pem -nocerts -nodes
# (Optional) Combine into a single PEM file
cat voip_cert.pem voip_key.pem > voip_combined.pem

Fazer upload de certificados para o portal de administração da plataforma CCAI

  • Faça login no portal de administração da plataforma CCAI.

  • Acesse Configurações > Configurações do desenvolvedor > App para dispositivos móveis.

  • Faça upload do certificado PEM de APNs.

  • Faça upload do certificado PEM de VoIP.

Ativar notificações push

  1. Selecione o destino e abra a guia Capabilities.

  2. Ative a opção Notificações push.

Testar notificações push

As seções a seguir mostram como testar as notificações push.

Seção de depuração de notificações push

No portal do administrador, acesse Configurações > Configurações do desenvolvedor. Nessa página, encontre a seção Depuração de notificações push:

Copie e cole o token do dispositivo na área de texto à direita e selecione o app para dispositivos móveis correto.

Receber o token do dispositivo

Veja um exemplo de string de token de dispositivo:

7db0bc0044c8a203ed87cdab86a597a2c43bf16d82dae70e8d560e88253364b7

As notificações push geralmente são definidas na classe que está em conformidade com o protocolo UIApplicationDelegate ou PKPushRegistryDelegate. Em algum momento, o token do dispositivo estará disponível para você. Você pode imprimir o token antes de transmiti-lo ao SDK do iOS. Para receber o token do dispositivo, use o snippet 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

Depois de inserir o arquivo PEM do certificado e o token do dispositivo, clique no botão.

O resultado vai mostrar a mensagem Notificação push configurada com sucesso se a notificação push de teste tiver sido entregue.

A entrega de uma notificação push não é garantida, dependendo da conexão de rede do dispositivo.

Configurações do projeto

As seções a seguir descrevem as mudanças necessárias para configurar o projeto.

Recursos

Nas configurações de destino, ative os seguintes recursos:

  • Notificações push

  • Modos em segundo plano (marque estes itens)

  • Áudio e AirPlay

  • Voz sobre IP

Info.plist

Para proteger a privacidade do usuário, qualquer app iOS vinculado ao iOS 10.0 ou versões mais recentes que acesse o microfone, a biblioteca de fotos e a câmera do dispositivo precisa declarar essa intenção. Inclua as seguintes chaves com um valor de string no arquivo Info.plist do seu app e forneça uma string de finalidade para essa chave. Se o app tentar acessar qualquer um dos microfones, a biblioteca de fotos e a câmera do dispositivo sem uma string de finalidade correspondente, ele será encerrado.

  • NSMicrophoneUsageDescription: permite o acesso ao microfone para ligar e conversar com as equipes de suporte ou solução de problemas e para enviar vídeos com som relacionados a consultas de produtos.

  • NSCameraUsageDescription: permite acesso à câmera para que o cliente tire e envie fotos relacionadas à consulta de suporte ao cliente.

  • NSPhotoLibraryUsageDescription: permite que o cliente envie fotos relacionadas à consulta de suporte ao cliente.

  • NSFaceIDUsageDescription: permite o acesso à verificação usando o Face ID.

Iniciar o SDK para iOS

Adicione a seguinte linha onde você quer iniciar o SDK para iOS:

[UJET startWithOptions:nil];

Também é possível iniciar o SDK para iOS em um ponto específico do menu com essa chave usando um ponto de acesso direto:

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

O menuKey pode ser criado com um ponto de acesso direto (DAP, na sigla em inglês). As etapas a seguir mostram como criar um DAP:

  1. Faça login no portal da plataforma CCAI com as credenciais de administrador.

  2. Acesse Configurações > Fila.

  3. Selecione qualquer fila na estrutura do menu.

  4. Selecione Criar ponto de acesso direto.

  5. Insira a chave no formulário de texto.

  6. Clique em Salvar.

Limpar o cache do local se os dados do usuário tiverem sido atualizados

Estamos armazenando em cache o token de autenticação no Keychain para reutilizar e fazer solicitações menos frequentes para assinar o payload do app host. O SDK vai usá-lo até que ele expire ou seja revogado pela chamada clearUserData. O app host é responsável por revogar esse cache sempre que os dados relacionados ao usuário forem alterados ou atualizados, como em um evento de saída.

[UJET clearUserData];

Verificar se há uma sessão antes de iniciar o Contact Center AI Platform

Antes de iniciar uma sessão, verifique se não há uma sessão atual. Isso é especialmente importante quando o userId muda.

[UJET getStatus];

Se houver uma sessão, peça ao usuário para retomá-la ou cancelar a ação:

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

Personalizar

Há várias opções para o tema do SDK listadas em UJETGlobalTheme.h.

Defina o tema depois de [UJET initialize]. Por exemplo:

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];

O nome da empresa é recuperado em Portal de administração > Configurações > Detalhes da central de suporte > Nome de exibição.

É possível definir a imagem do logotipo em vez do nome da empresa, desta forma:

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

A imagem será redimensionada para caber na área se for muito grande.

Strings

Também é possível personalizar strings substituindo o valor. Por exemplo, coloque esta chave-valor no seu Localizable.strings:

"ujet_greeting_title" = "Title";

"ujet_greeting_description" = "Description";

As strings personalizáveis disponíveis estão listadas no arquivo ujet.strings.

Modo escuro

É possível especificar uma tonalidade da cor que você quer para o modo escuro para melhorar a legibilidade das fontes.

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

Se você não definir a propriedade, UJETGlobalTheme.tintColor será usado para o modo escuro. Recomendamos definir essa propriedade se o app for compatível com o modo escuro. Consulte os seguintes artigos da Apple sobre como escolher a cor de tonalidade certa para o modo escuro:

Tema do chat

Para personalizar a tela de chat, você pode usar uma string JSON ou cada classe de tema.

Para referência, consulte o app de exemplo e remova a marca de comentário do 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)
}

Tema dos cards de conteúdo

É possível adicionar personalização para cards de conteúdo junto com a personalização de chat. Para isso, use o arquivo JSON (consulte a propriedade content_card) ou a classe 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)
}

Tema do card de formulário

É possível adicionar personalização para cards de formulário junto com a personalização do chat. Para isso, use o arquivo JSON (consulte form_card property) ou a classe 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)
}

Configuração do formulário da Web

Para configurar o recurso de formulário da Web, implemente o método ujetWebFormDidReceive do protocolo UJETDelegate. Esse método recebe um evento (um dicionário FormMessageReceivedEvent) como parâmetro, que contém informações relacionadas ao formulário. O dicionário de eventos (FormMessageReceivedEvent) inclui a seguinte estrutura JSON:

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

Para processar o evento, faça o seguinte:

  1. Extraia as informações relevantes do dicionário de eventos (smart_action_id, external_form_id e signature).

  2. Gere um URI de formulário e uma assinatura para os dados do formulário.

  3. Transmita os dados do formulário para o SDK como um dicionário FormDataEvent usando completion closure.

  4. Se ocorrer algum erro durante a geração do URI/assinatura, invoque o callback usando callback.onError() com o Error.

O dicionário (FormDataEvent) transmitido ao SDK precisa ter a seguinte estrutura:

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

A assinatura (HMAC-SHA:256) precisa ser gerada usando o data, assinado com a chave secreta compartilhada. As chaves de objeto dos dados precisam ser ordenadas em ordem alfabética antes de gerar assinaturas, e o mesmo data precisa ser enviado ao SDK.

Transferência pós-sessão

É possível adicionar personalização para o VA pós-sessão junto com a personalização do chat. Isso pode ser feito usando o arquivo JSON (consulte a propriedade post_session) ou a classe UJETChatPostSessionVaTheme. A largura da borda pode ser apenas 0 ou 1. Se você não quiser diferenciar a experiência do agente virtual pós-sessão, defina containerColor como branco e a borda como 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)
}

Menu de ações do chat

É possível adicionar personalização ao menu de ações de chat junto com a personalização do chat. Isso pode ser feito usando o arquivo JSON (consulte a propriedade form_card) ou a classe 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)
}

Outras aparições

Você pode personalizar outras aparências, como o tamanho da fonte e a cor do plano de fundo.

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

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

CallKit

No iOS 10.0 e versões mais recentes, o CallKit é ativado para todas as chamadas.

Com o CallKit, ele mostra uma chamada no app com a tela de chamada e mostra a chamada no histórico de ligações do smartphone.

Para iniciar uma nova sessão de suporte da CCAI Platform pelo histórico de ligações, adicione o seguinte bloco ao 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;
}

O CallKit permite que um ícone de 40 x 40 seja exibido na tela de bloqueio ao receber uma chamada enquanto o dispositivo está bloqueado. Coloque uma imagem nos Xcassets chamada "icon-call-kit".

Configurar o SDK

É possível definir várias opções antes de iniciar o SDK.

Confira os detalhes na classe UJETGlobalOptions.

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

[UJET setGlobalOptions:options];

Mostrar ou ocultar o botão de download da transcrição

É possível configurar o SDK para mostrar ou ocultar o botão de download da transcrição no menu "opções" do chat e na tela pós-chat.

O código a seguir mostra como configurar o botão de download da transcrição:

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

@property (nonatomic, assign) UJETChatDownloadTranscriptVisibilityOptions transcriptVisibilityOptions;

Fallback de RPTC

Oferecemos o fallback de PSTN para várias situações:

  • A rede móvel está off-line.

  • Não é possível acessar o back-end do aplicativo.

  • O VoIP não está disponível

    • A condição da rede não é boa o suficiente para se conectar. Consulte a propriedade UJETGlobalOptions.pstnFallbackSensitivity para mais detalhes.

    • Ocorreu uma falha durante a conexão devido à configuração do firewall ou a um problema do provedor.

Recomendamos definir o número da URA da sua empresa em UJETGlobalOptions.fallbackPhoneNumber. O formato recomendado é + seguido pelo código do país e número de telefone. Por exemplo: +18001112222.

Sensibilidade do substituto da RPTC

É possível ajustar o nível de sensibilidade da verificação da condição da rede para o fallback de PSTN.

@property (nonatomic, assign) float pstnFallbackSensitivity;

O valor precisa estar no intervalo de 0,0 a 1,0. Se definido como 1, a chamada sempre será conectada pela PSTN, em vez de VoIP. A latência máxima e o limite mínimo de largura de banda são 10.000 ms e 10 KB/s, respectivamente, para o valor 0. Por exemplo, um valor de 0,5 significa que a latência e a largura de banda mínimas são de 5.000 ms e 15 KB/s, respectivamente.

Para configurar esse valor, siga estas etapas:

  1. Faça login no portal da plataforma CCAI como administrador.

  2. Acesse Configurações > Configurações do desenvolvedor > Apps para dispositivos móveis.

  3. Encontre a seção Limite de números de telefone de fallback. O valor padrão é 0,85.

  4. Especifique o novo valor do limite.

  5. Clique em Salvar.

Desativar as notificações push no nível global

É possível desativar as notificações push no nível global. Definir a propriedade a seguir como false ignora todas as dependências de notificação push e impede que elas cheguem aos usuários finais:

@property (nonatomic, assign) BOOL allowsPushNotifications;

Ignorar modo escuro

É possível ignorar o modo escuro no SDK da plataforma CCAI especificamente com esta propriedade:

@property (nonatomic, assign) BOOL ignoreDarkMode;

Ocultar barra de status

É possível controlar a visibilidade da barra de status com esta propriedade:

  @property (nonatomic, assign) BOOL hideStatusBar;

Por padrão, o hideStatusBar é definido como false e visible.

Pular a pesquisa de CSAT

É possível adicionar um botão que permite ao usuário pular a pesquisa de CSAT. O exemplo de código a seguir mostra como adicionar o botão:

let options = UJETGlobalOptions()
options.skipCsat = true

Personalizar o indicador de atividade

É possível adicionar sua própria animação de carregamento (em um UIView) ao SDK e substituir o UIActivityIndicatorView padrão. Implemente o método ujet_activityIndicator de UJETDelegate e retorne a visualização 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
}

Se você já tiver definido UIUserInterfaceStyle como Light no Info.plist do app para desativar o modo escuro, ignore essa propriedade.

Idioma preferido

O SDK da plataforma CCAI vai usar a seguinte ordem de prioridade para determinar o idioma preferencial.

  1. Idioma selecionado na tela de apresentação do app.

  2. O idioma padrão foi selecionado em UJETGlobalOptions. É possível definir o idioma padrão com a propriedade preferredLanguage. Os códigos de idioma compatíveis podem ser encontrados no arquivo UJETGlobalOptions.h.

  3. O idioma selecionado no dispositivo (em Configurações > Geral > Idioma e região) será usado quando for compatível com o app.

  4. O dialeto mais próximo do idioma do dispositivo será usado quando o aplicativo não oferecer suporte ao idioma do dispositivo, mas oferecer suporte ao dialeto principal mais próximo. Por exemplo, se o usuário selecionar o espanhol de Cuba como idioma no dispositivo e o app não oferecer suporte a esse dialeto, mas oferecer suporte ao espanhol, o idioma espanhol será usado.

  5. O inglês será usado se o idioma do dispositivo não for compatível com o app.

Configurar ícones de link de desvio externo

Para personalizar o ícone no canal de link de desvio externo, faça upload dele no catálogo de recursos do app e use o mesmo nome ao criar o link de desvio externo em Configurações > Chat > Links de desvio externo > Ver links > Adicionar link de desvio no Portal de Administração. Se o nome do ícone no Portal de Administração não corresponder ao ícone enviado para o app, o SDK vai usar o ícone padrão. Consulte este link sobre como adicionar imagens ao catálogo de recursos.

Enviar dados personalizados para seu CRM

Você pode enviar dados personalizados para o tíquete do CRM.

Há dois métodos para enviar dados personalizados:

  1. Método seguro: assinatura de dados predefinida com JWT.

  2. Método não seguro: dados predefinidos com JSON simples (não recomendado).

Como usar o método seguro para enviar dados personalizados

Você precisa implementar o método de assinatura. Primeiro, você pode colocar seus dados personalizados no lado do cliente e enviá-los ao servidor para que sejam assinados. No servidor, é possível adicionar outros dados por formulário definido, assinar com o company.secret e retornar por 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];
    }
}

Uso de método não seguro para enviar dados personalizados

Esse método não é recomendado porque cria uma vulnerabilidade em potencial que pode abrir seu aplicativo para um ataque de interceptação. Se você optar por usar esse método, não seremos responsáveis pela exposição de segurança e pelos possíveis danos que possam ocorrer. Recomendamos que você use o método seguro descrito anteriormente para enviar dados personalizados no seu aplicativo. Ou você pode simplesmente iniciar o SDK do iOS com a instância UJETCustomData. Nesse caso, o delegado signPayload para UJETPayloadCustomData precisa apenas chamar 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];

Como usar dados personalizados não assinados para enviar transcrições de chat externas

É possível enviar a transcrição do chat para o SDK quando ele é iniciado com dados personalizados não assinados chamando o método setExternalChatTransfer: ou setExternalChatTransferWithDictionary: para definir dados JSON com NSString ou 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: dicionário

    • name: string

    • avatar: string [URL do avatar do agente, opcional]

  • transcript: matriz

    • sender: string [end_user ou agent]

    • timestamp: string [por exemplo, 2021-03-15 12:00:00Z]

    • content: matriz

      • type: string [text ou media]

      • text: string [obrigatório para o tipo de texto]

      • media: dicionário [obrigatório para o tipo de mídia]

        • type: string [image ou video]

        • url: string [URL público que aponta para o arquivo de mídia]

Exemplo 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"
        }
      ]
    }
  ]
}

É possível usar o Markdown no tipo de texto. A sintaxe aceita inclui itálico, negrito, lista com marcadores, hiperlink e sublinhado (--text--).

Exemplo de dados personalizados

Esta seção mostra um exemplo de dados personalizados.

JSON codificado para JWT

O arquivo JSON precisa validar o JWT. O objeto de dados personalizados é o valor da chave 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 dado é semelhante ao formato de objeto JSON e precisa conter a chave, o valor, o tipo e o rótulo.

A chave é o identificador exclusivo dos dados. O rótulo é o nome de exibição na página do CRM. O tipo é o tipo do valor.

  • string

    • String JSON
  • número

    • número inteiro, ponto flutuante
  • data

    • Formato de carimbo de data/hora Unix UTC com 13 dígitos. (contém milissegundos)
  • URL

    • Formato do URL HTTP
Exemplo de CRM

Veja um exemplo de CRM.

Local

Use o framework CoreLocation. Para mais detalhes, consulte AppDelegate.m.

Versão do SO do dispositivo
[customData set:@"os_version" label:@"OS Version" stringValue:[[UIDevice currentDevice] systemVersion]];

Impedir a exibição de dados personalizados

É possível usar a propriedade invisible_to_agent com um objeto de dados personalizados para impedir que dados personalizados assinados ou não assinados sejam mostrados no adaptador do agente. No exemplo anterior, o CPF do usuário final não é mostrado no adaptador do agente porque "invisible_to_agent" : true está incluído no objeto ssn.

Quando você inclui a propriedade "invisible_to_agent" : true com um objeto de dados personalizado, o comportamento esperado é o seguinte:

Para mais informações, consulte Ver dados da sessão no adaptador do agente.

Propriedades de dados reservadas

É possível enviar propriedades de dados do cliente para a Contact Center AI Platform (plataforma CCAI) como dados personalizados assinados quando uma sessão começa. Para mais informações, consulte Enviar propriedades de dados reservadas.

Confira um exemplo de propriedades de dados reservadas em dados personalizados:

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

Defina cada valor como true ou false.

  • reserved_verified_customer: true se você considerar que esse usuário final é um cliente legítimo.
  • reserved_bad_actor: true se você considerar que esse usuário final pode ser um usuário de má-fé.
  • reserved_repeat_customer: true se você determinou que esse usuário final já entrou em contato com seu contact center antes.

Personalizar o Flow

Nesta seção, descrevemos como personalizar o fluxo.

Desconectar a CCAI Platform para processar eventos do app host

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

Adiar uma chamada ou um chat recebido da CCAI Platform

Implemente um método delegado para processar eventos recebidos:

-   (BOOL)shouldConnectUjetIncoming:(NSString *)identifier forType:(UjetIncomingType)type {
  if (weDoingSomething) {
    // save identifier and type
    return NO; // postpone
  } else {
    return YES;
  }
}
Conectar evento adiado
[UJET connect:identifier forType:UjetIncomingTypeCall];

Configurar link direto

Isso permite que os agentes em chamadas PSTN usem ações inteligentes por SMS, tanto quando um usuário final tem ou não o app.

Acesse Configurações > Gerenciamento de operações > Ativar o envio de SMS para fazer o download do app no portal da plataforma CCAI.

Você pode definir o URL do aplicativo com sua página da Web (por exemplo, https://your-company.com/support) depois de configurar o link universal ou o esquema de URL personalizado. Você pode selecionar qualquer uma das opções.

Implementar o método delegado para processar o link direto

O link universal e o URL personalizado são semelhantes a https://your-company.com/support?call_id=x&nonce=y e your-company://support?call_id=x&nonce=y, respectivamente. Coloque um dos seus links sem parâmetros de consulta em "URL do aplicativo" no Portal de Administração. Por exemplo, coloque your-company://support se estiver usando um esquema de URL personalizado.

No método delegado, chame [UJET start] apenas quando os caminhos do URL e os parâmetros no link universal ou no URL personalizado forem específicos da plataforma da 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];
  }
  ...
}

Se o app adotar UIWindowSceneDelegate, adicione este snippet 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 mais informações, consulte os códigos de exemplo do arquivo UJETObject+DeepLink.

Observar evento da plataforma CCAI

Publicamos os seguintes eventos por meio de NSNotificationCenter.defaultCenter. Você pode ouvi-los e personalizar seu fluxo de acordo com seu caso de uso, por exemplo, layout de teclado personalizado.

  • UJETEventEmailDidClick

    • Dados do menu de fila
  • UJETEventEmailDidSubmit

    • Dados do menu de fila

    • has_attachment: (NSNumber) @YES, @NO

  • UJETEventSessionViewDidAppear

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

    • timestamp: (NSString) ISO 8601

  • UJETEventSessionViewDidDisappear

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

    • timestamp: (NSString) ISO 8601

  • UJETEventSessionDidCreate

    • Dados da sessão
  • UJETEventSessionDidEnd

    • Dados da sessão

    • agent_name: (NSString) nulo se o agente não entrou

    • duration: (NSNumber) somente para chamadas

    • ended_by: (NSString)

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

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

  • UJETEventSdkDidTerminate

  • UJETEventPostSessionOptInDidSelected

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

Dados do evento

Metadados

  • application: @"iOS"

  • app_id: (NSString) identificador do pacote

  • app_version: (NSString)

  • company: (NSString) subdomain

  • device_model: (NSString)

  • device_version: (NSString)

  • sdk_version: (NSString)

  • timestamp: (NSString) ISO 8601

Dados do cardápio da fila

Metadados

  • menu_id: NSString

  • menu_key: NSString, nullable

  • menu_name: NSString

  • menu_path : NSString

Dados da sessão

Metadados

  • session_id: NSString

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

  • end_user_identifier: NSString

Configurar compartilhamento de tela

Se você quiser usar o recurso de compartilhamento de tela, integre UJETCobrowseKit.xcframework.

CocoaPods

Adicione esta subespecificação ao destino do app:

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

Configurar o compartilhamento de tela com o Carthage

Adicione esta linha ao Cartfile:

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

SwiftPM

Selecione os produtos UJET e UJETCobrowse e adicione ao destino do app.

Defina o valor de UJETGlobalOptions.cobrowseKey como a chave de navegação conjunta da sua empresa.

swift
let options = UJETGlobalOptions()
options.cobrowseKey = cobrowseKey

UJET.setGlobalOptions(options)

Compartilhamento de tela cheia do dispositivo (opcional)

O compartilhamento de tela cheia do dispositivo permite que os agentes de suporte visualizem telas de aplicativos fora do seu. Isso costuma ser útil quando os agentes de suporte precisam verificar o estado das configurações do sistema ou ver o usuário navegar entre vários aplicativos. Se você não quiser esse recurso, pule esta seção.

Para personalizar a caixa de diálogo de consentimento de compartilhamento de tela, implemente o protocolo UJETCobrowseAlertProvider na sua classe de provedor. Nessa implementação, retorne um UIViewController personalizado ou qualquer outro objeto herdado de UIViewController pelo método de protocolo correspondente. UIViewController precisa ter dois botões: um para aceitar e outro para negar.

Depois de receber o consentimento, transmita-o ao nosso SDK chamando o encerramento consentStatus. UIViewController de cobrowseFullDeviceRequestAlert. O delegado precisa conter o RPSystemBroadcastPickerView com o título (veja no exemplo de código abaixo) e ter outro botão de negação. Invoque o fechamento dispensado ao clicar no botão de negação.

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
    }
}

O controlador de visualização personalizado precisa ter um fechamento para transmitir o status de consentimento ao 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)
        }
    }
}

O controlador de visualização personalizado para o alerta de solicitação de dispositivo completo precisa ter RPSystemBroadcastPickerView e um fechamento para transmitir o status dismiss ao 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?()
        }
    }
}

Não se esqueça de transmitir esse provedor ao nosso SDK usando a seguinte API:

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

Extensão de transmissão

O recurso exige a adição de uma extensão de transmissão.

  1. Abra o projeto do Xcode.

  2. Acesse Arquivo > Destino.

  3. Escolha Extensão de upload de transmissão.

  4. Insira um Nome para o destino.

  5. Desmarque a caixa de seleção Incluir extensão de interface.

  6. Crie o destino, anotando o ID do pacote.

  7. Mude o SDK de destino da sua extensão de transmissão para o iOS 12.0 ou mais recente.

Integre o SDK

CocoaPods{: adicione a seguinte subespecificação ao destino da extensão:

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

Se você estiver usando o SwiftPM, selecione o produto UJETCobrowseExtension e adicione-o ao destino da extensão.

Configurar o compartilhamento de conjunto de chaves

O app e a extensão criada anteriormente precisam compartilhar alguns secrets pelo conjunto de chaves do iOS. Eles fazem isso usando o próprio grupo de conjunto de chaves para que fiquem isolados do restante do conjunto de chaves dos seus apps.

No Xcode, selecione o destino do app e da extensão. Adicione um direito de compartilhamento de conjunto de chaves para o grupo de conjunto de chaves io.cobrowse.

Adicionar o ID do pacote ao plist

Pegue o ID do pacote da extensão que você criou anteriormente e adicione essa entrada ao Info.plist do seu app.

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

Implementar a extensão

O Xcode vai adicionar os arquivos SampleHandler.m e SampleHandler.h (ou SampleHandler.swift) como parte do destino que você criou anteriormente. Substitua o conteúdo dos arquivos pelo seguinte:

Swift:

Selecione o produto UJETCobrowseExtension e adicione ao destino da extensão:

import CobrowseIOAppExtension
class SampleHandler: CobrowseIOReplayKitExtension {
}

ObjC:

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

Criar e executar seu app

Agora você já pode criar e executar seu app. A capacidade total do dispositivo só está disponível em dispositivos físicos, não funciona no simulador do iOS.

Minimizar o SDK

O SDK da Contact Center AI Platform pode ser minimizado quando uma sessão de chat ou uma chamada está em andamento. Isso pode ser útil quando você quer direcionar o usuário de volta ao seu app depois que um evento do SDK é recebido, como um clique em um card de conteúdo. Para minimizar o SDK e direcionar o usuário de volta ao app, use:

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
}

Fallback

Use a função didHandleUjetError para o substituto de erros inesperados. Se você não usar essa função ou se ela retornar false, o SDK do iOS vai processar o erro.

A tabela a seguir mostra os erros que a função didHandleUjetError detecta:

Tipo de erro Código do erro Descrição
networkError 1 A rede não está disponível.

Observação: esse erro não é acionado quando a rede não está disponível durante uma sessão de chat ou chamada ou uma tela de avaliação.

authenticationError 100 Ocorreu um erro inesperado durante a autenticação.
authenticationJwtError 101 Ocorreu um erro inesperado durante a validação do JWT, por exemplo, um erro de análise.
voipConnectionError 1000 Falha ao estabelecer uma conexão com o provedor de VoIP. Um callback do SDK de VoIP processa isso.
voipLibraryNotFound 1001 O sistema espera que uma chamada seja conectada por um provedor de VoIP, mas não encontra nenhum. Isso pode acontecer se você integrar o SDK errado ou não adicionar uma biblioteca de provedor de VoIP às suas dependências.
chatLibraryNotFound 1100 Ocorre quando o sistema não consegue encontrar a biblioteca de chat. Isso pode acontecer quando você integra o SDK errado ou não adiciona uma biblioteca de chat da Twilio às dependências.

O exemplo de código a seguir mostra como usar a função 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
    }
}

Solução de problemas

Esta seção fornece informações para ajudar você a resolver problemas comuns.

Rejeição do envio do app

O envio do app é rejeitado devido à inclusão do framework CallKit no território da China.

Se o app for rejeitado pela Apple por esse motivo, deixe um comentário, já que o sistema foi projetado para desativar o framework do CallKit para a região da China em chamadas VoIP. Isso é válido a partir da versão 0.31.1 do SDK.

O tamanho do SDK é muito grande

Quando o tamanho do SDK é muito grande e difícil de rastrear no GitHub

Neste artigo, eles oferecem duas opções. É recomendável usar o Git LFS.

Se você não estiver usando o Bitcode, remover o bitcode do binário pode ser outra opção. Execute este comando na pasta UJETKit.xcframework.

xcrun bitcode_strip -r UJET -o UJET

dyld: erro de biblioteca não carregada

Adicione @executable_path/Frameworks em Runpath Search Paths em Target > Build Settings > Linking.

Envio de apps no iTunes Connect

A Apple pode fazer a seguinte pergunta durante o processo de revisão devido ao modo de segundo plano de voz sobre IP ativado:

Os usuários podem receber chamadas VoIP no seu app?

Responda Yes à pergunta.

A notificação de alerta fica indisponível ao iniciar o SDK

Verifique se:

  • Use um dispositivo real, não um simulador.

  • Ative as notificações push e o recurso Modos em segundo plano > Voz sobre IP.

Se isso não ajudar, tente criar com o perfil de provisionamento de distribuição (Ad-hoc ou Apple Store).

Testar a notificação push no app de teste

Prepare o certificado de VoIP e o token do dispositivo.

No portal da plataforma CCAI, consulte a seção Depuração de notificações push no menu Configurações > Configurações do desenvolvedor.

Se você já tiver definido o certificado para APNS, não será necessário inseri-lo novamente.

Insira seu certificado (opcional) e verifique se é um sandbox ou não (opcional) e insira o token do dispositivo de notificação push do app de teste.

O início de uma nova conversa levou mais de 30 segundos

Verifique se você está respondendo a um método delegado de dados do cliente. Você precisa retornar dados personalizados válidos mediante solicitação ou apenas retornar nil no bloco de sucesso.

Use este snippet de código como um exemplo de configuração:

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