Skip to main content

Route 2: Call the native SDKs

Choose this route when Swift or Kotlin owns the Link Mobile feature, such as an existing native access screen or service inside a React Native application. Your native code calls the SDK and consumes its callbacks. You integrate and maintain each platform's SDK setup.

For JavaScript access, use Route 1 (recommended) or build Route 3 on top of this setup. Route 2 does not install @last-lock/link-mobile, register a TurboModule, or provide the supplied library's JavaScript methods and events.

Compare routes and complete the app allowlisting first. Use the production SDK packages and matching release versions from the native setup guides. Configure Bluetooth permissions, signing, and attestation on physical devices for each platform you support.

iOS: install the native SDK​

Follow the iOS setup guide to install the production Swift package and link the matching LinkMobile and LinkMobileTypes products to your app target. In a React Native app, open the .xcworkspace after installing Pods. Set deployment targets to your selected SDK release's requirements, add Bluetooth usage descriptions, configure signing, and enable App Attest.

The production SDK repository distributes LinkMobile and bridge as XCFrameworks, and LinkMobileTypes as Swift source. Use the Swift package to download the matching binaries and compile LinkMobileTypes with its dependencies; there is no separate LinkMobileTypes.xcframework. Adding only the two XCFrameworks manually does not supply the complete integration. Do not also add standalone copies of the frameworks when using the Swift package. The native SDK does not expose a JavaScript API by itself.

Swift example: own discovery in native code​

Add this application-owned controller to the iOS app target. It retains one native Bridge and listener, waits for Bluetooth readiness, and reports discoveries through an application callback. Use the production hostname plane.lastlock.com and service UUID FC5A shown below. Change them only when Last Lock instructs you to use a different deployment.

// ExampleLinkSession.swift
import Foundation
import LinkMobile
import UIKit

@objc(ExampleLinkSession)
final class ExampleLinkSession: BlueLinkEventListener {
@objc static let shared = ExampleLinkSession()
@objc var onDiscovery: ((String, Int32) -> Void)?

private let serviceUUID = "FC5A"
private var bridge: Bridge?
private var startRequested = false
private var bluetoothReady = false

// Call on the main thread, from a visible native screen or action.
@objc func start() throws {
precondition(Thread.isMainThread)
guard UIApplication.shared.applicationState == .active else {
throw NSError(domain: "ExampleLink", code: 1, userInfo: [
NSLocalizedDescriptionKey: "Start Link Mobile while the app is visible."
])
}
startRequested = true
if bridge == nil {
let instance = Bridge(
serverAddress: "plane.lastlock.com", uuid: serviceUUID
)
bridge = instance
instance.getBlueLink().addListener(listener: self, prependListener: false)
}
if bluetoothReady {
bridge?.start(UUID: serviceUUID)
}
}

override func onBLEStateChanged(newState: Bool) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.bluetoothReady = newState
if newState && self.startRequested {
self.bridge?.start(UUID: self.serviceUUID)
}
}
}

override func onDeviceDiscovered(
device: BluetoothDevice,
advertisementData: BluetoothAdvertisement,
rssi: Int32,
signalStrength: SignalStrength?,
connectionStatus: BlueLinkDeviceConnectionStatus
) {
let identifier = String(device.deviceId)
DispatchQueue.main.async { [weak self] in
self?.onDiscovery?(identifier, rssi)
}
}
}

From your native screen, install the callback before starting and present any error to the user:

// Inside a native screen's start action, on the main thread:
ExampleLinkSession.shared.onDiscovery = { identifier, rssi in
print("Native iOS discovered lock \(identifier), RSSI \(rssi)")
// Update your native view model, keyed by identifier.
}
do {
try ExampleLinkSession.shared.start()
} catch {
print("Unable to start Link Mobile: \(error.localizedDescription)")
}

This example keeps the SDK owner for the app lifetime. Clear the screen's callback when it is disposed; use weak captures when referencing the screen. The native SDK also reacts to Bluetooth state changes. A successful start() call accepts the startup request; observe discovery and Proxy state separately. Use the iOS guide for credential delegates and complete native workflows.

Android: install the native SDK​

Follow the Android setup guide for Maven credentials, repositories, manifest permissions, and release requirements. Use the repository and coordinates together for the distribution supplied to your team. For example, the production dependency is:

