iOS SDK ガイド

Apple iOS オペレーティング システム用のコンタクト センター AI プラットフォーム(CCAI プラットフォーム)モバイル SDK を使用すると、CCAI プラットフォームのモバイル エクスペリエンスを iOS モバイルアプリに埋め込むことができます。

要件

iOS モバイル SDK には次の要件があります。

  • iOS 12.0 以降

会社の認証情報を取得する

  1. 管理者の認証情報を使用して、コンタクト センター AI プラットフォーム(CCAI プラットフォーム)ポータルにログインします。

  2. [設定] > [開発者向け設定] に移動します。

  3. [Company Key] と [Secret Code] で、会社キーと会社シークレット コードをメモします。

ご利用にあたって

以下では、CCAI Platform iOS モバイル SDK の使用を開始する方法について説明します。

インストール

まず、iOS SDK をインストールする必要があります。

サンプルアプリをダウンロードする

  1. iOS サンプルアプリをダウンロードします。

  2. フォルダに移動し、CocoaPods を使用して依存関係をインストールします。

    $ pod install --project-directory=ExampleApp
    
  3. プロジェクト設定をすばやく構成するには、シェル スクリプトを実行します。

    $ ./setup.sh
    

    または、次の手順でプロジェクト設定を手動で編集することもできます。

    1. ExampleApp.xcworkspace を開きます。

    2. Info.plist の UJETCompanyKey と UJETCompanySecret の値を、CCAI Platform ポータルの [Settings] > [Developer Settings] ページの [Company Key] と [Company Secret Code] の値に置き換えます。

    3. Info.plist の UJETSubdomain 値を、CCAI Platform ポータルの URL のサブドメインに置き換えます。サブドメインは、URL の .ujet.com の直前にあります(例: https://your-subdomain.ujet.com/settings/developer-setting の your-subdomain)。

プロジェクトに統合する

iOS SDK をアプリに統合する方法は、開発環境によって異なります。

Swift Package Manager

  1. GitHub から iOS SDK 用の Swift パッケージを追加します。

  2. ビルド設定で、[Other Linker Flags] に「-ObjC」を追加します。

  3. Xcode 13.2 の時点では、Swift Package Manager を使用して配布されたバイナリ フレームワークの使用に関する既知の問題があります。この問題の現在の回避策は、Xcode プロジェクトのビルドフェーズに実行スクリプト フェーズを追加することです。この Run Script Phase は、Embed Frameworks ビルドフェーズの後に行う必要があります。この新しい 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

CCAI Platform の依存関係は Carthage をサポートしていないため、Google Cloud では依存関係マネージャーまたは手動統合を使用することをおすすめします。

別の依存関係マネージャーを使用する手順は次のとおりです。

  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、UJETTwilioCallKit.xcframework、およびすべての依存関係 TwilioVoice.xcframework と TwilioConversationsClient.xcframework をダウンロードします。

  2. UJETKit.xcframework を [Frameworks, Libraries, and Embedded Content] セクションにドラッグして、ターゲットに追加します。

  3. ステップ 1 のすべての依存関係について、ステップ 2 と 3 を繰り返します。

  4. [Build Settings] で、-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 と UJET_SUBDOMAIN を使用して CCAI Platform を初期化します。

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

ログレベルを verbose から error に変更できます。デフォルトのログレベルは UjetLogLevelInfo です。

[UJET.setLogLevel:UjetLogLevelVerbose];

エンドユーザー認証

iOS アプリから iOS SDK にアクセスする。

エンドユーザーが認証されていることを確認するために、JWT 署名メカニズムを導入しています。

iOS SDK は、認証が必要な場合にペイロードに署名するよう求めます。署名が成功すると、アプリケーションは署名付き JWT をエンドユーザー認証トークンと交換します。成功または失敗のブロックは、デリゲートが戻る前に呼び出す必要があります。

匿名ユーザー(identifier = nil)の場合、アプリケーションはユーザーの UUID を作成します。後でユーザーが識別子で認証されると、アプリケーションは UUID に基づいて 2 人のユーザーを統合しようとします。

サンプル プロジェクトの 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 のエンドユーザー認証をご覧ください。

プッシュ通知を設定する

CCAI Platform は、確認や写真などのスマート アクションをリクエストするプッシュ通知を送信し、着信を報告します。チャットキューでチェックインを使用している場合(iOS SDK 2.16.0 以降が必要)、CCAI Platform は、待機中の消費者にチャットに戻り、準備ができていることを確認するよう促すプッシュ通知を送信します。Mobile SDK のチェックインをご覧ください。CCAI プラットフォームでは、2 種類の証明書(VoIP と APN)を CCAI プラットフォーム管理ポータルに保存する必要があります。

証明書を構成する

CCAI Platform では、CCAI Platform 管理ポータルに 2 種類の Apple プッシュ証明書を保存する必要があります。

  • Apple Push Notification サービス(APNs)の SSL 証明書 - 標準のリモート プッシュ通知に使用されます。

  • VoIP サービス証明書 - 着信通知と特定の通話関連のスマート アクションに使用されます。

どちらの証明書も Apple デベロッパー ポータルで作成され、PEM 形式に変換されてから、CCAI Platform 管理ポータルにアップロードされます。

APNs SSL 証明書を作成する

Apple の公式ガイド Establishing a Certificate-Based Connection to APNs に沿って、APNs SSL 証明書を作成します。

  • developer.apple.com にログインし、[Certificates, identifiers & profiles] に移動します。

  • [+] ボタンをクリックして、新しい証明書を作成します。

  • [Services] で [Apple プッシュ通知サービス SSL (Sandbox & production)] を選択します。

  • アプリのバンドル ID と一致するアプリ ID を選択し、[Continue] をクリックします。

  • Mac のキーチェーン アクセスから生成された証明書署名リクエスト(CSR)をアップロードします。

    • [キーチェーン アクセス] > [証明書アシスタント] > [認証局に証明書を要求] を開きます。

    • メールアドレスを入力し、[CA email address] を空のままにして、[Saved to disk] を選択し、[Continue] をクリックします。

  • [続行] をクリックし、生成された .cer ファイルをダウンロードします。

  • .cer ファイルをダブルクリックして、キーチェーンにインストールします。

  • [キーチェーン アクセス] で、インストールした証明書を見つけて右クリックし、[書き出す…] を選択して .p12 ファイルとして保存します。エクスポート パスワードを設定するよう求められます。

  • openssl を使用して、.p12 ファイルを PEM 形式に変換します。

# 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

VoIP サービス証明書を作成する

VoIP サービス証明書を使用すると、アプリは Apple の PushKit フレームワークを介して優先度の高いプッシュ通知を受信できます。これにより、アプリを終了状態から復帰させて着信を処理できます。最新の公式手順については、Apple Developer ドキュメントの VoIP サービス証明書を作成するをご覧ください。

  • developer.apple.com にログインし、[Certificates, Identifiers & Profiles] に移動します。

  • [+] ボタンをクリックして、新しい証明書を作成します。

  • [Services] で [VoIP services certificate] を選択します。

  • アプリのバンドル ID と一致するアプリ ID を選択し、[Continue] をクリックします。

  • 前のセクションで生成した証明書署名リクエスト(CSR)をアップロードします(または新しい CSR を作成します)。

  • [続行] をクリックし、生成された .cer ファイルをダウンロードします。

  • .cer ファイルをダブルクリックして、キーチェーンにインストールします。

  • [キーチェーン アクセス] で、インストールした VoIP サービス証明書を見つけて右クリックし、[書き出す…] を選択して .p12 ファイルとして保存します。

  • openssl を使用して、.p12 ファイルを PEM 形式に変換します。

# 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

CCAI Platform 管理ポータルに証明書をアップロードする

  • CCAI Platform 管理ポータルにログインします。

  • [設定] > [デベロッパー設定] > [モバイルアプリ] に移動します。

  • APNs PEM 証明書をアップロードします。

  • VoIP PEM 証明書をアップロードします。

プッシュ通知を有効にする

  1. ターゲットを選択し、[Capabilities] タブを開きます。

  2. [プッシュ通知] のスイッチをオンにします。

プッシュ通知をテストする

以降のセクションでは、プッシュ通知をテストする方法について説明します。

プッシュ通知のデバッグ セクション

管理者ポータルで、[Settings] > [Developer Settings] に移動します。このページで、[プッシュ通知のデバッグ] というセクションを見つけます。

デバイス トークンをコピーして右側のテキスト領域に貼り付け、適切なモバイルアプリを選択します。

デバイス トークンを取得する

デバイス トークン文字列の例を次に示します。

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 ファイルとデバイス トークンを入力したら、ボタンをクリックします。

テストのプッシュ通知が正常に配信されると、[Push notification successfully configured] というメッセージが表示されます。

プッシュ通知は、デバイスのネットワーク接続によっては配信が保証されません。

プロジェクト構成

以降のセクションでは、プロジェクトの構成に必要な変更の概要について説明します。

機能

ターゲット設定で、次の機能を有効にします。

  • プッシュ通知

  • バックグラウンド モード(以下の項目を確認)

  • オーディオと AirPlay

  • Voice over IP

Info.plist

ユーザーのプライバシーを保護するため、iOS 10.0 以降でリンクされ、デバイスのマイク、フォト ライブラリ、カメラにアクセスする iOS アプリは、その旨を宣言する必要があります。アプリの Info.plist ファイルに文字列値を含む次のキーを追加し、このキーの目的文字列を指定します。アプリが、対応する目的文字列なしでデバイスのマイク、フォト ライブラリ、カメラにアクセスしようとすると、アプリは終了します。

  • NSMicrophoneUsageDescription: サポートチームやトラブルシューティング チームとの通話や会話、製品に関するお問い合わせに関連する音声付き動画の送信のために、マイクへのアクセスを許可します。

  • NSCameraUsageDescription: カスタマー サポートのお問い合わせに関連する写真を撮影して送信できるように、カメラへのアクセスを許可します。

  • NSPhotoLibraryUsageDescription: カスタマー サポートのお問い合わせに関連する写真を送信するため、お客様にアクセスを許可します。

  • NSFaceIDUsageDescription: Face ID を使用した認証へのアクセスを許可します。

iOS SDK を起動する

iOS SDK を開始する場所に次の行を追加します。

[UJET startWithOptions:nil];

ダイレクト アクセス ポイントを使用して、このキーでメニューの特定のポイントから iOS SDK を起動することもできます。

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

menuKey は、ダイレクト アクセス ポイント(DAP)を作成することで作成できます。DAP を作成する手順は次のとおりです。

  1. 管理者認証情報を使用して CCAI Platform ポータルにログインします。

  2. [設定] > [キュー] に移動します。

  3. メニュー構造からキューを選択します。

  4. [直接アクセス ポイントを作成] を選択します。

  5. テキスト フォームにキーを入力します。

  6. [保存] をクリックします。

ユーザーデータが更新されている場合は、ローカルからキャッシュを消去する

SDK は、ホストアプリからのペイロード署名リクエストの頻度を減らすため、認証トークンをキーチェーンにキャッシュして再利用します。このトークンは、有効期限が切れるか、clearUserData 呼び出しによって取り消されるまで使用されます。ホストアプリは、ユーザー関連のデータが変更または更新された場合(ログアウト イベントなど)に、このキャッシュを取り消す役割を担います。

[UJET clearUserData];

コンタクト センター AI プラットフォームを開始する前に、既存のセッションを確認する

セッションを開始する前に、現在のセッションがないかどうかを確認します。これは、userId が変更された場合に特に重要です。

[UJET getStatus];

既存のセッションがある場合は、セッションを再開するか、アクションをキャンセルするようユーザーに求める必要があります。

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

カスタマイズ

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

会社名は、[Admin Portal] > [Settings] > [Support Center Details] > [表示名] から取得されます。

会社名の代わりにロゴ画像を設定するには、次のようにします。

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

画像が大きすぎる場合は、領域に収まるようにサイズが変更されます。

文字列

値をオーバーライドして文字列をカスタマイズすることもできます。たとえば、Localizable.strings に次の Key-Value を追加します。

"ujet_greeting_title" = "Title";

"ujet_greeting_description" = "Description";

カスタマイズ可能な文字列は、ujet.strings ファイルに一覧表示されます。

ダークモード

フォントの読みやすさを向上させるために、ダークモードで使用する色の色合いを指定できます。

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

このプロパティを設定しない場合、ダークモードには UJETGlobalTheme.tintColor が使用されます。アプリがダークモードをサポートしている場合は、このプロパティを設定することをおすすめします。ダークモードに適した色合いを選択する方法については、次の Apple の記事をご覧ください。

チャットのテーマ

チャット画面をカスタマイズするには、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)
}

