iOS SDK 指南

适用于 Apple iOS 操作系统的 Contact Center AI 平台 (CCAI 平台) 移动 SDK 能够将 CCAI 平台移动体验嵌入到 iOS 移动应用中。

开始使用

以下指南介绍了如何开始使用 CCAI Platform iOS 移动 SDK。

要求

iOS 移动 SDK 具有以下要求:

  • iOS 13.0+

检索公司凭证

  1. 使用管理员凭据登录 Contact Center AI 平台 (CCAI Platform) 门户。

  2. 依次前往设置 > 开发者设置。

  3. 在公司密钥和密码下,记下公司密钥和公司密码。

安装

若要开始使用,您需要安装 iOS SDK。

下载示例应用

  1. 下载 iOS 示例应用。

  2. 前往该文件夹,并使用 CocoaPods 安装依赖项:

    $ pod install --project-directory=ExampleApp
    
  3. 如需快速配置项目设置,请运行 shell 脚本:

    $ ./setup.sh
    

    或者,您也可以按照以下步骤手动修改项目设置:

    1. 打开 ExampleApp.xcworkspace。

    2. 将 Info.plist 中的 UJETCompanyKey 和 UJETCompanySecret 值替换为 CCAI Platform 门户中设置 > 开发者设置页面中的公司密钥和公司密码值。

    3. 将 Info.plist 中的 UJETSubdomain 值替换为 CCAI 平台门户网址中的子网域。子域名在网址中直接位于 .ujet.com 之前,例如 https://your-subdomain.ujet.com/settings/developer-setting 中的 your-subdomain。

集成到您的项目中

iOS SDK 与应用的集成取决于您的开发环境。

Swift 软件包管理器

  1. 添加 iOS SDK 的 Swift 软件包。

  2. 在 build 设置中,将 -ObjC 放在“Other Linker Flags”(其他链接器标志)中。

  3. 从 Xcode 13.2 开始,使用 Swift Package Manager 分发的二进制框架存在一个已知问题。此问题的当前解决方法是向 Xcode 项目的构建阶段添加运行脚本阶段。此“运行脚本”阶段应在“嵌入框架”构建阶段之后。此新 Run Script Phase 应包含以下代码:

    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. 将以下行添加到 Podfile:

    pod 'UJET', :podspec =>
    'https://sdk.ujet.co/ios/x.y.z/ujet.podspec' #specific version
    x.y.z
    
  2. 运行 pod install。如果之前已集成 iOS SDK,请改为运行 pod update CCAI Platform。

Carthage

Google Cloud 建议使用依赖项管理器或手动集成,因为 CCAI Platform 依赖项不支持 Carthage。

如需使用其他依赖项管理器,请按以下步骤操作:

  1. 添加以下几行内容:

    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. 运行 carthage bootstrap --use-xcframeworks(如果您要更新依赖项,则运行 carthage update --use-xcframeworks)。

手动集成

  1. 下载 UJETKit.xcframework、UJETFoundationKit.xcframework、UJETChatRedKit.xcframework、UJETChatBlueKit.xcframework、UJETCallRedKit.xcframework 以及所有依赖项(例如 TwilioVoice.xcframework 和 TwilioConversationsClient.xcframework)。

  2. 将 UJETKit.xcframework 拖动到“Frameworks, Libraries, and Embedded Content”(框架、库和嵌入式内容)部分,将其添加到您的目标。

  3. 对第 1 步中的所有依赖项重复第 2 步和第 3 步。

  4. 在build 设置中,将 -ObjC 放在 Other Linker Flags 上。

  5. 在目标的 Linked Frameworks 部分中将 libc++.tbd 添加为依赖项。

如果您要使用示例项目手动构建 SDK,请按照以下部分中的步骤操作。

使用示例项目手动构建 SDK

请按以下步骤操作:

  1. 下载所有框架,包括 UJETKit.xcframework 和其他依赖项。

  2. 在项目根目录下创建 CCAI Platform 文件夹,并解压所有框架。

  3. 选择 Objc-Manual 或 Swift-Manual 目标并进行构建。

导入框架

以下部分提供了有关如何导入框架的说明。

Objective-C 项目

@import UJETKit;

Swift 项目

swiftimport
UJETimport UJETKit

初始化 SDK

使用 UJET_COMPANY_KEY 和实例的 API 网址 https://YOUR_TENANT_HOST/api/v2 初始化 CCAI Platform。https:// 和 /api/v2 各包含一次。

[UJET initialize:UJET_COMPANY_KEY
         baseUrl:@"https://YOUR_TENANT_HOST/api/v2"
        delegate:self];

您可以将日志级别从 verbose 更改为 error。默认日志级别为 UjetLogLevelInfo。

[UJET setLogLevel:UjetLogLevelVerbose];

最终用户身份验证

通过 iOS 应用访问 iOS SDK。

为了确保最终用户通过身份验证,我们引入了 JWT 签名机制。

当需要进行身份验证时,iOS SDK 会要求对载荷进行签名。如果签名成功,应用会将已签名的 JWT 交换为最终用户身份验证令牌。必须在委托返回之前调用成功或失败块。

对于匿名用户(标识符 = nil),应用将为用户创建 UUID。如果用户稍后使用标识符进行身份验证,应用将尝试根据 UUID 合并这两个用户。

