Intercom & PushKit

Intercom integration

In order to integrate VoIP push messages into your application, you need to make the following changes.

Capabilities

Go to your project -> your target -> Signing & Capabilities -> + Capability.

Add Background Modes.

And Push Notifications.

Further it is necessary to mark the following points: Audio, Voice over IP, and Remote notifications.

Frameworks

You need to link CallKit and PushKit frameworks.

Code


import AVFoundation
import CallKit
import PushKit
import SmartSpacesKit
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder {

...

    let intercomHandler = Intercom.Handler()

}

extension AppDelegate: UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        #if DEBUG
            UserDeviceManager.shared().pushEnvironment = .development
        #else
            UserDeviceManager.shared().pushEnvironment = .production
        #endif
        
        ...

        return intercomHandler.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        intercomHandler.application(application, continue: userActivity, restorationHandler: restorationHandler)
    }

}


Create the intercom handler


import AVFoundation
import CallKit
import Foundation
import JitsiMeetSDK
import PushKit
import SmartSpacesKit

extension Intercom {

    class Handler: NSObject {

        let callProvider: CXProvider = {
            let configuration: CXProviderConfiguration

            if #available(iOS 14, *) {
                configuration = CXProviderConfiguration()
            } else {
                configuration = CXProviderConfiguration(localizedName: "VoIP Service")
            }

            configuration.supportsVideo = true
            configuration.supportedHandleTypes = [.generic]
            configuration.maximumCallsPerCallGroup = 1
            configuration.iconTemplateImageData = UIImage(named: "icon name")?.pngData()

            return CXProvider(configuration: configuration)
        }()

        var currentIntercomDevice: Intercom.Device?

        let intercomManager = SWSmartLockManager.shared().intercomManager

        var isCallStarted = false

        let jitsiView = JitsiMeetView(frame: .zero)

        let pushRegistry = PKPushRegistry(queue: nil)

        let userDeviceManager = UserDeviceManager.shared()

        override init() {
            super.init()

            callProvider.setDelegate(self, queue: nil)
            intercomManager.delegate = self
            jitsiView.delegate = self
            pushRegistry.delegate = self
            pushRegistry.desiredPushTypes = [.voIP]
        }

    }

}


Extension for supporting fast Jitsi initialization


extension Intercom.Handler {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        return JitsiMeet.sharedInstance().application(application, didFinishLaunchingWithOptions: launchOptions ?? [:])
    }

    func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        guard userActivity.activityType == "INStartVideoCallIntent" else {
            return false
        }

        return JitsiMeet.sharedInstance().application(application, continue: userActivity, restorationHandler: restorationHandler)
    }

}

Intercom.Handler main actions:


extension Intercom.Handler {

    func showCallScreen() {
        guard currentIntercomDevice != nil else { return }
        // Display calling screen
    }

    func hideCallScreen() {
        // Hiding the calling screen
    }

    func startAudio() {
        guard
            let audioToken = currentIntercomDevice?.audio?.token,
            let audioUrl = currentIntercomDevice?.audio?.url
        else {
            print("No audio token and/or URL provided")
            return
        }

        let callHandle = currentIntercomDevice?.name
        let callUUID = currentIntercomDevice?.id
        let options = JitsiMeetConferenceOptions.fromBuilder { builder in
            builder.audioMuted = false
            builder.audioOnly = true
            builder.callHandle = callHandle
            builder.callUUID = callUUID
            builder.room = audioUrl.lastPathComponent
            builder.serverURL = audioUrl
            builder.token = audioToken
            builder.welcomePageEnabled = false
        }

        jitsiView.join(options)
    }

    func stopAudio() {
        jitsiView.hangUp()
    }

    func configureAudioSession(completion: @escaping (Bool) -> Void) {
        let audioSession = AVAudioSession.sharedInstance()

        audioSession.requestRecordPermission { granted in
            if granted {
                do {
                    if audioSession.category != .playAndRecord {
                        try audioSession.setCategory(.playAndRecord, options: .defaultToSpeaker)
                    }
                    if audioSession.mode != .voiceChat {
                        try audioSession.setMode(.voiceChat)
                    }
                } catch {
                    print("Error configuring AVAudioSession: \(error.localizedDescription)")
                }
            } else {
                print("Mic permission is not granted")
            }

            completion(granted)
        }
    }

    // The call ends by user
    func endCurrentCall() {
        guard let intercomDevice = currentIntercomDevice else {
            print("End current call: No current intercom device")
            return
        }

        hideCallScreen()
        stopAudio()
        callProvider.reportCall(with: intercomDevice.id, endedAt: nil, reason: .remoteEnded)
        intercomManager.report(callStatus: .end, for: intercomDevice) { error in
            if let error = error {
                print("Reporting the call status \(Intercom.CallStatus.end) failed with error: \(error)")
            }
        }

        currentIntercomDevice = nil
        isCallStarted = false
    }

    // The call stopped not by user
    func stopCurrentCall() {
        guard let intercomDevice = currentIntercomDevice else {
            print("Stop current call: No current intercom device")
            return
        }

        callProvider.reportCall(with: intercomDevice.id, endedAt: nil, reason: .answeredElsewhere)

        currentIntercomDevice = nil
        isCallStarted = false
    }

    func openDoor() {
        guard let intercomDevice = currentIntercomDevice else {
            print("Open door: No current intercom device")
            return
        }

        // Completion or didFinishOpening delegate method
        intercomManager.open(doorAssociatedWith: intercomDevice, completion: nil)
    }

}

SmartSpacesKit’s Intercom.Manager delegate