// app/build.gradle.kts — retain the existing React Native plugins and settings.
dependencies {
implementation("com.lastlock:bridge-android:ANDROID_SDK_VERSION")
}

Replace ANDROID_SDK_VERSION with the exact published native SDK version. Route 2 selects its native SDK releases directly; it does not inherit the versions pinned by a React Native package.

Initialize once in your existing React Native MainApplication, preserving its React Native host initialization:

import com.lastlock.bridge.Bridge

// Add inside MainApplication.onCreate(), after super.onCreate():
Bridge.init(this, "plane.lastlock.com")

Do not replace MainApplication with a plain Application or initialize a second SDK owner in an activity.

Kotlin example: own discovery in native code​

Add this application-owned session in the app module. The native screen must request runtime permissions before calling it. This example checks Bluetooth readiness and uses Bridge.start() to run BLE and Proxy in a foreground service, which posts a notification and can continue working after the app moves to the background.

// ExampleLinkSession.kt
package com.example.link

import android.Manifest
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import androidx.core.content.ContextCompat
import com.lastlock.bridge.Bridge
import com.lastlock.bridge.bluelink.BlueLinkDeviceConnectionStatus
import com.lastlock.bridge.bluelink.BluetoothAdvertisement
import com.lastlock.bridge.bluelink.BluetoothDevice
import com.lastlock.bridge.bluelink.interfaces.BlueLinkEventListener
import com.lastlock.bridge.bluelink.interfaces.SignalStrength

object ExampleLinkSession {
// Assign and clear this callback on the main thread.
var onDiscovery: ((String, Int) -> Unit)? = null
private val main = Handler(Looper.getMainLooper())
private var attached = false
private val listener = object : BlueLinkEventListener() {
override fun onDeviceDiscovered(
device: BluetoothDevice,
advertisementData: BluetoothAdvertisement,
rssi: Int,
signalStrength: SignalStrength,
connectionStatus: BlueLinkDeviceConnectionStatus,
) {
val identifier = device.deviceId.toString()
main.post { onDiscovery?.invoke(identifier, rssi) }
}
}

// Caller must be a foreground activity/action; Bridge.init ran in MainApplication.
fun startFromForeground(context: Context) {
check(Looper.myLooper() == Looper.getMainLooper())
val permissions = if (Build.VERSION.SDK_INT >= 31) {
listOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
} else {
listOf(Manifest.permission.ACCESS_FINE_LOCATION)
}
check(permissions.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}) { "Grant Bluetooth permissions before starting Link Mobile." }

val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
check(manager.adapter?.isEnabled == true) { "Turn on Bluetooth before starting." }
if (!attached) {
Bridge.getBlueLink().addListener(listener)
attached = true
}
Bridge.start()
}
}

From a native activity's visible action, after permissions have been granted:

ExampleLinkSession.onDiscovery = { identifier, rssi ->
android.util.Log.d("Link", "Native Android discovered lock $identifier, RSSI $rssi")
// Update your native view model, keyed by identifier.
}
try {
ExampleLinkSession.startFromForeground(this)
} catch (error: Exception) {
android.util.Log.e("Link", "Unable to start Link Mobile", error)
// Display the error and allow retry after the user resolves it.
}

Clear the screen's callback on disposal. The singleton retains its SDK listener for the app lifetime. On older Android releases, also meet the native guide's location-service requirements. Declare the matching permissions in the manifest; requesting them at runtime alone is insufficient.

Call startFromForeground while the app is visible to start the session. You do not need to call it again solely because the app returns to the foreground; the foreground service can continue running while the app is in the background. Configure the service and manifest as described in the Android setup guide, including the foreground-service notification. Startup returning does not prove Proxy is connected or an unlock succeeded.

What this route gives your React Native app​

The examples discover locks and handle results in native code. A React Native JavaScript screen cannot import ExampleLinkSession or call Bridge directly.

Keep this route if the feature remains native. If JavaScript needs discovery events or operations, prefer Route 1 for the maintained API, or use these sessions as the native owners for your own TurboModule in Route 3. Add credential handling using the native platform guides; these discovery examples do not implement unlock or firmware update flows.