Mobile ID SDK common integration patterns
Permission handling flow
The SDK provides both automatic and manual permission handling.
Automatic permission request (default behavior)
The SDK automatically handles permissions for you:
- When you set
credentials, the SDK checks permission status - If permissions not granted, SDK automatically requests them
availabilityFlowupdates to reflect permission status- No manual intervention required in most cases
// This triggers automatic permission check and request
sdk.credentials = listOf(credential)
// Monitor the result via availability
sdk.onAvailabilityChange = { availability ->
when (availability) {
AvailabilityStates.OK -> // Permissions granted, SDK ready
AvailabilityStates.PERMISSIONS_DENIED -> // User denied
AvailabilityStates.PERMISSIONS_PERMANENTLY_DENIED -> // Permanently denied
}
}
Recommendation: Let the SDK handle permissions automatically unless you have specific UX requirements.
Manual permission control (optional)
Use manual control when you want explicit control over permission flow:
Use cases:
- Show explanation dialog before requesting permissions
- Retry after user initially denied
- Request permissions before setting credentials
- Custom permission explanation UI
Example: Pre-request with explanation
fun requestPermissionsWithExplanation() {
lifecycleScope.launch {
// Show custom explanation dialog first
showDialog("We need Bluetooth to connect to access control readers")
// Then request permissions
val granted = sdk.requestPermissions()
if (granted) {
// Proceed to set credentials
sdk.credentials = listOf(credential)
} else {
// Handle denial
handlePermissionDenied()
}
}
}
Example: Retry after denial
sdk.onAvailabilityChange = { availability ->
when (availability) {
AvailabilityStates.PERMISSIONS_DENIED -> {
// Show explanation and retry
showExplanationDialog(
onRetry = {
lifecycleScope.launch {
sdk.requestPermissions()
}
}
)
}
}
}
Handling different permission states
fun handlePermissionState(availability: Availability) {
when (availability) {
AvailabilityStates.PERMISSIONS_REQUIRED -> {
// Transient state - SDK is requesting
showLoadingIndicator()
}
AvailabilityStates.PERMISSIONS_DENIED -> {
// User denied - can request again
showRetryButton {
lifecycleScope.launch {
sdk.requestPermissions()
}
}
}
AvailabilityStates.PERMISSIONS_PERMANENTLY_DENIED -> {
// Must use settings
showSettingsDialog {
lifecycleScope.launch {
sdk.openPermissionSettings()
}
}
}
AvailabilityStates.OK -> {
// All good
hidePermissionUI()
}
}
}
iOS permission flow
func handleAvailability(_ availability: String) {
let states = AvailabilityStates.shared
switch availability {
case states.PERMISSIONS_DENIED, states.PERMISSIONS_REQUIRED:
// SDK auto-requests or manually retry
Task {
let granted = try? await sdk.requestPermissions()
if granted == false {
showPermissionExplanation()
}
}
case states.PERMISSIONS_PERMANENTLY_DENIED:
// Show alert with settings option
showAlert(
title: "Permissions Required",
message: "Please enable Bluetooth in Settings",
primaryButton: .default(Text("Open Settings")) {
Task {
try? await sdk.openPermissionSettings()
}
},
secondaryButton: .cancel()
)
case states.OK:
// Ready to use
break
default:
break
}
}
Server-side credential derivation (security best practice)
STRONGLY RECOMMENDED: Derive credentials on your backend server rather than on mobile devices.
Why server-side derivation?
- Security: Project key never stored on mobile devices
- Device Isolation: Compromised device cannot generate credentials for other devices
- Device-Specific Keys: Each device gets unique credentials based on device ID
- Auditability: Server logs all credential requests
- Revocation: Easy to revoke specific device credentials
Implementation architecture
The credential derivation flow involves 3 components:
Mobile device → backend server → access control system
-
Mobile device obtains device ID
- Retrieves unique device identifier (UUID, IMEI, installation ID, etc.)
-
Mobile device requests credential from backend server
- Sends
deviceIdandcredentialId(e.g., employee ID) - Uses secure HTTPS connection
- Sends
-
Backend server loads project key
- Retrieves project master key from secure storage (AWS Secrets Manager, Azure Key Vault, HSM)
- Project key never transmitted to mobile device
-
Backend server derives device-specific key
- Computes:
deviceSpecificKey = SHA256(projectKey + deviceId)[0:16] - Each device gets a unique key, preventing cross-device credential generation
- Computes:
-
Backend server creates credential
- Uses SDK derivation logic to generate credential components
- Derives
keyInitblock,keyComm, and signedcredentialBytes
-
Backend server returns credential components
- Sends back:
keyComm,keyInitblock,credentialBytes - Logs request for audit trail
- Sends back:
-
Mobile device uses credential
- Stores credential components securely (Keychain/EncryptedSharedPreferences)
- Uses credential for BLE communication with access control readers
Backend implementation (pseudocode)
# Backend API Endpoint
@app.post("/api/credentials/derive")
async def derive_credential(request: CredentialRequest):
"""
Derives a device-specific credential from the project master key.
Args:
deviceId: Unique device identifier (UUID, IMEI, etc.)
credentialId: User/credential identifier (employee ID, etc.)
Returns:
Credential components (keyComm, keyInitblock, credentialBytes)
"""
# 1. Validate request
if not is_valid_device(request.deviceId):
raise Unauthorized("Invalid device")
if not is_authorized_user(request.credentialId):
raise Unauthorized("User not authorized")
# 2. Get project master key from secure storage
# (e.g., AWS Secrets Manager, Azure Key Vault, HSM)
projectKey = get_project_key_from_secure_storage() # 16 bytes
# 3. Derive device-specific key
# This ensures each device has unique key
deviceSpecificData = projectKey + request.deviceId.encode('utf-8')
deviceSpecificKey = SHA256(deviceSpecificData)[0:16] # First 16 bytes
# 4. Create credential using SDK logic
# (replicate Credential.create() logic on server)
# Step 4a: Derive keyInitblock
keyInitblock = SHA256(deviceSpecificKey)[0:16]
# Step 4b: Build credential structure
credentialIdBytes = request.credentialId.encode('ascii')
headerSize = 2 + len(credentialIdBytes)
header = bytes([headerSize, 0x10, len(credentialIdBytes)]) + credentialIdBytes
# Step 4c: Sign with AES-CMAC
cmac = AES_CMAC(header, deviceSpecificKey)
credentialBytes = header + cmac
# Step 4d: Derive keyComm
divdata = SHA256(credentialBytes)[0:16]
keyComm = SHA256(deviceSpecificKey + divdata)[0:16]
# 5. Log the request (for audit trail)
log_credential_request(
deviceId=request.deviceId,
credentialId=request.credentialId,
timestamp=now()
)
# 6. Return credential components
return {
"keyComm": base64_encode(keyComm),
"keyInitblock": base64_encode(keyInitblock),
"credentialBytes": base64_encode(credentialBytes)
}
### Activating/deactivating credentials
```kotlin
// Activate
fun activateCredential(credential: Credential) {
sdk.credentials = listOf(credential)
}
// Deactivate (stops BLE protocol)
fun deactivateCredential() {
sdk.credentials = emptyList()
}
// Switch credentials (automatically stops and restarts)
fun switchCredential(newCredential: Credential) {
sdk.credentials = listOf(newCredential) // Old one stopped, new one started
}
Reader management
Observing reader updates
class ReaderManager(private val sdk: MobileIdSdk) {
private val _readers = MutableStateFlow<List<Reader>>(emptyList())
val readers: StateFlow<List<Reader>> = _readers.asStateFlow()
init {
sdk.onReadersUpdate = {
_readers.value = sdk.readers
}
}
}
// In Compose
@Composable
fun ReaderScreen(readerManager: ReaderManager) {
val readers by readerManager.readers.collectAsState()
LazyColumn {
items(readers) { reader ->
ReaderCard(reader)
}
}
}
Displaying readers in UI
@Composable
fun ReaderCard(reader: Reader) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { reader.trigger() }
.padding(8.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = reader.displayName,
style = MaterialTheme.typography.titleMedium
)
Text(
text = "Tap to open",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
}
Icon(
imageVector = Icons.Default.Lock,
contentDescription = null
)
}
}
}
Remote trigger dialog
@Composable
fun RemoteTriggerDialog(
readers: List<Reader>,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Available Readers") },
text = {
if (readers.isEmpty()) {
Text("No readers in range")
} else {
LazyColumn {
items(readers) { reader ->
TextButton(
onClick = {
reader.trigger()
onDismiss()
},
modifier = Modifier.fillMaxWidth()
) {
Text(reader.displayName)
}
}
}
}
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text("Close")
}
}
)
}
Handling disconnections
sdk.onReadersUpdate = {
val currentReaders = sdk.readers
if (currentReaders.isEmpty()) {
// All readers disconnected
showToast("No readers in range")
} else {
// Readers available
showToast("${currentReaders.size} reader(s) available")
}
}
Logging and support integration
Implementing sendLogs() - strongly recommended
Why this is critical:
- BALTECH support cannot diagnose issues without logs
- Simple one-method integration
- Dramatically improves support quality
Basic integration:
// Add "Contact Support" button anywhere in your app
Button(onClick = {
lifecycleScope.launch {
try {
sdk.sendLogs()
} catch (e: IllegalStateException) {
Toast.makeText(context, "No logs available", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Toast.makeText(context, "Failed to send logs", Toast.LENGTH_SHORT).show()
}
}
}) {
Text("Contact Support")
}
With custom message:
fun reportIssue(issueDescription: String) {
lifecycleScope.launch {
try {
sdk.sendLogs(
subject = "MyApp - Issue Report",
message = "User reported: $issueDescription\n\nAdditional context: ..."
)
} catch (e: Exception) {
// Handle error
}
}
}
Automatic on errors:
sdk.onAvailabilityChange = { availability ->
if (availability == AvailabilityStates.UNAUTHORIZED ||
availability == AvailabilityStates.UNSUPPORTED) {
// Show dialog offering to contact support
showDialog(
title = "Issue Detected",
message = "Would you like to send logs to support?",
positiveButton = "Send Logs" to {
lifecycleScope.launch {
sdk.sendLogs()
}
}
)
}
}
Implementing log viewer - optional
Android/Compose:
@Composable
fun LogViewerScreen(sdk: MobileIdSdk) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("SDK Logs") },
actions = {
IconButton(onClick = {
// Send logs to support
lifecycleScope.launch {
sdk.sendLogs()
}
}) {
Icon(Icons.Default.Email, "Send to Support")
}
}
)
}
) { padding ->
Box(modifier = Modifier.padding(padding)) {
sdk.createLogView()
}
}
}
iOS/Swift:
struct LogViewerView: View {
let sdk: MobileIdSdk
var body: some View {
NavigationView {
LogViewControllerWrapper(
viewController: sdk.createLogViewController()
)
.navigationTitle("SDK Logs")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: sendLogs) {
Image(systemName: "envelope")
}
}
}
}
}
private func sendLogs() {
Task {
try? await sdk.sendLogs()
}
}
}