Integrating the Tapkey Mobile SDK

Warning

This documentation refers to version 2.3.x.x and later. This documentation may not be appropriate for older versions. Take a look at the upgrade section for upgrading to the latest version of the Tapkey Mobile SDK.

Sample App

A Tapkey Mobile SDK sample for iOS app is available on GitHub.

Requirements

The Tapkey Mobile SDK is written in Swift and is distributed as a compiled static Swift 5.0 library. Therefore, the target App has to be built at least with Xcode 10.2 and Swift 5.0. Because of missing ABI stability of Swift 4.2 and older, older Xcode versions are not supported.

Adding the Tapkey Mobile SDK via CocoaPods

The Tapkey Mobile SDK is published as a Pod in an private Podspec repository. To tell CocoaPods where to find the artefacts add the Tapkey Podspec repository, add the official Tapkey Podspec repository as a source to the beginning of the Podfile and add TapkeyMobileLib as a dependency to your target app.

source 'https://github.com/tapkey/TapkeyCocoaPods'
source 'https://github.com/CocoaPods/Specs.git'

target 'App' do
    pod 'TapkeyMobileLib', '2.3.x.x'
end

Bootstrap the Tapkey Mobile SDK

  1. Import the TapkeyMobileLib module.
  2. Use the TKMServiceFactoryBuilder to create an instance of the TKMServiceFactory in the willFinishLaunchingWithOptions callback in the app delegate.
import TapkeyMobileLib
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    private var TKMServiceFactory: TKMServiceFactory!

    func application(_ application: UIApplication,
                     willFinishLaunchingWithOptions launchOptions:
                     [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {

        // Build service factory
        self.TKMServiceFactory = TKMServiceFactoryBuilder()
            .build()
    }
}

Polling for Data

It is recommended to use Background App Refresh to poll the Tapkey Trust Service for notifications. This is configured in the app delegate:

// Background App Refresh
func application(
        _ application: UIApplication,
        performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {

    // Configure the Tapkey SDK to poll for notifications.
    // Run the code via runAsyncInBackground to prevent the app from sleeping while fetching is in progress.
    runAsyncInBackground(application, promise:
        TKMServiceFactory.notificationManager
                .pollForNotificationsAsync(cancellationToken: TKMCancellationTokens.None)
                .finallyOnUi {
                    completionHandler(UIBackgroundFetchResult.newData)
                }
    )
}
While the Background App Refresh is periodically looking for updated keys, it is recommended to poll for updates whenever updates are expected. NotificationManager#pollForNotificationsAsync() can be used to manually check for updates. Manually checking for updated keys is recommended in the following situations:

  • After a user has been logged in to the SDK.
  • After a new grant has been issued for a logged-in user.
  • After a grant that affects the current user has been modified or revoked.
  • After grants, affecting locks that one of the logged-in users has access to, have been revoked. The logged-in users might receive updated revocation information.

Logging In Users

The SDK takes a Tapkey access token to log in a user. Retrieving such an access token is part of the implementing application. Usually, either the Token Exchange or Authentication Code with PKCE grant type will be used. For more information about authenticating against Tapkey, take a look at the authentication section.

Once an access token has been obtained, it can be used to log in a user as outlined below:

TKMServiceFactory.userManager.logInAsync(accessToken: "eyJhbGciOiJSUz...O-YbBq8F7086rQi-kEbERp4dA3r0WonpHnmYcXEnA", cancellationToken: ct)
    .continueOnUi(userId -> {
        // User logged in, use user's ID here.
    })

Subsequently, userManager.users will yield a list of IDs of all logged-in users. In most applications, only one user will be logged in at a time.

val userId = userManager.users[0]
// Fetch information about the user using the ID

Note that a token refresh handler must be registered with the TKMServiceFactory prior to logging in any users. This can be done using the TKMServiceFactoryBuilder's setTokenRefreshHandler(tokenRefreshHandler: TKMTokenRefreshHandler) method.

// In the `willFinishLaunchingWithOptions` callback in the app delegate:

// Build service factory
self.tapkeyServiceFactory = TKMServiceFactoryBuilder()
    .setTokenRefreshHandler(SampleTokenRefreshHandler())
    .build()
// Sample token refresh handler implementation

import RxSwift
import TapkeyMobileLib

class SampleTokenRefreshHandler: TKMTokenRefreshHandler {

    func refreshAuthenticationAsync(userId: String, cancellationToken: TKMCancellationToken) -> TKMPromise<String> {
        let promiseSource = TKMPromiseSource<String>()

        let accessToken = getNewTapkeyAccessTokenFromCustomApplicationLogic()

        // OK:
        promiseSource.setResult(value)

        // Or, in case no access token could be obtained:
        promiseSource.setError(TKMError(errorDescriptor: TKMErrorDescriptor(
                code: TKMAuthenticationHandlerErrorCodes.TokenRefreshFailed,
                message: "No new token can be obtained.",
                details: nil)))

        return promiseSource.promise
    }

    func onRefreshFailed(userId: String) {
        /*
         * Will be called if re-authentication failed and a manual call to
         * userManager.updateAccessToken() is required.
         */
    }
}

Searching for Nearby Locks

Searching for nearby locks can be done via the TKMBleLockScanner. An instance of TKMBleLockScanner can be obtained from the TKMServiceFactory.

TKMBleLockScanner bleLockScanner = tapkeyServiceFactory.bleLockScanner

// Returns an observer registration to stop scanning once finished.
bleScanObserverRegistration = bleLockScanner.startForegroundScan()

To observe locks entering and leaving the range of this device, the TKMBleLockScanner's observable can be used.

// Listen for Tapkey locks coming into or leaving range.
nearbyLocksObserverRegistration = bleLockScanner.observable
    .addObserver(locks -> {
        // Handle nearby locks here, e.g. present them to the user.
    })

// Alternatively, the locks property can be used to get a list of nearby locks in this moment.
nearbyLocks = bleLockScanner.locks

Make sure to stop scanning if the app does not required information about nearby locks anymore:

// Stop scanning for nearby locks.
if self.nearbyLocksObserverRegistration != nil {
    self.nearbyLocksObserverRegistration!.close()
    self.nearbyLocksObserverRegistration = nil
}

Triggering a Lock

After a nearby lock was found with the TKMBleLockScanner, it can be opened using the trigger lock command, which is available from the TKMCommandExecutionFacade. Any command inside the command execution facade, requires a TLCP connection to a lock. The following example uses TKMBleLockCommunicator to establish a TCLP connection via BLE.

// Use the BLE lock communicator to send a command to the lock
return bleLockCommunicator.executeCommandAsync(
                bluetoothAddress: bluetoothAddress,
                physicalLockId: physicalLockId,
                commandFunc: { tlcpConnection in
                    // Pass the TLCP connection to the command execution facade
                    commandExecutionFacade!.triggerLockAsync(
                            tlcpConnection,
                            cancellationToken: TKMCancellationTokens.None
                    )
                },
                cancellationToken: TKMCancellationTokens.None)

    // Process the command's result
    .continueOnUi({ commandResult in
        let code: TKMCommandResult.TKMCommandResultCode = commandResult?.code ??
                TKMCommandResult.TKMCommandResultCode.technicalError

        switch code {
        case TKMCommandResult.TKMCommandResultCode.ok:
            return true
        default:
            return false
        }
    })
    .catchOnUi({ (_: Error) -> Bool in
        NSLog("Trigger lock failed")
        return false
    })