MigrationGuide from 2.x to 3.0.0 - Bandyer/Bandyer-iOS-SDK GitHub Wiki

This guide will introduce you to the changes made to the Kaleyra Video SDK 3.0.0 version. There are some breaking change you must take into account in order to use the 3.0 version of the SDK.

Table of contents

What's new

Minimum deployment target

The Kaleyra Video SDK raised its runtime target to iOS 12.0. While it is still possible to compile your app if your app's minimum deployment target is under iOS 12.0, you cannot use the Kaleyra Video SDK at runtime unless the iOS device, where your app is deployed to, is running iOS 12.0 and above. In other words, the SDK minimum deployment target is iOS 10, the runtime target is iOS 12.

Tools

Any collaborative tool (such as Chat, File sharing, Whiteboard and so on...) are now disabled by default. If your use cases require some collaborative tool you should enable them while setting up the Kaleyra Video SDK.

User authentication

Starting from 3.0.0 version the Kaleyra Video SDK introduced a strong user authentication mechanism based on JWT access tokens. In order to connect with the Kaleyra Video platform, any client must now provide an access token authenticating the user.

Region

The Kaleyra Video SDK added regional support. This means you can choose in which region of the world your users' data is stored. At the moment, the only two supported regions are Europe and India. Beware, every Region has its own dedicated infrastructure. Regions don't talk to each other, so you cannot connect one device to the Europe region another one to the India region and expect that they will be able to talk to each other.

Manual recording

The Kaleyra Video SDK added manual recording support to this revision. A call with "manual recording" type may be recorded from a certain point in time, the call admin can start and stop the recording whenever she / he requires it. You can also create calls with "manual recording" option using the updated CallOptions initialisers and factory methods.

Migrating to 3.0.0

Setup

First and foremost, the 3.0.0 version changed how you setup the Kaleyra Video and its features completely. You cannot create a Config objects directly anymore, now you use a set of builder objects that will guide you in the process of creating the configuration object.

For example, let's pretend you were setting up the SDK like this in the 2.x version:

import UIKit
import PushKit
import CallKit
import Bandyer

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let config = Config()
        config.environment = .sandbox
        config.isCallKitEnabled = true
        config.nativeUILocalizedName = "My wonderful app"
        config.nativeUIRingToneSound = "MyRingtoneSound"
        let callKitIcon = UIImage(named: "callkit-icon")
        config.nativeUITemplateIconImageData = callKitIcon?.pngData()
        config.supportedHandleTypes = Set(arrayLiteral: NSNumber(integerLiteral: CXHandle.HandleType.generic.rawValue))
        config.pushRegistryDelegate = self
        config.notificationPayloadKeyPath = "SET YOUR PAYLOAD KEY PATH HERE"
        BandyerSDK.instance().initialize(withApplicationId: "My app id", config: config)

        return true
    }
}

extension AppDelegate: PKPushRegistryDelegate {

    func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
        guard let token = pushCredentials.tokenAsString else { return }
        
        debugPrint("Push credentials updated \(token), you should send them to your backend system")
    }
}

Now in the 3.0.0 version you use a ConfigBuilder like this:

import UIKit
import PushKit
import CallKit
import Bandyer

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
	let config = try ConfigBuilder(appID: "My app id", environment: .sandbox, region: .europe)
                                .callKit { callkit in
                                    callkit.enabled { provider in
                                        provider
                                            .supportedHandles([.generic])
                                            .ringtoneSound("MyRingtoneSound")
                                            .icon(UIImage(named: "callkit-icon")!)
                                    }
                                }
                                .voip { voip in
                                    voip.automatic(pushRegistryDelegate: self)
                                }
                                .tools { tools in
                                    tools.chat()
                                         .whiteboard(uploadEnabled: true)
                                         .fileshare()
                                }
                                .build()
                        
        BandyerSDK.instance.configure(config)

        return true
    }
}

extension AppDelegate: PKPushRegistryDelegate {

    func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {        
        debugPrint("Push credentials updated \(pushCredentials.tokenAsString), you should send them to your backend system")
    }
}

As you can see from the snippet above, you use the builders provided by the SDK to build a Config using a declarative interface specifying what SDK settings you want to enable. As you might already noticed the tools must be enabled explicitly.

Connect

Next, you must connect the client to the Kaleyra Video platform. In the 2.0.0 version, you were used to open a user session and then start the CallClient and the ChatClient (if your app used the Chat feature of the Kaleyra Video SDK). In the 3.0.0 all these steps have been merged into a single method call.

Let's pretend you were starting the clients like this:

func startClients() {        
    BandyerSDK.instance().openSession(userId: "foobar")
    BandyerSDK.instance().callClient.add(observer: self, queue: .main)
    BandyerSDK.instance().chatClient.add(observer: self, queue: .main)
    BandyerSDK.instance().callClient.start()
    BandyerSDK.instance().chatClient.start()
}

Now in the 3.0.0 version you call the connect method on the BandyerSDK singleton instance:

