Zendesk Chat SDK を使ってiOSアプリにカスタマーサポートチャットを1時間で実装する

## はじめに
はじめまして。
今回は気軽にチャットによるカスタマーサポート(cs)機能を追加できるSaaS
Zendesk Chat を導入しiOSアプリでチャットができるようにする手順を記録していきます。
（[Chat SDK v2 for iOS](https://developer.zendesk.com/embeddables/docs/chat-sdk-v-2-for-ios/introduction)を利用しています）

zendeskは便利な反面、ios SDKのドキュメントが少ないのと
情報がいくつかの製品のドキュメントに跨っており、
必要な設定を見落としがちなので記事にまとめることにしました。

## zendesk chatの説明

このようなチャットの画面を簡単に構築できるツールです。

<img src="https://raw.githubusercontent.com/zendesk/mobile_sdk_images/master/ios/docs-chat-white.png" width="500px">

料金形態は↓こんな感じ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25725/be6c4727-7551-6848-43a4-ef0a884e119f.png)

詳細な料金表は[こちら](https://www.zendesk.co.jp/chat/compare/#compare)

無料含めて４プランあり、
push通知が$19/月のTeam以上、
営業時間設定が$35/月のProfessional以上になっています。

ある程度規模のあるプロダクトなら Professional プランが安心ですが、工夫次第では無料プランでも十分に活用できそうです。


## 大まかな手順
- SDK導入
- 初期化
- チャット画面の呼び出し
- エージェントがオフラインならチャットボタンを消す
- Push通知設定

## SDK導入

[公式ドキュメント](https://developer.zendesk.com/embeddables/docs/chat-sdk-v-2-for-ios/getting_started)に従って作業します。

SDKの導入はCocoaPods、Carthage、手動の3パターンありますが
私はCocoaPodsを利用しています。

これがPodfileです

```Podfile
target 'MyApp' do
　# zendesk chatに必要
  pod 'ZendeskChatSDK'

  # zendesk chatで細かい挙動のフックをしたいときに必要
  pod 'ZendeskChatProvidersSDK'

  # push通知を飛ばすために必要だった Support
  pod 'ZendeskSupportSDK'
```

ハマったのが、Push通知を飛ばすために
どうやらZendeskSupportSDKが必要になるということでした。

「SDKが用意してくれる基本のチャット画面を利用するだけ」という必要最低限の利用であれば
`pod 'ZendeskChatSDK'`だけで動きます。

## 初期化

ZendeskSuportとChatを初期化していきます。

```swift
import ZendeskCoreSDK
import SupportProvidersSDK
import ChatProvidersSDK
import ChatSDK

Zendesk.initialize(appId: ZENDESK_APP_ID,
            clientId: ZENDESK_CLIENT_ID,
            zendeskUrl: ZENDESK_URL)
Support.initialize(withZendesk: Zendesk.instance)
        
Chat.initialize(accountKey: ZENDESK_ACCOUNT_KEY, appId: ZENDESK_CHAT_APP_ID)
```
Zendesk.initializeに渡すID群は `zendeskの設定 > install modele SDK` に
記載されているのをコピペするだけなのですが、
ここで迷うのが Chat.initializeに渡す `accountKey` と `appId` って
どこに書いてあるんだ！ということです。


`accountKey`はこちら、`chat > 右上のアイコン > 接続を確認` で確認できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25725/d799269e-7316-64bc-c644-7b8ad64cadda.jpeg" width="600px">

`appId` はメソッドの定義を見ると `appId: String? = nil` と、
appIdを指定し無くても実行できます。

無くても動くので、すっかりその存在を忘れてしまうのですが
これがないと、**Push通知が届きません。**

`appId`は後半の Push通知についての箇所で明記するので
いったん `Chat.initialize(accountKey: チャットアカウントキー)` だけで動かして
先に進んでしまっても問題ありません。


ユーザーの指定はjwtの認証を必要としない
匿名ユーザーで問題ありませんでした。

```
// 匿名ユーザーとして作成
let anonymous = Identity.createAnonymous()
Zendesk.instance?.setIdentity(anonymous)
```


これらを `AppDelegate.swift` の `didFinishLaunchingWithOptions` で初期化します。

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

  Zendesk.initialize(appId: ZENDESK_APP_ID,
            clientId: ZENDESK_CLIENT_ID,
            zendeskUrl: ZENDESK_URL)
  Support.initialize(withZendesk: Zendesk.instance)
  Chat.initialize(accountKey: ZENDESK_ACCOUNT_KEY, appId: ZENDESK_CHAT_APP_ID)

  let anonymous = Identity.createAnonymous()
  Zendesk.instance?.setIdentity(anonymous)
}

```

わかりやすいようにベタで書いていますが、
当然メソッドに切り出すなりクラスに切り出すなりしてご使用ください。笑


## チャット画面の呼び出し

公式ドキュメントだと[この辺り](https://developer.zendesk.com/embeddables/docs/chat-sdk-v-2-for-ios/getting_started#starting-a-chat)です。

```swift
    // 「チャット画面を開くボタン」のタップアクション
    @IBAction func tapStartChat(_ sender: Any) {
        try! startChat()
    }

    // チャット画面への遷移（ViewContollerを生成してモーダル遷移）
    func startChat() throws {
        let chatViewController = trybuildChatUI()
        
        let button = UIBarButtonItem(title: "閉じる", style: .plain, target: self, action: #selector(dismissNavigationController))
        chatViewController.navigationItem.leftBarButtonItem = button

        let modalController = UINavigationController(rootViewController: chatViewController)
        present(modalController, animated: true)
    }

    // チャット画面の生成
    private func buildChatUI() throws -> UIViewController {
        let messagingConfiguration = getMessagingConfiguration()
        let chatConfiguration = getChatConfigure()

        // Build view controller
        let chatEngine = try ChatEngine.engine()
        return try Messaging.instance.buildUI(engines: [chatEngine], configs: [messagingConfiguration, chatConfiguration])
        // ちなみにenginesにはChatEngineだけでなく、
        // メッセージを残してメールでやり取りをしたりFAQを用意するSupportEngine、
        // Botが自動でよしなに返信してくれるAnswerBotEngineがあります。
    }
    
    
    // チャットBotの表示の設定
    private func getMessagingConfiguration() -> MessagingConfiguration {
        let messagingConfiguration = MessagingConfiguration()
        messagingConfiguration.name = "チャット画面に表示されるBotの名前"
        return messagingConfiguration
    }
    
    // チャットの詳細設定
    private func getChatConfigure() -> ChatConfiguration {
        // isPreChatFormEnabled = trueだと
        // 画面を開いたときに「お問い合わせありがとうございます。」とチャットが自動でメッセージを送信し
        // ユーザーに対して、名前やメールアドレス、電話番号などを質問してくれる
        let chatConfiguration = ChatConfiguration()
        chatConfiguration.isPreChatFormEnabled = true
        
        // isPreChatでどの情報をユーザーに聞くか
        let formConfiguration = ChatFormConfiguration(name: .optional, email: .optional, phoneNumber: .hidden, department: .hidden)
        chatConfiguration.preChatFormConfiguration = formConfiguration
        
        return chatConfiguration
    }

    @objc private func dismissNavigationController() {
        self.navigationController?.dismiss(animated: true, completion: nil)
    }

```


これでチャット画面を開き、
エージェントとチャットをすることができるようになりました。


## エージェントがオフラインならチャットボタンを消す

エージェントがオフラインだと
チャットを開始すると自動で `「オンライン中のエージェントがおりません」` とBotが回答してくれます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25725/f1df8dae-f222-43dd-28af-f2539fea6332.jpeg" width="500px">


とはいえ、この辺の自動回答メッセージがカスタマイズできない（多分）こともあり、
エージェントがいないときはボタン自体をdisabledにしたい！という場合は
`ChatProvidersSDK`の機能を使って実装していきます。

公式ドキュメントだと[この辺り](https://developer.zendesk.com/embeddables/docs/chat-sdk-v-2-for-ios/providers#getting-the-availability-of-agents)

```swift

Chat.accountProvider?.getAccount { (result) in
            switch result {
            case .success(let account):
                switch account.accountStatus {
                case .online:
                    // オンラインの場合、ボタンをenabledに
                    chatButton.isEnabled = true
                default:
                    // オンライン以外の場合、ボタンをdisabledに
                    chatButton.isEnabled = false
                }
            case .failure(let error):
                // エラーが返ってきたときもボタンをdisabledに
                chatButton.isEnabled = false
            }
        }
```

## Push通知設定
いよいよ Push通知設定です。
Push通知は、エージェントがユーザーに向けて返信をした場合と
エージェントがチャットを終了させた場合に発火します。

- pemの作成
- zendesk chatの管理画面にpemをアップロード
- pem登録時に作成されるアプリIDをChat.initialize()に渡す
- push通知の実装

この流れでpush通知が利用できるようになります。

### pemの作成

参考になるのは[こちらの記事](https://develop.zendesk.com/hc/en-us/articles/360033915354-Enabling-iOS-push-notifications-for-the-Chat-SDK)です。

ご自身の[Apple Developerのページ](https://developer.apple.com/account/resources/identifiers/list)から作業します。

push通知の設定のEditをクリックし、
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25725/d6c61a70-9923-1276-698a-1db992259c5d.png" width="600px">

**Production** SSL Certificate をCreate Certificate します
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25725/a44513e2-cc0a-7320-cbfc-2756a1ceb467.png" width="600px">


すると.p12の証明書ファイルがDLできるのでこれからpemを作成します。

先程の公式ドキュメントに
> Save the .p12 file to your computer. Leave the password empty.

と書いてあるのでpasswordは空で設定します。

ターミナルの作業

```
> openssl pkcs12 -clcerts -nodes -in ~/Desktop/証明書.p12  -out myapp.pem                                                                       

Enter Import Password: # 空でEnter
MAC verified OK
```

pemが正しく生成されたのかテストを実行

```
> openssl s_client -connect gateway.push.apple.com:2195 -cert myapp.pem -keymyapp.pem

~~~大量の文字列~~~
New, TLSv1/SSLv3, Cipher is DES-CBC3-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : DES-CBC3-SHA
    Session-ID:
    Session-ID-ctx:
    Master-Key: 
    Start Time: 1602554889
    Timeout   : 7200 (sec)
Verify return code: 0 (ok)
```

最後が `Verify return code: 0 (ok)` になっていれば成功です。

### zendesk chatの管理画面にpemをアップロード

`chat管理画面 > 設定 > アカウント > モバイルSDK > アプリを追加`
でpemを登録すると「アプリが作成されました」
とアプリIDが生成されます。

![スクリーンショット 2020-10-21 16.57.15.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25725/f006e24a-bed3-34d0-7410-e23186cebb9c.jpeg)

### pem登録時に作成されるアプリIDをChat.initialize()に渡す

最初の初期化のときのこれ↓です

```swift
Chat.initialize(accountKey: ZENDESK_ACCOUNT_KEY, appId: ここ！！)
```

これに気が付かず半日潰した。。。

### push通知の実装

公式ドキュメントは[こちら](https://developer.zendesk.com/embeddables/docs/chat-sdk-v-2-for-ios/push_notifications)

一般的にiosアプリでpush通知を利用するときと同様の設定も多く含みます。

・push通知の許可依頼を表示させ

```swift
import UserNotifications

let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.delegate = self
notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
   guard granted else { return }

   DispatchQueue.main.async {
      UIApplication.shared.registerForRemoteNotifications()
   }
}
```

・didRegisterForRemoteNotificationsWithDeviceTokenでトークンを登録

``` AppDelegate.swift
import ChatProvidersSDK

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    Chat.registerPushToken(deviceToken)
}
```

・ push通知をタップしたときの挙動

```AppDelegate.swift
import ChatProvidersSDK

func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
   let userInfo = response.notification.request.content.userInfo
   let application = UIApplication.shared
   Chat.didReceiveRemoteNotification(userInfo, in: application)
   completionHandler()
}
```

・アプリがフォアグラウンドでもpush通知を受信

```AppDelegate.swift
import UserNotifications

func userNotificationCenter(_ center: UNUserNotificationCenter,
                           willPresent notification: UNNotification,
                           withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
   completionHandler([.alert, .sound, .badge])
}

```

これでエージェントが返信をするとpush通知が届きます！
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25725/9734d719-7f2d-ac76-9af6-332ed2afb99d.jpeg" width="600px">

## まとめ

- チャット画面を開いて、エージェントとチャットができた
- エージェントのオンライン状態をアプリ側でフックできた
- チャットが来たときに Push通知を受け取ることができた