‫Flutter ל-iOS

יש כמה דרכים להגדיר אפליקציות לנייד כך שיפעלו עם Contact Center AI Platform (פלטפורמת CCAI), כולל באמצעות Flutter. בדף הזה מוסבר איך לשלב את iOS SDK באפליקציית iOS באמצעות Flutter.

לפני שמתחילים

לפני שמבצעים את ההוראות בדף הזה, צריך קודם לפעול לפי ההוראות שבקטע שילוב באמצעות Flutter.

שילוב ה-SDK באמצעות CocoaPods

כדי לשלב את ה-SDK באמצעות CocoaPods, מבצעים את השלבים הבאים:

  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 ואז את יעד האפליקציה. מוסיפים את PushKit.framework בקטע Frameworks, Libraries, and Embedded Content.

הגדרת קישורי עומק

כדי להגדיר קישורי עומק, מוסיפים את הקוד הבא:

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