iOS 向け Flutter

モバイルアプリを Contact Center AI プラットフォーム(CCAI プラットフォーム)と連携させるには、Flutter を使用するなど、さまざまな方法があります。このページでは、Flutter を使用して iOS SDK を iOS アプリに統合する方法について説明します。

始める前に

このページの手順を行う前に、まず Flutter を使用して統合するの手順を行ってください。

CocoaPods を使用して SDK を統合する

CocoaPods を使用して SDK を統合する手順は次のとおりです。

  1. Podfile を開き、次のコード例のように、ターゲットに依存関係を追加します。

    UJETPodspec = { :podspec => 'https://sdk.ujet.co/ios/UJET.podspec' }
    
    pod 'UJET', UJETPodspec
    pod 'UJET/Cobrowse', UJETPodspec
    
  2. ターミナルで example/ios ディレクトリに移動し、pod install コマンドを実行して依存関係をインストールします。

iOS プッシュ通知を設定する

iOS プッシュ通知を設定する手順は次のとおりです。

  1. Xcode で example/ios/Runner.xcodeproj を開きます。

  2. AppDelegate.swift ファイルに次のコードを追加します。

    import PushKit
    import UJETKit
    
    @UIApplicationMain
    @objc class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Register Flutter Plugins
        GeneratedPluginRegistrant.register(with: self)
        UJETModule.register(with: self.registrar(forPlugin: "UjetModule")!)
        UJETModule.onInitDone = {
            // setup push notification
            let voipRegistry = PKPushRegistry(queue: DispatchQueue.main)
            voipRegistry.desiredPushTypes = Set([PKPushType.voIP])
            voipRegistry.delegate = self
            UNUserNotificationCenter.current().delegate = self
        }
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    }
    
    // Extension for Push Notification
    
    extension AppDelegate {
        func tokenFromData(data: Data) -> String {
            return data.map { String(format: "%02x", $0) }.joined()
        }
    
        override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
            print("apns token: ", tokenFromData(data: deviceToken))
    
            UJET.updatePushToken(deviceToken, type: .APN)
        }
    
        override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
            UJET.updatePushToken(nil, type: .APN)
        }
    
        override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
            handleNotification(userInfo: userInfo, completion: nil)
        }
    }
    
    // MARK: PushKit
    
    extension AppDelegate: PKPushRegistryDelegate {
        func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
            print("voip token: ", tokenFromData(data: credentials.token))
    
            if type == .voIP {
                UJET.updatePushToken(credentials.token, type: .voIP)
            }
        }
    
        func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
            if type == .voIP {
                UJET.updatePushToken(nil, type: .voIP)
            }
        }
    
        func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
            if type == .voIP {
                handleNotification(userInfo: payload.dictionaryPayload, completion: completion)
            }
        }
    }
    
    extension AppDelegate {
    // handle push received in foreground state
    override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        let userInfo = notification.request.content.userInfo
        handleNotification(userInfo: userInfo, completion: nil)
    }
    
    // handle push received and tapped in background state
    override func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo = response.notification.request.content.userInfo
        handleNotification(userInfo: userInfo, completion: nil)
    }
    }
    
    private func handleNotification(userInfo: [AnyHashable: Any], completion: (() -> Void)?) {
        if userInfo["ujet"] != nil {
            UJET.receivedNotification(userInfo, completion: completion)
        } else {
            // Handle your notification here
            completion?()
        }
    }
    
  3. .xcodeproj を選択し、アプリのターゲットを選択します。Frameworks, Libraries, and Embedded Content セクションに PushKit.framework を追加します。

ディープリンクを設定する

ディープリンクを設定するには、次のコードを追加します。

// Extension for deep linking

extension AppDelegate {
    override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
        print("Open app with url: \(url.absoluteString)")

        return self.handleRouting(url)
    }

    override func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        // Universal links
        if NSUserActivityTypeBrowsingWeb == userActivity.activityType {
            return self.handleRouting(userActivity.webpageURL!)
        } else if userActivity.activityType == "INStartAudioCallIntent" {
            // Open app from Call history
            UJET.start(with: UJETStartOptions())

            return true
        }
        return false
    }

    func handleRouting(_ url: URL) -> Bool {
        let availableSchema = [
            "ujetrn", // TODO: Change to your custom URL scheme. Config from Portal > Developer Settings > Mobile App > Enable Send SMS to Download App > iOS App > URL
            "https" // universal link
        ]
        let availableHostAndPath = [
            "call",         // custom URL scheme
            "ujet.cx/app"  // universal link,
        ]

        if !(availableSchema.contains(url.scheme ?? "")) {
            return false
        }

        let hostAndPath = String.init(format: "%@%@", url.host ?? "", url.path)
        if !(availableHostAndPath.contains(hostAndPath)) {
            return false
        }

        // ujet://call?call_id={call_id}&nonce={nonce}
        // https://ujet.co/app?call_id={call_id}&nonce={nonce}
        let urlComponents: URLComponents? = URLComponents(url: url, resolvingAgainstBaseURL: false)
        let queryItems = urlComponents?.queryItems
        let callId = value(forKey: "call_id", fromQueryItems: queryItems)

        // validate call ID
        if !isValidCallId(callId) {
            return false
        }

        guard let nonce = value(forKey: "nonce", fromQueryItems: queryItems) else {
            return false
        }

        let options = UJETStartOptions.init(callId: callId!, nonce: nonce)

        UJET.start(with: options)

        return true
    }

    func value(forKey key: String?, fromQueryItems queryItems: [URLQueryItem]?) -> String? {
        let predicate = NSPredicate(format: "name=%@", key ?? "")
        let filtered = (queryItems as NSArray?)?.filtered(using: predicate) as? [URLQueryItem]
        let queryItem: URLQueryItem? = filtered?.first

        return queryItem?.value
    }

    func isValidCallId(_ callId: String?) -> Bool {
        if (callId ?? "").isEmpty {
            return false
        }

        let nonNumbers = CharacterSet.decimalDigits.inverted
        let r = callId?.rangeOfCharacter(from: nonNumbers)

        return r == nil
    }
}