ウェブフォームの設定

ウェブフォーム機能を構成するには、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/署名の生成中にエラーが発生した場合は、Error を指定して callback.onError() を使用してコールバックを呼び出します。

SDK に渡される辞書(FormDataEvent)は、次の構造にする必要があります。

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

署名(HMAC-SHA:256)は、data を使用して生成し、共有秘密鍵で署名する必要があります。データのオブジェクトキーは、署名を生成する前にアルファベット順に並べ替える必要があります。また、同じ data を SDK に送信する必要があります。

セッション後の転送

チャットのカスタマイズとともに、セッション後の VA のカスタマイズを追加できます。これは、JSON ファイル(post_session プロパティを参照)を使用するか、UJETChatPostSessionVaTheme クラスを使用して行うことができます。枠線の太さは 0 または 1 のみです。セッション後の仮想エージェント エクスペリエンスを区別しない場合は、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)
}

チャットの操作メニュー

チャットのカスタマイズとともに、チャット アクション メニューのカスタマイズを追加できます。これは、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 のアイコンが表示されます。「icon-call-kit」という名前の画像を Xcassets に配置します。

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 に設定すると、通話は VoIP ではなく PSTN 経由で常に接続されます。値が 0 の場合、最大レイテンシと最小帯域幅のしきい値はそれぞれ 10,000 ミリ秒と 10 KB/秒です。たとえば、値が 0.5 の場合、最小レイテンシと帯域幅はそれぞれ 5,000 ミリ秒と 15 KB/秒になります。