在示例项目的 UJETObject.h 中:

@import UJETKit;

@interface UJETObject : NSObject <UJETDelegate>

实现 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;
}

我们强烈建议您从应用服务器(而非客户端)对载荷进行签名。

此示例使用本地签名进行测试。请参阅 UJETObject.m 文件中的 signDataInRemote: success: failure:。

如需了解详情,请参阅 SDK 最终用户身份验证。

设置推送通知

应用会发送推送通知,以请求执行验证和拍照等智能操作,以及报告来电。该应用需要将两种不同类型的证书(VoIP 和 APN)保存在管理门户中。

准备 VoIP 服务证书

如需了解 Apple 的 VoIP 推送通知,请参阅相关参考文档。

  1. 在 Apple 开发者网站上创建并下载 VoIP 证书。

  2. 双击证书,将其添加到钥匙串中。

  3. 在 Mac 上启动“钥匙串访问”应用。

  4. 在左侧边栏中选择“我的证书”类别。

  5. 右键点击“VoIP Services: your.app.id”证书。

  6. 在随即显示的对话框中,选择导出。

  7. 将其保存为 cert.p12,不使用密码保护(将密码留空)。

  8. 在终端中运行以下命令。

    # Extract the certificate
    openssl pkcs12 -in cert.p12 -out voip_cert.pem -clcerts -nokeys
    # Extract the private key
    openssl pkcs12 -in cert.p12 -out voip_key.pem -nocerts -nodes
    # Combine into a single PEM file
    cat voip_cert.pem voip_key.pem > cert.pem
    
  9. cert.pem 的上半部分是证书,下半部分是私钥。

  10. 检查您的证书是否可与 Apple 的推送通知服务器搭配使用。如需详细了解如何测试推送通知的传送情况,请参阅测试推送通知。

  11. 使用管理员凭据登录 CCAI Platform 门户,然后依次前往设置 > 开发者设置 > 移动应用。

  12. 在“VoIP Services Certificate”(VoIP 服务证书)部分中填写证书,然后保存。 请务必包含证书和私钥的边界(-----BEGIN----- 和 -----END-----)。

  13. 如果您运行的应用具有开发预配配置文件(例如在 Xcode 中调试),请选中“沙盒”复选框。如果您的应用已针对临时或应用商店进行归档,并且使用的是分发预配配置文件,请清除“Sandbox”复选框。

准备 Apple 推送通知服务 SSL

此过程与 VOIP 服务证书的过程类似。在这种情况下,系统会使用 Apple 推送通知服务 SSL(沙盒和生产环境)证书。如需获取有关如何创建证书的指导,请参阅 Apple 远程通知服务器文档。

集成推送通知

在 AppDelegate.m 中:

@import PushKit;

@interface AppDelegate() <PKPushRegistryDelegate>
In application:didFinishLaunchingWithOptions: method:
// Initialize CCAI Platform
[UJET initialize:UJET_COMPANY_KEY
         baseUrl:@"https://YOUR_TENANT_HOST/api/v2"
        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];

在实现 UIApplicationDelegate 协议的文件中添加以下委托方法:

输出设备令牌,以测试推送通知。

// 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"]) {
    [UJET receivedNotification:payload.dictionaryPayload completion:completion];
  } else {
    completion();
  }
}

// UIApplicationDelegate

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
  [UJET updatePushToken: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];
    }
}

启用推送通知

  1. 选择您的目标,然后打开 Capabilities 标签页。

  2. 开启推送通知开关。

测试推送通知

以下部分提供了有关如何测试推送通知的指南。

推送通知调试部分

在管理员门户中,依次前往“设置”>“开发者设置”。在此页面上,找到标题为推送通知调试的部分:

将设备令牌复制并粘贴到右侧文本区域,然后选择正确的移动应用。

获取设备令牌

设备令牌字符串示例如下:

7db0bc0044c8a203ed87cdab86a597a2c43bf16d82dae70e8d560e88253364b7

推送通知通常在符合 UIApplicationDelegate 或 PKPushRegistryDelegate 协议的类中设置。在某个时间点,您会获得设备令牌。您可以在将该令牌传递给 iOS SDK 之前将其打印出来。如需获取设备令牌,请使用代码段。

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

结果

输入证书 PEM 文件和设备令牌后,点击该按钮。

如果测试推送通知已成功发送,结果将显示消息已成功配置推送通知。

推送通知不一定能送达,具体取决于设备的网络连接。

项目配置

以下各部分概述了配置项目所需的更改。

功能

在目标设置中,开启以下功能:

  • 推送通知

  • 后台模式(勾选这些项)

  • 音频和 AirPlay

  • Voice over IP

Info.plist

为保护用户隐私,在 iOS 10.0 或更高版本上关联的任何 iOS 应用,如果需要访问设备的麦克风、照片库和相机,都必须声明其意图。请在应用的 Info.plist 文件中添加以下键和字符串值,并为此键提供用途字符串。如果您的应用尝试访问设备的任何麦克风、照片库和相机,但没有相应的用途字符串,则应用会退出。

  • NSMicrophoneUsageDescription:允许访问麦克风,以便与支持团队或问题排查团队通话和交谈,以及发送与产品咨询相关的有声视频。

  • NSCameraUsageDescription:允许使用相机,以便客户拍摄和发送与其客户支持咨询相关的照片。

  • NSPhotoLibraryUsageDescription:允许客户发送与其客户支持咨询相关的照片。

  • NSFaceIDUsageDescription:允许使用面容 ID 进行验证。

