Skip to content

iOS Integration

This guide covers the iOS-specific integration of the Tapkey Mobile SDK for local vehicle access via BLE.

  • Partner backend integration is available (assignments + webhooks)
  • SDK Key available in the flinkey Portal (UAT)
  • Mobile access context can be requested from your backend
  • Real iOS test devices available (do not rely only on simulators for BLE)
Library Purpose
Tapkey Mobile SDK Mobile key handling, SDK login, BLE access flow
WITTE Mobile Library Box ID conversion, Box Command data, box feedback parsing
AppAuth for iOS OAuth / token exchange
source 'https://github.com/tapkey/TapkeyCocoaPods'
source 'https://cdn.cocoapods.org/'
use_frameworks!
target 'PartnerApp' do
pod 'TapkeyMobileLib', '~> VERSION'
pod 'witte-mobile-library', '~> VERSION'
pod 'AppAuth'
end

Sample resources: witte-mobile-sample-ios | witte-mobile-library-for-objc

<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to communicate with the vehicle access device.</string>

Request Bluetooth access before starting the access flow. Handle denied permissions gracefully — missing Bluetooth descriptions may cause iOS to prevent BLE communication or terminate the app.

sequenceDiagram
    autonumber
    actor customer as End customer
    participant app as iOS app
    participant backend as Partner backend
    participant api as flinkey API
    participant tapkey as Tapkey Trust Service
    participant sdk as Tapkey Mobile SDK

    customer->>app: Sign in
    app->>backend: Request mobile access context
    backend->>api: Get IdToken
    api-->>backend: Return IdToken
    backend-->>app: Return IdToken
    app->>tapkey: Token exchange via AppAuth
    tapkey-->>app: Tapkey access token
    app->>sdk: SDK login
import AppAuth
let request = OIDTokenRequest(
configuration: configuration,
grantType: "http://tapkey.net/oauth/token_exchange",
authorizationCode: nil,
redirectURL: nil,
clientID: "wma-native-mobile-app",
clientSecret: nil,
scopes: ["register:mobiles", "read:user", "handle:keys"],
refreshToken: nil,
codeVerifier: nil,
additionalParameters: [
"provider": "wma.oauth",
"subject_token_type": "jwt",
"subject_token": idToken,
"audience": "tapkey_api",
"requested_token_type": "access_token"
]
)
OIDAuthorizationService.perform(request) { response, error in
guard let accessToken = response?.accessToken else { return }
// Use accessToken for SDK login.
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
TapkeyMobileSdk.initialize { builder in
builder.setTokenRefreshHandler(PartnerTokenRefreshHandler())
}
return true
}
}
class PartnerTokenRefreshHandler: TKMTokenRefreshHandler {
func refreshAuthenticationAsync(userId: String,
cancellationToken: TKMCancellationToken) -> TKMPromise<String> {
let source = TKMPromiseSource<String>()
requestFreshTapkeyAccessToken(userId: userId) { result in
switch result {
case .success(let token): source.setResult(token)
case .failure: source.setError(TKMError(errorDescriptor:
TKMErrorDescriptor(code: TKMAuthenticationHandlerErrorCodes.TokenRefreshFailed,
message: "No new token can be obtained.", details: nil)))
}
}
return source.promise
}
func onRefreshFailed(userId: String) {
// Handle refresh failure — may require user to sign in again.
}
}
TapkeyMobileSdk.serviceFactory.userManager
.logInAsync(accessToken: accessToken, cancellationToken: TKMCancellationTokens.none)
.continueOnUi { userId in
// SDK login successful.
}
.catchOnUi { error in
// SDK login failed.
return nil
}
let bleLockScanner = TapkeyMobileSdk.serviceFactory.bleLockScanner
let scanRegistration = bleLockScanner.startForegroundScan()
let nearbyLocksRegistration = bleLockScanner.observable.addObserver { locks in
// Match discovered locks against expected physical lock ID.
}
// Stop scanning when done
scanRegistration.close()
nearbyLocksRegistration.close()
let boxId = "..." // from mobile access context
let physicalLockId = WDBoxIdConverter().toPhysicalLockId(withBoxId: boxId)
let scanner = TapkeyMobileSdk.serviceFactory.bleLockScanner
let communicator = TapkeyMobileSdk.serviceFactory.bleLockCommunicator
let commandFacade = TapkeyMobileSdk.serviceFactory.commandExecutionFacade
guard let lock = scanner.locks.first(where: { $0.physicalLockId == physicalLockId }) else {
// Handle box not found.
return
}
let timeout = TKMCancellationTokens.fromTimeout(timeoutMs: 15000)
communicator.executeCommandAsync(
peripheralId: lock.peripheralId,
physicalLockId: physicalLockId,
commandFunc: { tlcpConnection in
commandFacade.triggerLockAsync(tlcpConnection, cancellationToken: timeout)
},
cancellationToken: timeout
)
.continueOnUi { result in
switch result?.code {
case .ok: return true // Access succeeded
default: return false // Access failed
}
}
.catchOnUi { error in return false }
Error User-facing Technical
Missing access context Ask to retry Check backend assignment flow
Token exchange failed Ask to retry Log sanitized error
Bluetooth disabled Ask to enable BT Do not start scanning
Permission missing Ask to grant Show iOS permission guidance
Box not found Move closer to vehicle Stop scan after timeout
Command failed Show access failed Store sanitized diagnostics
  • App authenticates customer against partner backend
  • App requests and receives mobile access context
  • AppAuth token exchange succeeds
  • Tapkey SDK login succeeds
  • Token refresh handler is implemented
  • Info.plist Bluetooth usage descriptions configured
  • Bluetooth permission handling implemented
  • BLE scanning starts and stops correctly
  • App discovers the flinkey BLE Box
  • Access command succeeds
  • Box feedback is handled
  • Bluetooth disabled / box not found states handled
  • No credentials, tokens or digital keys in logs
  • Real-device testing completed