Skip to content

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:

  1. When you set credentials, the SDK checks permission status
  2. If permissions not granted, SDK automatically requests them
  3. availabilityFlow updates to reflect permission status
  4. 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?

  1. Security: Project key never stored on mobile devices
  2. Device Isolation: Compromised device cannot generate credentials for other devices
  3. Device-Specific Keys: Each device gets unique credentials based on device ID
  4. Auditability: Server logs all credential requests
  5. Revocation: Easy to revoke specific device credentials

Implementation architecture

The credential derivation flow involves 3 components:

Mobile device → backend server → access control system

  1. Mobile device obtains device ID

    • Retrieves unique device identifier (UUID, IMEI, installation ID, etc.)
  2. Mobile device requests credential from backend server

    • Sends deviceId and credentialId (e.g., employee ID)
    • Uses secure HTTPS connection
  3. 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
  4. Backend server derives device-specific key

    • Computes: deviceSpecificKey = SHA256(projectKey + deviceId)[0:16]
    • Each device gets a unique key, preventing cross-device credential generation
  5. Backend server creates credential

    • Uses SDK derivation logic to generate credential components
    • Derives keyInitblock, keyComm, and signed credentialBytes
  6. Backend server returns credential components

    • Sends back: keyComm, keyInitblock, credentialBytes
    • Logs request for audit trail
  7. 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

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()
        }
    }
}

Title