启动 iOS SDK

在您要启动 iOS SDK 的位置添加以下代码行:

[UJET startWithOptions:nil];

您还可以使用此键,通过直接访问点从菜单中的特定点启动 iOS SDK:

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

您可以通过创建直接访问点 (DAP) 来创建 menuKey。以下步骤说明了如何创建 DAP:

  1. 使用管理员凭证登录 CCAI Platform 门户。

  2. 前往设置 > 队列。

  3. 从菜单结构中选择任意队列。

  4. 选择创建直接访问点

  5. 在文本表单中输入密钥。

  6. 点击保存。

通过预提取菜单更快地发起对话

从 iOS SDK 2.17.0 开始,您可以缩短开始对话所需的时间。您的应用会在消费者请求帮助之前预提取直接访问点密钥的菜单,然后从预提取返回的菜单 ID 启动聊天。仅更新 SDK 不会启用此行为。您的应用必须调用本部分中所述的方法。

请按以下顺序操作:

  1. 在可能需要聊天时调用 [UJET prefetchMenuWithKey:languageCode:completion:],例如在支持界面的 viewDidAppear: 中。在 [UJET initialize:...] 之后,在主线程上调用它。

  2. 存储补全返回的 UJETPrefetchedMenu。完成块在主队列中运行一次。

  3. 当消费者发起聊天时,请检查存储的结果是否不为 nil,以及聊天是否为其可用渠道之一。

  4. 如果同时满足这两个条件,请使用 initWithMenuId:preferredChannel:languageCode: 创建启动选项,并将聊天作为首选渠道。否则,请使用 initWithMenuKey: 创建它们,并将 preferredChannel 设置为 chat。

必须提供语言代码。如果没有,SDK 将不会预提取,并且完成时会返回 nil。将结果的 menuId 和 languageCode 原封不动地传递给启动选项。您传递的语言将成为聊天会话的语言,用于其界面和请求,而不是首选语言中描述的顺序。传递消费者应看到的语言,例如您在 preferredLanguage 中设置的值。

@interface SupportViewController ()
@property (nonatomic, strong, nullable) UJETPrefetchedMenu *prefetchedMenu;
@property (nonatomic, assign) NSUInteger prefetchGeneration;
@end

@implementation SupportViewController

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    self.prefetchGeneration += 1;
    NSUInteger generation = self.prefetchGeneration;
    __weak typeof(self) weakSelf = self;
    // Pass the language the consumer should see in the chat.
    [UJET prefetchMenuWithKey:@"MENU_KEY" languageCode:@"en" completion:^(UJETPrefetchedMenu *menu) {
        if (weakSelf.prefetchGeneration == generation) {
            weakSelf.prefetchedMenu = menu;
        }
    }];
}

- (void)startChat {
    UJETPrefetchedMenu *menu = self.prefetchedMenu;
    UJETStartOptions *options;

    if (menu != nil && [menu isChannelAvailable:UjetPreferredChannelTypeChat]) {
        options = [[UJETStartOptions alloc] initWithMenuId:menu.menuId
                                          preferredChannel:UjetPreferredChannelTypeChat
                                              languageCode:menu.languageCode];
    } else {
        options = [[UJETStartOptions alloc] initWithMenuKey:@"MENU_KEY"];
        options.preferredChannel = UjetPreferredChannelTypeChat;
    }

    [UJET startWithOptions:options];
}

// Call this as soon as your app signs out or changes the user.
- (void)discardPrefetchedMenu {
    self.prefetchGeneration += 1;
    self.prefetchedMenu = nil;
}

// Call this only when the SDK isn't running, for example after
// UJETEventSdkDidTerminate. Don't prefetch or start the SDK for the
// new user until this has run.
// See Clear UJET cache from local if user data has been updated.
- (void)clearSDKUserData {
    [UJET clearUserData];
}

@end

如果您设置了 unsignedCustomData 或 ticketId,请在您开始使用的任何选项上设置它们。

仅在预提取结果成功时,才从菜单 ID 开始。如果没有,SDK 会启动渠道,而不会检查该渠道是否已启用、是否已偏转或是否受会话前智能操作的约束。

如果键未解析为没有子菜单且没有重定向的单个菜单、请求或身份验证失败,或者预提取未在 30 秒内完成,则补全会返回 nil。结果也可能不包含任何可用渠道。只有在为该菜单启用 Chat、其模块包含在您的应用中、Chat 未被转移且未应用会话前智能操作时,Chat 才会显示在 availableChannels 中。

仅在应用知道最终用户是谁后才进行预提取。如果未缓存任何身份验证令牌,预提取可以使用 UjetPayloadAuthToken 调用 signPayload:payloadType:success:failure: 委托方法,并且令牌会使用应用当时具有的身份进行签名。预提取期间的网络和身份验证失败也可能达到 didHandleUjetError:。