この値は、次の手順で構成できます。

  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 アンケートをスキップする

ユーザーが CSAT アンケートをスキップできるボタンを追加できます。次のコードサンプルは、ボタンを追加する方法を示しています。

let options = UJETGlobalOptions()
options.skipCsat = true

アクティビティ インジケーターをカスタマイズする

独自のローダー アニメーション(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. デバイスの言語がアプリでサポートされていない場合は、英語が使用されます。

外部デフレクション リンクのアイコンを設定する

外部デフレクション リンク チャネルのアイコンをカスタマイズするには、アプリのアセット カタログにアイコンをアップロードします。管理ポータルで [Settings] > [Chat] > [External Deflection Links] > [View links] > [Add Deflection Link] の順に選択して外部デフレクション リンクを作成する際に、同じアイコン名を使用してください。管理ポータルのアイコン名がアプリにアップロードされたアイコンと一致しない場合、SDK はデフォルトのアイコンを使用します。アセット カタログに画像を追加する方法については、こちらのリンクをご覧ください。

CRM にカスタムデータを送信する

カスタムデータを CRM チケットに送信できます。

カスタムデータを送信するには、次の 2 つの方法があります。

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

安全でない方法でカスタムデータを送信している

この方法は、アプリケーションが中間者攻撃を受ける可能性のある脆弱性を生み出すため、おすすめしません。この方法を使用する場合、Google はセキュリティ上の問題や発生する可能性のある損害について責任を負いません。アプリケーションでカスタムデータを送信する場合は、前述の安全な方法を使用することをおすすめします。または、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];