extension Intercom.Handler: IntercomManagerDelegate {

    func didFinishOpening(intercomManager: Intercom.Manager, device intercomDevice: Intercom.Device, error: Error?) {
        // Handle the callback
    }

    func didReceiveIncomingCall(intercomManager: Intercom.Manager, device intercomDevice: Intercom.Device) {
        let backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)

        currentIntercomDevice = intercomDevice

        let update = CXCallUpdate()
        update.hasVideo = true
        update.localizedCallerName = intercomDevice.name
        update.supportsDTMF = false
        update.supportsGrouping = false
        update.supportsHolding = false
        update.supportsUngrouping = false

        callProvider.reportNewIncomingCall(with: intercomDevice.id, update: update) { [weak self] error in
            if let error = error {
                print("Reporting a new call is failed with error: \(error)")

                if let incomingCallError = error as? CXError, let errorCode = CXErrorCodeIncomingCallError.Code(rawValue: incomingCallError.errorCode) {
                    switch errorCode {
                    case .unknown:
                        break
                    case .unentitled:
                        print("The app isn’t entitled to receive incoming calls")
                    case .callUUIDAlreadyExists:
                        print("The incoming call UUID \(intercomDevice.id) already exists")
                    case .filteredByDoNotDisturb:
                        print("The incoming call is filtered because Do Not Disturb is active and the incoming caller is not a VIP")
                        // Handle Do Not Disturb
                    case .filteredByBlockList:
                        print("The incoming call is filtered because the incoming caller has been blocked by the user")
                    default:
                        break
                    }
                }

                self?.intercomManager.report(callStatus: .error, for: intercomDevice) { reportingError in
                    if let reportingError = reportingError {
                        print("Reporting the call status \(Intercom.CallStatus.error) failed with error: \(reportingError)")
                    }

                    UIApplication.shared.endBackgroundTask(backgroundTask)
                }

                return
            }

            UIApplication.shared.endBackgroundTask(backgroundTask)
        }
    }

    func shouldStopCalling(intercomManager: Intercom.Manager, device intercomDevice: Intercom.Device?) {
        guard currentIntercomDevice != nil else { return }
        stopCurrentCall()
    }

}

Handle events from Jitsi


extension Intercom.Handler: JitsiMeetViewDelegate {

    func conferenceWillJoin(_ data: [AnyHashable: Any]) {
        print("JitsiMeetView: Conference will join: \(String(describing: data))")
    }

    func conferenceJoined(_ data: [AnyHashable: Any]) {
        print("JitsiMeetView: Conference joined: \(String(describing: data))")
    }

    func conferenceTerminated(_ data: [AnyHashable: Any]) {
        print("JitsiMeetView: Conference terminated: \(String(describing: data))")
    }

    func participantJoined(_ data: [AnyHashable: Any]) {
        print("JitsiMeetView: Participant \(String(describing: data)) joined")
    }

    func participantLeft(_ data: [AnyHashable: Any]) {
        print("JitsiMeetView: Participant \(String(describing: data)) left")
    }

}

Handling PushKit events


extension Intercom.Handler: PKPushRegistryDelegate {

    func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
        guard type == .voIP else { return }

        userDeviceManager.update(voipNotificationToken: pushCredentials.token)
    }

    func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
        userDeviceManager.update(voipNotificationToken: Data())
    }

    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
        let backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)

        defer {
            UIApplication.shared.endBackgroundTask(backgroundTask)
            completion()
        }

        guard type == .voIP else {
            print("The push payload type is not VoIP")
            return
        }

        guard SWUserSessionManager.shared().session != nil else {
            print("The user is not logged in")
            return
        }

        guard JSONSerialization.isValidJSONObject(payload.dictionaryPayload), let payloadDictionary = payload.dictionaryPayload as? [String: Any] else {
            print("VoIP push payload is not valid")
            return
        }

        do {
            try userDeviceManager.restoreCurrentUserDevice()
        } catch {
            print("UserDeviceManager is not restored, error: \(error)")
            return
        }

        do {
            try intercomManager.on(pushNotificationPayload: payloadDictionary)
        } catch {
            print("Intercom.Manager returned an error: \(error)")
        }
    }

}

Handle CallKit events


extension Intercom.Handler: CXProviderDelegate {

    func providerDidReset(_ provider: CXProvider) {
        stopAudio()
        hideCallScreen()

        currentIntercomDevice = nil
    }

    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        guard let currentIntercomDevice = currentIntercomDevice else {
            print("There is no active intercomDevice, the action \(action.callUUID) failed")
            action.fail()
            return
        }

        isCallStarted = true

        intercomManager.report(callStatus: .start, for: currentIntercomDevice) { [weak self] error in
            guard error == nil else {
                print("Reporting the call status \(Intercom.CallStatus.start) failed with error: \(error!)")
                action.fail()
                return
            }

            self?.configureAudioSession { succeded in
                if succeded {
                    action.fulfill()
                } else {
                    action.fail()
                }
            }
        }
    }

    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        guard let currentIntercomDevice = currentIntercomDevice else {
            print("There is no active intercomDevice, the action \(action.callUUID) failed")
            action.fail()
            return
        }

        if isCallStarted {
            stopAudio()
        }

        intercomManager.report(callStatus: isCallStarted ? .end : .ignore, for: currentIntercomDevice) { error in
            if let error = error {
                print("Reporting the call status \(Intercom.CallStatus.end) failed with error: \(error)")
            }
        }

        isCallStarted = false
        action.fulfill()
    }

    func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
        showCallScreen()
        startAudio()
    }

    func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
        hideCallScreen()
    }

}