SDK 会从预提取中获取可用渠道和偏转状态,并且在聊天开始时不会再次检查。如果预提取后 CCAI Platform 门户中的营业时间、偏转或渠道设置发生更改,则存储的结果不会反映更改。每次支持屏幕出现时,再次预提取。该示例会保留上一个结果,直到较新的预提取完成,并且其计数器可确保只有最新的预提取会更新结果。

[UJET clearUserData] 会丢弃 SDK 保留的结果。同时,丢弃已存储的结果,并在下次对话前重新预提取。在 clearUserData 之前开始的预提取会以 nil 完成,计数器会使示例忽略它。如果您的应用在支持界面之外清除用户数据,请将计数器保留在两个位置都可以访问的位置。

如果用户数据已更新,则从本地清除缓存

我们会在钥匙串中缓存身份验证令牌,以便重复使用,并减少从宿主应用对载荷进行签名的请求频率。SDK 将一直使用该令牌,直到其过期或通过 clearUserData 调用撤消。每当用户相关数据发生更改或更新(例如退出事件)时,宿主应用都会负责撤消此缓存。

[UJET clearUserData];

请勿在通话或聊天期间清除用户数据。清除操作会移除缓存的身份验证令牌,并可能会中断会话,包括阻止通话连接。

仅在 SDK 未运行时调用 clearUserData。如需了解 SDK 何时停止,请观察 UJETEventSdkDidTerminate。请在启动 SDK 之前添加观察器,以免应用错过该事件。

当最终用户退出或用户发生更改时,请执行以下步骤:

  1. 舍弃应用存储的预提取菜单结果。在完成第 3 步之前,请勿为新用户预提取或启动 SDK。

  2. 如果您的应用启动了 SDK,但此后未收到 UJETEventSdkDidTerminate,请调用 [UJET disconnect:nil]。然后等待 UJETEventSdkDidTerminate,而不是 disconnect: 完成处理程序。

  3. 调用 [UJET clearUserData]。

  4. 为新用户预提取菜单或启动 SDK。

请勿使用 getStatus 或 UJETEventSessionDidEnd 来决定何时清除。 对于尚未接通的通话,getStatus 会返回 UjetStatusNone。并非每个在接通前结束的通话都会发布 UJETEventSessionDidEnd,并且在它之后可能会显示调查问卷或 CSAT 屏幕。

disconnect:无法结束尚未接通的通话。在这种情况下,请等到调用结束且 SDK 发布 UJETEventSdkDidTerminate。 会话后屏幕(例如调查问卷或 CSAT)可能会延迟此事件。SDK 会在移除其通话和聊天状态后发布 UJETEventSdkDidTerminate,因此在此事件之后进行清除是安全的。disconnect: 的完成处理程序不会显示 SDK 已停止:当 disconnect: 结束未最小化的聊天时,不会调用该处理程序。第 1 步适用于通过预提取菜单更快地发起对话中的预提取结果,该结果属于前一个用户。

在启动 Contact Center AI 平台之前检查是否存在现有会话

在开始会话之前,请检查当前是否没有会话。当 userId 发生更改时,这一点尤为重要。

[UJET getStatus];

如果存在现有会话,我们应提示用户恢复会话或取消操作:

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

自定义 SDK 主题

UJETGlobalTheme.h 中列出了 SDK 主题的多个选项。

在 [UJET initialize] 之后设置主题,例如:

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

公司名称是从管理门户 > 设置 > 支持中心详细信息 > 显示名称中检索到的。

您可以设置徽标图片,而不是公司名称,如下所示:

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

如果图片过大,系统会调整其大小,使其适合该区域。

字符串

您还可以通过替换值来自定义字符串。例如,在 Localizable.strings 中添加以下键值对:

"ujet_greeting_title" = "Title";

"ujet_greeting_description" = "Description";

可自定义的字符串列在 ujet.strings 文件中。

深色模式

您可以为深色模式指定所需的颜色色调,以提高字体的可读性。

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

如果您未设置该属性,则 UJETGlobalTheme.tintColor 将用于深色模式。如果您的应用支持深色模式,我们建议您设置此属性。 请参阅以下 Apple 文章,了解如何为深色模式选择合适的色调颜色:

Chat 主题

如需自定义聊天界面,您可以选择使用 JSON 字符串或每个主题类。

如需参考,请查看示例应用并取消注释 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)
}

内容卡片主题

您可以同时为内容卡片和对话添加自定义设置。 您可以使用 JSON 文件(请参阅 content_card 属性)或 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)
}

表单卡片主题

您可以同时为表单卡片和聊天添加自定义设置。为此,您可以使用 JSON 文件(请参阅 form_card property)或 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)
}

网页表单配置

如需配置 Web 表单功能,请实现 UJETDelegate 协议的 ujetWebFormDidReceive 方法。此方法接收一个事件(一个 FormMessageReceivedEvent 字典)作为参数,其中包含与表单相关的信息。事件 (FormMessageReceivedEvent) 字典包含以下 JSON 结构:

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

如需处理该事件,请执行以下操作:

  1. 从事件字典(smart_action_id、external_form_id 和 signature)中提取相关信息。

  2. 为表单数据生成表单 URI 和签名。

  3. 使用 completion closure 将表单数据作为 FormDataEvent 字典传递给 SDK。

  4. 如果在 URI/签名生成期间发生任何错误,请使用 callback.onError() 和 Error 调用回调。