func startClients() {
    BandyerSDK.instance.callClient.add(observer: self, queue: .main)
    BandyerSDK.instance.chatClient.add(observer: self, queue: .main)

    let provider = MyAccessTokenProvider()
    let session = Session(userId: "alice", tokenProvider: provider)
    BandyerSDK.instance.connect(session)
}

Session

As you might have noticed, the connect method takes a Session object as parameter. This object represents the current user's session used throughout the lifecycle of the SDK, from when you connect the client until you disconnect it explictly. You create a Session object providing the identifier of the user you want connect to the Kaleyra Video platform and a component conforming to the AccessTokenProvider protocol (more on this later). You can optionally provide a component conforming to the SessionObserver protocol that will be notified when session state changes occur.

Access tokens

The Kaleyra Video platform now uses a strong authentication mechanism based on JWT tokens while authenticating its clients. You are required to provide an object conforming to the AccessTokenProvider protocol to the Session object before connecting the SDK. The Kaleyra Video SDK will call the provideAccessToken(userId:completion:) method every time it needs an access token. You can retrieve an access token from our back-end system through REST APIs. We strongly suggest you to make those REST calls from your back-end system to ours instead of making REST calls from your iOS App. Once you retrieved an access token, you must call the completion closure provided in the provideAccessToken(userId:completion:) method with a Result value indicating success or failure.

Here's an example of an AccessTokenProvider:

import Foundation
import Bandyer

class RestAccessTokenProvider: AccessTokenProvider {

    private let client: HTTPClient
    
    private lazy var decoder: JSONDecoder = {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy =  .formatted(DateFormatter.remoteApiFormatter)
        return decoder
    }()

    init(client: HTTPClient) {
        self.client = client
    }

    func provideAccessToken(userId: String, completion: @escaping (Result<String, Error>) -> Void) {
        let request = URLRequest(url: URL(string: "https://my-app.com/user/access_token?user_id=\(userId)")!)

        client.post(request) { (result) in
            completion(Result {
                switch result {
                    case let .success(result):
                        do {
                            if (200..<300).contains(result.response.statusCode) {
                                return try decoder.decode(String.self, from: result.data)
                            } else {
                                throw BadRequestError()
                            }
                        } catch let(error) {
                            throw error
                        }
                    case let .failure(error):
                        throw error
                }
            })
        }
    }
}

Call and Chat clients

Call and chat clients still play an essential role in the Kaleyra Video SDK. However, their public interface changed a bit in the 3.0.0 version, you cannot start or stop them directly anymore.

Let's pretend you were starting and stopping the clients like this:

func startClients() {        
    BandyerSDK.instance().callClient.add(observer: self, queue: .main)
    BandyerSDK.instance().chatClient.add(observer: self, queue: .main)
    BandyerSDK.instance().callClient.start()
    BandyerSDK.instance().chatClient.start()
}

func stopClients() {
    BandyerSDK.instance().callClient.stop()
    BandyerSDK.instance().chatClient.stop()
}

Now in the 3.0.0 version the start, stop, pause, resume clients methods have been removed. You can still observe the clients state changes but you cannot stop or start them one by one. You start or stop them by calling the connect and disconnect methods of the BandyerSDK singleton instance respectively.

func startClients() {        
    BandyerSDK.instance.callClient.add(observer: self, queue: .main)
    BandyerSDK.instance.chatClient.add(observer: self, queue: .main)
    let provider = MyAccessTokenProvider()
    let session = Session(userId: "alice", tokenProvider: provider)
    BandyerSDK.instance.connect(session)
}

func stopClients() {
    BandyerSDK.instance.disconnect()
}
Incoming calls

Starting from 3.0.0 version you are required to add a new observer to the CallClient in order to be notified when an incoming call is received. The CallClientObserver and the IncomingCallObserver protocols have been split into two separate protocols. You must conform to the IncomingCallObserver protocol and register the observer to the CallClient object in order to receive incoming calls events.

Let's pretend you were listening for incoming calls events in the 2.x.x SDK version like this:


class ViewController: UIViewController {

    func startClients() {
        BandyerSDK.instance().openSession(userId: selectedUserId)
        BandyerSDK.instance().callClient.add(observer: self, queue: .main)
        BandyerSDK.instance().callClient.start()
    }
}

extension ViewController: CallClientObserver {

    func callClientWillStart(_ client: CallClient) {
	// Do something when client will start
    }

    func callClientDidStart(_ client: CallClient) {
	// Do something when client did start
    }

    func callClient(_ client: CallClient, didFailWithError error: Error) {
	// Do something when client fails
    }
    
    func callClient(_ client: CallClient, didReceiveIncomingCall call: Call) {
	// Do something when a call is received
    }
}

Now in the 3.0.0 version you must conform to a new protocol and subscribe as an event observer like this:


class ViewController: UIViewController {

    func startClients() {
        BandyerSDK.instance.callClient.add(observer: self, queue: .main)
        BandyerSDK.instance.callClient.addIncomingCall(observer: self, queue: .main)
        BandyerSDK.instance.callClient.start()
    }
}