署名されていないカスタムデータを使用して外部チャットの文字起こしを送信する

署名されていないカスタムデータでチャットが開始されたときに、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: 文字列 [エージェントのアバターの URL、省略可]

  • transcript: 配列

    • sender: 文字列 [end_user または agent]

    • timestamp: 文字列(例: 2021-03-15 12:00:00Z)

    • content: 配列

      • type: 文字列 [text、media のいずれか]

      • text: 文字列 [テキストタイプに必須]

      • media: 辞書 [メディアタイプに必須]

        • type: 文字列 [image、video のいずれか]

        • url: 文字列 [メディア ファイルを指す公開 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"
        }
      ]
    }
  ]
}

テキストタイプではマークダウンを使用できます。サポートされている構文には、斜体、太字、箇条書き、ハイパーリンク、下線(--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
  • 日付

    • 13 桁の UTC Unix タイムスタンプ形式。(ミリ秒を含む)
  • URL

    • HTTP URL 形式
CRM の例

CRM の例を次に示します。

ロケーション

CoreLocation フレームワークを使用します。詳しくは、AppDelegate.m をご覧ください。

デバイスの OS バージョン
[customData set:@"os_version" label:@"OS Version" stringValue:[[UIDevice currentDevice] systemVersion]];

顧客データの表示を防止する

カスタム データ オブジェクトで invisible_to_agent プロパティを使用すると、署名付きまたは署名なしのカスタム データがエージェント アダプタに表示されないようにできます。前の例では、"invisible_to_agent" : true が ssn オブジェクトに含まれているため、エンドユーザーの社会保障番号はエージェント アダプタに表示されません。

カスタム データ オブジェクトに "invisible_to_agent" : true プロパティを含めると、次の動作が想定されます。

詳細については、エージェント アダプタでセッション データを表示するをご覧ください。

予約済みのデータ プロパティ

セッションの開始時に、予約済みのデータ プロパティを署名付きカスタムデータとしてコンタクト センター AI プラットフォーム(CCAI プラットフォーム)に送信できます。詳細については、予約済みデータ プロパティを送信するをご覧ください。

カスタムデータの予約済みデータ プロパティの例を次に示します。

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

Flow をカスタマイズ

このセクションでは、フローをカスタマイズする方法について説明します。

ホストアプリのイベントを処理するために 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 通話のエージェントは、エンドユーザーがアプリを持っている場合と持っていない場合の両方で、SMS によるスマート アクションを使用できます。

CCAI Platform ポータルで、[Settings] > [Operation Management] > [Enable Send SMS to Download App] に移動します。

ユニバーサル リンクまたはカスタム URL スキームを設定した後、ウェブページ(例: https://your-company.com/support)でアプリの URL を設定できます。どちらの方法でも選択できます。

ディープリンクを処理するデリゲート メソッドを実装する

ユニバーサル リンクとカスタム URL は、それぞれ https://your-company.com/support?call_id=x&nonce=y と your-company://support?call_id=x&nonce=y のようになります。クエリ パラメータのないリンクの 1 つを、管理ポータルの [アプリの URL] に入力します。たとえば、カスタム URL スキームを使用する場合は your-company://support を指定します。

デリゲート メソッドでは、ユニバーサル リンクまたはカスタム URL の URL パスとパラメータが 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 プラットフォーム イベントをモニタリングする

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

    • セッション データ
  • 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)バンドル ID

  • app_version: (NSString)

  • company:(NSString)サブドメイン

  • 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/UJETKit.json"

SwiftPM

UJET プロダクトと UJETCobrowse プロダクトを選択し、アプリ ターゲットに追加します。

UJETGlobalOptions.cobrowseKey の値を会社の画面共有キーに設定します。

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

UJET.setGlobalOptions(options)

デバイスの全画面共有(省略可)

デバイスの全画面共有を使用すると、サポート エージェントは、お客様のアプリケーション以外の画面を表示できます。これは、サポート エージェントがシステム設定の状態を確認する必要がある場合や、ユーザーが複数のアプリケーション間を移動する様子を確認する必要がある場合に役立ちます。この機能が不要な場合は、このセクションをスキップできます。

画面共有の同意ダイアログをカスタマイズするには、プロバイダ クラスに UJETCobrowseAlertProvider プロトコルを実装する必要があります。この実装では、それぞれのプロトコル メソッドを介して、カスタムの UIViewController または UIViewController を継承するその他のオブジェクトを返します。UIViewController には、承認用と拒否用の 2 つのボタンが必要です。

同意を取得したら、クロージャ consentStatus を呼び出して SDK に渡します。cobrowseFullDeviceRequestAlert の UIViewController。デリゲートには、タイトル付きの RPSystemBroadcastPickerView(以下のサンプルコードを参照)と、別の拒否ボタンを含める必要があります。拒否ボタンをクリックすると、dismissed クロージャが呼び出されます。

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. [Include UI Extension] チェックボックスをオフにします。

  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 キーチェーンを介していくつかのシークレットを共有する必要があります。これは、独自のキーチェーン グループを使用して行われるため、アプリのキーチェーンの他の部分から分離されます。

Xcode で、アプリ ターゲットと拡張機能ターゲットの両方を選択します。io.cobrowse キーチェーン グループのキーチェーン共有の利用資格を追加します。

plist にバンドル ID を追加する

以前に作成した拡張機能のバンドル ID を取得し、このエントリをアプリの Info.plist に追加します。

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

拡張機能を実装する

Xcode によって、先ほど作成したターゲットの一部として SampleHandler.m ファイルと SampleHandler.h(または SampleHandler.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 を最小化する

チャット セッションまたは通話の進行中に、コンタクト センター 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 で追跡するのが難しい場合

この記事では、2 つの選択肢が提示されています。Git LFS を使用することをおすすめします。

ビットコードを使用していない場合は、バイナリからビットコードを削除することもできます。UJETKit.xcframework フォルダで次のコマンドを実行します。

xcrun bitcode_strip -r UJET -o UJET

dyld: ライブラリが読み込まれていないエラー

[Target] > [Build Settings] > [Linking] で、[Runpath Search Paths] に @executable_path/Frameworks を追加します。

iTunes Connect でのアプリの送信

Voice over IP バックグラウンド モードが有効になっているため、審査プロセス中に Apple から次の質問を受けることがあります。

ユーザーはアプリで VoIP 通話を受信できますか?

質問に「はい」と回答します。

SDK の起動時にアラート通知が利用できない

以下を確認します。

  • シミュレータではなく、実際のデバイスを使用してください。

  • プッシュ通知と [Background Modes] > [Voice over IP] 機能を有効にします。

それでも問題が解決しない場合は、配布プロビジョニング プロファイル(アドホックまたは 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)
    }
}