传递给 SDK 的字典 (FormDataEvent) 应具有以下结构:

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

必须使用 data 生成签名 (HMAC-SHA:256),并使用共享密钥进行签名。在生成签名之前,必须按字母顺序对数据的对象键进行排序,并且应将相同的 data 发送到 SDK。

设置完成后的数据转移

您可以添加会话后 VA 的自定义设置以及聊天自定义设置。 这可以通过使用 JSON 文件(请参阅 post_session 属性)或使用 UJETChatPostSessionVaTheme 类来实现。边框宽度只能为 0 或 1,如果您不想区分会话后的 VA 体验,可以将 containerColor 设置为白色,并将边框设置为 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)
}

Chat 操作菜单

您可以将聊天操作菜单的自定义设置与聊天自定义设置一起添加。这可以通过使用 JSON 文件(请参阅 form_card 属性)或使用 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)
}

其他外观

您可以自定义其他外观,例如字体大小和背景颜色。

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

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

CallKit

在 iOS 10.0 及更高版本中,系统会为所有通话启用 CallKit。

借助 CallKit,它会显示应用内来电,并显示通话界面,还会在手机的通话记录中显示该通话。

如需从通话记录中发起新的 CCAI Platform 支持会话,请将以下代码块添加到 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 允许在锁定的屏幕上显示一个 40x40 的图标。在 Xcassets 中放置一个名为“icon-call-kit”的图片。

配置 SDK

您可以在启动 SDK 之前设置多个选项。

如需了解详情,请查看 UJETGlobalOptions 类。

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

[UJET setGlobalOptions:options];

显示或隐藏“下载转录内容”按钮

您可以配置 SDK,以在对话选项菜单和对话后界面中显示或隐藏下载转写内容按钮。

以下代码展示了如何配置下载转写文稿按钮:

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

@property (nonatomic, assign) UJETChatDownloadTranscriptVisibilityOptions transcriptVisibilityOptions;

PSTN 后备

我们在以下几种情况下提供 PSTN 回退:

  • 移动网络处于离线状态。

  • 无法访问应用后端。

  • 无法使用 VoIP

    • 网络状况不佳,无法连接。如需了解详情,请参阅 UJETGlobalOptions.pstnFallbackSensitivity 属性。

    • 由于防火墙配置或提供商问题,连接期间发生故障。

我们建议您在 UJETGlobalOptions.fallbackPhoneNumber 中设置公司的 IVR 号码。建议的格式为 + 后跟国家/地区代码和手机号码。例如:+18001112222。

PSTN 备用敏感度

您可以调整检查网络状况以回退到 PSTN 的灵敏度级别。

@property (nonatomic, assign) float pstnFallbackSensitivity;

该值必须在 0.0 到 1.0 之间。如果设置为 1,则通话将始终通过 PSTN 而非 VoIP 连接。如果值为 0,则最大延迟时间和最小带宽阈值分别为 10000 毫秒和 10KB/秒。例如,值为 0.5 表示最小延迟时间和带宽分别为 5000 毫秒和 15KB/秒。

您可以按照以下步骤配置此值:

  1. 以管理员身份登录 CCAI Platform 门户。

  2. 依次前往设置 > 开发者设置 > 移动应用。

  3. 找到备用手机号码阈值部分。默认值为 0.85。

  4. 指定新的阈值。

  5. 点击保存。

在全局级别关闭推送通知

您可以在全局级别关闭推送通知。将以下属性设置为 false 会绕过所有推送通知依赖项,并阻止推送通知到达最终用户:

@property (nonatomic, assign) BOOL allowsPushNotifications;

忽略深色模式

您可以使用以下属性专门忽略 CCAI Platform SDK 中的深色模式:

@property (nonatomic, assign) BOOL ignoreDarkMode;

隐藏状态栏

您可以使用以下属性控制状态栏的可见性:

  @property (nonatomic, assign) BOOL hideStatusBar;

默认情况下,hideStatusBar 设置为 false 和 visible。

跳过满意度调查问卷

您可以添加一个按钮,让用户跳过 CSAT 调查问卷。以下代码示例展示了如何添加该按钮:

let options = UJETGlobalOptions()
options.skipCsat = true

非营业时间聊天签到选项

如果您的聊天队列使用聊天签到功能,您可以控制非工作时间聊天签到屏幕上显示的内容。这两个选项都是 UJETGlobalOptions 的属性。

// YES to hide the "Next availability" message on the after-hours
// chat check-in screen. Default is NO.
@property (nonatomic, assign) BOOL hideNextAvailability;

// YES to hide the "Start over" button on the after-hours chat
// check-in screen. Default is NO.
@property (nonatomic, assign) BOOL hideStartOver;

自定义活动指示器

您可以将自己的加载器动画(在 UIView 内)添加到 SDK,并替换默认的 UIActivityIndicatorView。实现 UJETDelegate 中的 ujet_activityIndicator 方法,并返回自定义视图。

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
}

如果您已在应用的 Info.plist 中将 UIUserInterfaceStyle 设置为 Light,以完全停用深色模式,则可以忽略此属性。