extension ViewController: CallClientObserver {

    func callClientDidChangeState(_ client: CallClient, oldState: CallClientState, newState: CallClientState) {
	// Do something when client change its state
    }

    func callClient(_ client: CallClient, didFailWithError error: Error) {
	// Do something when client change has failed
    }
}

extension ViewController: IncomingCallObserver {

   func callClient(_ client: CallClient, didReceiveIncomingCall call: Call) {
	// Do something when an incoming call is received
   }
}
Observers

As you might have noticed from the code snippets above, the CallClientObserver and the ChatClientObserver protocols have been heavily modified. Now both protocols expose only three methods notifying you about client state changes.

Let's pretend you were listening for call client state change events in the 2.x.x SDK version like this:


class ViewController: UIViewController {

    func startClients() {
        BandyerSDK.instance().openSession(userId: selectedUserId)
        BandyerSDK.instance().callClient.add(observer: self, queue: .main)
        BandyerSDK.instance().callClient.start()
    }
}

extension ViewController: CallClientObserver {

    func callClientDidStart(_ client: CallClient) {
	// Do something when client started running
    }

    func callClientDidStartReconnecting(_ client: CallClient) {
	// Do something when client starts reconnecting
    }

    func callClientWillResume(_ client: CallClient) {
	// Do something when client is about to resume
    }

    func callClientDidResume(_ client: CallClient) {
	// Do something when client resumed
    }
}

Now in the 3.0.0 version:


class ViewController: UIViewController {

    func startClients() {
        BandyerSDK.instance.callClient.add(observer: self, queue: .main)
    }
}

extension ViewController: CallClientObserver {

    func callClientDidChangeState(_ client: CallClient, oldState: CallClientState, newState: CallClientState) {
	// Do something after client state changed
    }
}

CallViewControllerConfiguration

The CallViewControllerConfiguration object cannot be created directly anymore, you are required to use the builder objects provided

func prepareCallWindow() {
    let config = CallViewControllerConfiguration()

    let filePath = Bundle.main.path(forResource: "SampleVideo_640x360_10mb", ofType: "mp4")

    guard let path = filePath else {
        fatalError("The fake file for the file capturer could not be found")
    }

    let url = URL(fileURLWithPath: path)
    config.fakeCapturerFileURL = url
    config.isFeedbackEnabled = true
    callWindow?.setConfiguration(config)
}

Now in the 3.0.0 version:

func prepareCallWindow() {
    guard let fileURL = Bundle.main.url(forResource: "SampleVideo_640x360_10mb", withExtension: "mp4") else {
        fatalError("The fake file for the file capturer could not be found")
    }    

    let config = CallViewControllerConfigurationBuilder()
                    .withFakeCapturerFileURL(fileURL)
                    .withFeedbackEnabled()
                    .build()
    callWindow?.setConfiguration(config)
}

Starting outgoing calls

When you create an outgoing call you are now required to specify the recording type option using one of the CallOptions object initialisers or factory methods. Every call created or received by the Kaleyra Video SDK has a set of CallOptions, one of this options is the "recordingType" option which determines whether the call is going to be / might be recorded or it won't be recorded.

Let's pretend you were creating an outgoing call with "recording" option enabled in the 2.x.x SDK version like this:

func startOutgoingCall() {
    guard let path = Bundle.main.path(forResource: "SampleVideo_640x360_10mb", ofType: "mp4") else {
        fatalError("The fake file for the file capturer could not be found")
    }

    let config = CallViewControllerConfiguration()
    let url = URL(fileURLWithPath: path)
    config.fakeCapturerFileURL = url
    callWindow?.setConfiguration(config)

    let intent = StartOutgoingCallIntent(callees: aliases,
                                         options: CallOptions(callType: .audioVideo,
                                                              recorded: true,
                                                              duration: 0))
    callWindow?.presentCallViewController(for: intent) { error in
         guard let error = error else { return }

         debugPrint("An error occurred while presenting the call window \(error)")
    }
}

The code snippet above created an outgoing videocall with recording enabled. That call would have been recorded from start to end automatically without the intervention of any user participanting in the call. Now in the 3.0.0 version you can create the same call specifying the CallRecordingType the call in the CallOptions object initialiser:

func startOutgoingCall() {
    guard let path = Bundle.main.path(forResource: "SampleVideo_640x360_10mb", ofType: "mp4") else {
        fatalError("The fake file for the file capturer could not be found")
    }

    let builder = CallViewControllerConfigurationBuilder()
                    .withFakeCapturerFileURL(URL(fileURLWithPath: path))
    callWindow?.setConfiguration(builder.build())

    let intent = StartOutgoingCallIntent(callees: userIDs,
                                         options: CallOptions(callType: .audioVideo,
                                                              recordingType: .automatic,
                                                              duration: 0))
    callWindow?.presentCallViewController(for: intent) { error in
        guard let error = error else { return }
            
        debugPrint("An error occurred while presenting the call window \(error)")            
    }
}