首选语言

CCAI Platform SDK 将使用以下优先级顺序来确定首选语言。

  1. 在应用内的启动画面中选择的语言。

  2. 默认语言选自UJETGlobalOptions。您可以使用 preferredLanguage 属性设置默认语言。您可以在 UJETGlobalOptions.h 文件中找到支持的语言代码。

  3. 如果应用支持在设备中选择的设备语言(依次点击设置 > 常规 > 语言和地区),则会使用该语言。

  4. 如果应用不支持设备语言,但支持其最接近的父级方言,则会使用设备语言的最接近方言。例如,如果用户在设备中选择古巴西班牙语作为语言,而应用不支持古巴西班牙语,但支持父级方言西班牙语,则系统会使用西班牙语。

  5. 如果应用不支持设备语言,则会使用英语。

配置外部偏转链接图标

您可以自定义外部偏转链接渠道中的图标,方法是将图标上传到应用的资源目录中,并确保在管理门户的“设置”>“Chat”>“外部偏转链接”>“查看链接”>“添加偏转链接”中创建外部偏转链接时使用相同的图标名称。如果管理门户中的图标名称与上传到应用中的图标不匹配,则 SDK 将使用默认图标。您可以参阅此链接,了解如何在资产目录中添加图片。

向 CRM 发送自定义数据

您可以向 CRM 工单发送自定义数据。

您可以通过以下两种方法发送自定义数据:

  1. 安全方法:使用 JWT 进行预定义数据签名。

  2. 不安全的方法:使用纯 JSON 的预定义数据(不推荐)。

使用安全的方法发送自定义数据

您必须实现签名方法。首先,您可以将自定义数据放在客户端,然后发送到服务器进行签名。在服务器上,您可以通过定义的表单添加其他数据,并使用 company.secret 进行签名,然后通过 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];
    }
}

使用不安全的方法发送自定义数据

不建议使用此方法,因为它会造成潜在漏洞,使您的应用容易受到中间人攻击。如果您选择使用此方法,我们对可能发生的安全风险和潜在损害不承担任何责任。我们建议您使用前面介绍的安全方法在应用中发送自定义数据。或者,您也可以直接使用 UJETCustomData 实例启动 iOS SDK。在这种情况下,UJETPayloadCustomData 的 signPayload 委托应仅调用 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];

使用未签名的自定义数据发送外部聊天记录

当 SDK 启动时,如果聊天记录包含未签名的自定义数据,您可以通过调用 setExternalChatTransfer: 或 setExternalChatTransferWithDictionary: 方法,分别使用 NSString 或 NSDictionary 设置 JSON 数据,从而将聊天记录发送到 SDK。

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

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

[UJET startWithOptions:options];

JSON 格式:

  • greeting_override:字符串

  • agent:字典

    • name:字符串

    • avatar:字符串 [代理头像的网址,可选]

  • transcript:数组

    • sender:字符串 [end_user 或 agent]

    • timestamp:字符串 [例如 2021-03-15 12:00:00Z]

    • content:数组

      • type:字符串 [text、media 之一]

      • text:字符串 [文本类型必需]

      • media:字典 [媒体类型必需]

        • type:字符串 [image、video 之一]

        • url:字符串 [指向媒体文件的公开网址]

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

您可以在文本类型中使用 Markdown。支持的语法包括斜体、粗体、项目符号列表、超链接和下划线 (--text--)。

自定义数据示例

JSON 编码为 JWT:

JSON 文件应验证 JWT。自定义数据的对象是 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"
    }
  }
}

每个数据都类似于 JSON 对象格式,应包含键、值、类型和标签。

键是数据的唯一标识符。标签是 CRM 页面上的显示名称。类型是值的类型。

  • 字符串

    • JSON 字符串
  • 数值

    • integer, float
  • 日期

    • UTC Unix 时间戳格式,13 位数字。(包含毫秒)
  • 网址

    • HTTP 网址格式

CRM 示例

以下是 CRM 中自定义数据的显示方式示例。

位置:

使用 CoreLocation 框架。如需了解详情,请参阅 AppDelegate.m。

设备操作系统版本:

[customData set:@"os_version" label:@"OS Version" stringValue:[[UIDevice currentDevice] systemVersion]];

阻止显示客户数据

您可以将 invisible_to_agent 属性与自定义数据对象搭配使用,以防止在代理适配器中显示已签名或未签名的自定义数据。在前面的示例中,最终用户的社会保障号不会显示在代理适配器中,因为 "invisible_to_agent" : true 包含在 ssn 对象中。

如果您在自定义数据对象中添加 "invisible_to_agent" : true 属性,则可能会遇到以下行为:

如需了解详情,请参阅在代理适配器中查看会话数据。

预留数据属性

您可以在会话开始时将预留数据属性作为已签名的自定义数据发送到 Contact Center AI 平台 (CCAI Platform)。如需了解详情,请参阅发送预留数据属性。

以下是自定义数据中预留数据属性的示例:

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

将每个值设置为 true 或 false。

  • reserved_verified_customer:如果您认为此最终用户是合法客户,则为 true。
  • reserved_bad_actor:如果您认为此最终用户可能是恶意行为者,则为 true。
  • reserved_repeat_customer:如果您已确定此最终用户之前曾联系过您的联络中心,则为 true。

自定义流程

断开 CCAI Platform 与处理宿主应用事件的连接:

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

推迟 CCAI 平台来电或聊天

实现用于处理传入事件的委托方法:

- (BOOL)shouldConnectUjetIncoming:(NSString *)identifier forType:(UjetIncomingType)type {
  if (weDoingSomething) {
    // save identifier and type
    return NO; // postpone
  } else {
    return YES;
  }
}

连接已延期的活动

[UJET connect:identifier forType:UjetIncomingTypeCall];

设置深层链接

这样一来,无论最终用户是否安装了应用,客服人员都可以在 PSTN 通话期间通过短信使用智能操作。

在 CCAI Platform 门户中,依次前往设置 > 操作管理 > 启用“发送短信以下载应用”。

配置通用链接或自定义网址协议后,您可以使用网页(例如 https://your-company.com/support)设置应用网址。您可以选择任一方式。

实现委托方法以处理深层链接

通用链接和自定义网址分别类似于 https://your-company.com/support?call_id=x&nonce=y 和 your-company://support?call_id=x&nonce=y。在管理门户中的“应用网址”下,输入一个不含查询参数的链接。例如,如果使用自定义网址协议,请输入 your-company://support。

在委托方法中,请确保仅当通用链接或自定义网址中的网址路径和参数特定于 CCAI Platform 时,才调用 [UJET start]。

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

如果您的应用采用 UIWindowSceneDelegate,请添加以下代码段:

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: [:])
    }
}

如需了解详情,请参阅 UJETObject+DeepLink 文件中的示例代码。

观察 CCAI Platform 事件

我们会通过 NSNotificationCenter.defaultCenter 发布以下事件。您可以监听这些事件,并根据您的用例(例如自定义键盘布局)自定义流程。

  • UJETEventEmailDidClick

    • 队列菜单数据
  • UJETEventEmailDidSubmit

    • 队列菜单数据

    • has_attachment: (NSNumber) @YES, @NO

  • UJETEventSessionViewDidAppear

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

    • timestamp: (NSString) ISO 8601

  • UJETEventSessionViewDidDisappear

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

    • timestamp: (NSString) ISO 8601

  • UJETEventSessionDidCreate

    • 会话数据
  • UJETEventContentCardDidClick、UJETEventContentCardButtonDidClick、UJETEventQuickReplyLinkDidClick

    • 会话数据

    • event_params: (NSDictionary) nullable

    • link:(NSString) 网址或深层链接,可为 null

  • UJETEventSessionDidEnd

    • 会话数据

    • agent_name:(NSString) 如果客服人员未加入,则为 null

    • duration: (NSNumber) 仅限通话

    • ended_by: (NSString)

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

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

  • UJETEventSdkDidTerminate

  • UJETEventPostSessionOptInDidSelected

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

事件数据

元数据

  • application: @"iOS"

  • app_id:(NSString) 软件包标识符

  • app_version: (NSString)

  • company: (NSString) subdomain

  • device_model: (NSString)

  • device_version: (NSString)

  • sdk_version: (NSString)

  • timestamp: (NSString) ISO 8601

队列菜单数据

元数据

  • menu_id: NSString

  • menu_key: NSString, nullable

  • menu_name: NSString

  • menu_path : NSString

会话数据

元数据

  • session_id: NSString

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

  • end_user_identifier: NSString

设置屏幕共享

如果您想使用屏幕共享功能,请集成 UJETCobrowseKit.xcframework。

CocoaPods:将以下子规范添加到应用目标。

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

Carthage:在 Cartfile 中添加以下行:

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

SwiftPM:选择 UJET 和 UJETCobrowse 产品,然后添加到您的应用目标。

并设置 UJETGlobalOptions.cobrowseKey 属性。

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

UJET.setGlobalOptions(options)

完整设备屏幕共享(可选)

通过完整设备屏幕共享,支持人员可以查看您自己的应用之外的应用屏幕。当支持人员需要检查系统设置的状态,或需要查看用户在多个应用之间导航时,此功能通常很有用。如果您不需要此功能,可以跳过此部分。

如需自定义屏幕共享意见征求对话框,您需要在提供程序类中实现 UJETCobrowseAlertProvider 协议。在此实现中,通过相应的协议方法返回自定义 UIViewController 或继承自 UIViewController 的任何其他对象。UIViewController 应包含两个按钮,一个用于接受,另一个用于拒绝。

征得用户同意后,通过调用闭包 consentStatus 将其传递给我们的 SDK。来自cobrowseFullDeviceRequestAlert的UIViewController。委托应包含带有标题的 RPSystemBroadcastPickerView(请参阅下面的示例代码),并且应具有另一个拒绝按钮。在点击拒绝按钮时调用已关闭的闭包。

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

自定义视图控制器应具有一个闭包,用于将用户同意情况传递给 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)
        }
    }
}

用于显示完整设备请求提醒的自定义视图控制器应具有 RPSystemBroadcastPickerView 和一个闭包,以便将 dismiss 状态传递给 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?()
        }
    }
}

请务必通过以下 API 将此提供程序传递给我们的 SDK:

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

广播扩展程序

此功能需要添加广播扩展程序。

  1. 打开您的 Xcode 项目。

  2. 依次前往 File(文件)> Target(目标)。

  3. 选择 Broadcast Upload Extension。

  4. 输入目标的名称。

  5. 清除包含界面扩展程序复选框。

  6. 创建目标,并记下其软件包 ID。

  7. 将广播扩展程序的目标 SDK 更改为 iOS 12.0 或更高版本。

集成 SDK

CocoaPods:将以下子规范添加到扩展程序目标:

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

如果您使用的是 SwiftPM,请选择 UJETCobrowseExtension 产品并将其添加到扩展程序目标。

设置钥匙串共享

您的应用和您之前创建的应用扩展程序需要通过 iOS 密钥链共享一些密钥。它们使用自己的钥匙串群组来执行此操作,因此它们与应用钥匙串的其余部分隔离开来。

在应用目标和扩展程序目标中,为 io.cobrowse 钥匙串群组添加钥匙串共享授权。

将软件包 ID 添加到 plist

获取您之前创建的扩展程序的软件包 ID,并在应用的 Info.plist(注意:不是扩展程序的 Info.plist)中添加以下条目,并将以下软件包 ID 替换为您自己的软件包 ID:

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

实现扩展

Xcode 会将 SampleHandler.m 和 SampleHandler.h(或 SampleHander.swift)文件添加到您之前创建的目标中。将文件的内容替换为以下内容:

Swift:选择 UJETCobrowseExtension 产品并将其添加到您的扩展程序目标:

import CobrowseIOAppExtension
class SampleHandler: CobrowseIOReplayKitExtension {
}

ObjC:

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

构建并运行您的应用

现在,您可以构建并运行应用了。完整的设备功能仅在实体设备上可用,在 iOS 模拟器中无法使用。

最小化 SDK

在聊天会话或通话进行期间,Contact Center AI 平台 SDK 可以最小化。当您希望在收到 SDK 事件(例如内容卡片点击)后将用户引导回您的应用时,这会非常有用。如需最小化 SDK 并引导用户返回您的应用,您可以使用:

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
}

后备

您可以使用 didHandleUjetError 函数来回退意外错误。如果您不使用此函数或此函数返回 false,则 iOS SDK 会处理错误。

下表显示了 didHandleUjetError 函数监听的错误:

错误类型 错误代码 说明
networkError 1 网络不可用。

注意:在聊天或通话会话期间或在评分界面上,如果网络不可用,系统不会触发此错误。

authenticationError 100 身份验证期间发生意外错误。
authenticationJwtError 101 JWT 验证期间发生意外错误,例如解析错误。
voipConnectionError 1000 未能与 VoIP 提供商建立连接。VoIP SDK 回调会处理此问题。
voipLibraryNotFound 1001 系统预期通过 VoIP 提供商连接通话,但找不到提供商。如果您集成了错误的 SDK 或未将 VoIP 提供商库添加到依赖项中,就可能会发生这种情况。
chatLibraryNotFound 1100 当系统找不到聊天库时,就会发生此错误。如果您集成了错误的 SDK 或未将 Twilio 聊天库添加到依赖项中,就可能会发生这种情况。

以下代码示例展示了如何使用 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
    }
}

问题排查

本部分介绍了常见问题及其解决方法。

应用提交遭拒

提交的应用因在中国大陆境内包含 CallKit 框架而被拒。

如果您的应用因这个原因被 Apple 拒绝,只需留下评论即可,因为系统设计为在 VoIP 通话时停用中国大陆区域的 CallKit 框架。此变更自 SDK 0.31.1 版起生效。

SDK 过大

SDK 规模过大,难以在 GitHub 上跟踪

在这篇文章中,他们提供了两种选择。建议使用 Git lfs。

如果您不使用 Bitcode,则可以考虑从二进制文件中剥离 Bitcode。在 UJETKit.xcframework 文件夹下运行此命令。

xcrun bitcode_strip -r UJET -o UJET

dyld:库未加载错误

在 Runpath Search Paths(Runpath 搜索路径)中添加 @executable_path/Frameworks,具体操作路径为:Target > Build Settings > Linking。

在 iTunes Connect 上提交应用

由于启用了 Voice over IP 后台模式,Apple 可能会在审核过程中提出以下问题:

用户可以在您的应用中接听 VoIP 来电吗?

对问题回答是。

启动 SDK 时无法发送提醒通知

请检查以下各项:

  • 使用真实设备,而非模拟器。

  • 启用推送通知和后台模式 > Voice over IP 功能。

如果这些方法不起作用,请尝试使用分发预配配置文件(Ad-hoc 或 Apple Store)进行构建。

针对测试应用测试推送通知

准备好 VoIP 证书和设备的设备令牌。

在 CCAI Platform 门户上,参阅设置 > 开发者设置菜单中的推送通知调试部分。

如果您已为 APNS 设置证书,则无需再次放置证书。

输入您的证书(可选),检查是否为沙盒(可选),然后输入测试应用的推送通知设备令牌。

发起新对话用时超过 30 秒

检查您是否在响应自定义数据的委托方法。您应在收到请求时返回有效的自定义数据,或在成功块中返回 nil。

使用以下代码段作为示例配置:

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