Engagement Manager
The MdocEngagementManager is the primary entry point for mDoc operations in the IDK. It manages the complete lifecycle from creating engagements through completing data transfers.
Obtaining the Engagement Manager
The engagement manager is available from the session component:
- Android/Kotlin
- iOS/Swift
val engagementManager = session.component.mdocEngagementManager
let engagementManager = session.component.mdocEngagementManager
Key Properties
The engagement manager provides access to engagement instances and events:
- Android/Kotlin
- iOS/Swift
// Event hub for UI integration (primary integration point)
val eventHub: MdocEventHub = engagementManager.eventHub
// Shared parameters (BLE UUIDs, ephemeral keys)
val sharedParameters: SharedParameters = engagementManager.sharedParameters
// Engagement instances by type (only ONE per type at a time)
val engagementsByType: StateFlow<Map<EngagementType, EngagementInstance>>
// Direct access to specific engagement types
val qrEngagement: StateFlow<EngagementInstance?> = engagementManager.qrEngagement
val nfcEngagement: StateFlow<EngagementInstance?> = engagementManager.nfcEngagement
val toAppEngagement: StateFlow<EngagementInstance?> = engagementManager.toAppEngagement
// Currently active engagement (ONE at a time)
val activeEngagement: StateFlow<EngagementInstance?> = engagementManager.activeEngagement
// Active transfer instances
val transferInstances: StateFlow<Map<Uuid, TransferInstance>>
// Event hub for UI integration (primary integration point)
let eventHub: MdocEventHub = engagementManager.eventHub
// Shared parameters (BLE UUIDs, ephemeral keys)
let sharedParameters: SharedParameters = engagementManager.sharedParameters
// Engagement instances by type (only ONE per type at a time)
let engagementsByType: StateFlow<Map<EngagementType, EngagementInstance>>
// Direct access to specific engagement types
let qrEngagement: StateFlow<EngagementInstance?> = engagementManager.qrEngagement
let nfcEngagement: StateFlow<EngagementInstance?> = engagementManager.nfcEngagement
let toAppEngagement: StateFlow<EngagementInstance?> = engagementManager.toAppEngagement
// Currently active engagement (ONE at a time)
let activeEngagement: StateFlow<EngagementInstance?> = engagementManager.activeEngagement
// Active transfer instances
let transferInstances: StateFlow<Map<Uuid, TransferInstance>>
Engagement Types
The manager supports three engagement types:
| Type | Description |
|---|---|
QR | ISO 18013-5 QR code-based engagement |
NFC | ISO 18013-5 proximity-based engagement |
TO_APP | ISO 18013-7 app-to-app / reverse engagement |
Creating TO_APP Engagement
Create a TO_APP engagement from an incoming URI:
- Android/Kotlin
- iOS/Swift
// Handle incoming mdoc:// URI
val uri = "mdoc://..." // or "mdoc:" or "mdoc-openid4vp://"
val result = engagementManager.toApp(
mdocUri = uri,
autoStart = true // Automatically start the engagement
)
when (result) {
is IdkResult.Success -> {
val engagementInstance = result.value
// Engagement created successfully
}
is IdkResult.Failure -> {
val error = result.error
// Handle error
}
}
// Handle incoming mdoc:// URI
let uri = "mdoc://..." // or "mdoc:" or "mdoc-openid4vp://"
let result = engagementManager.toApp(
mdocUri: uri,
autoStart: true // Automatically start the engagement
)
switch result {
case .success(let engagementInstance):
// Engagement created successfully
case .failure(let error):
// Handle error
}
URI Scheme Patterns
The toApp() method supports three URI patterns:
| Scheme | Description |
|---|---|
mdoc: (opaque) | Classic reverse engagement with BLE/NFC |
mdoc:// (hierarchical) | REST API / Website retrieval |
mdoc-openid4vp:// | OID4VP with OAuth 2.0 |
Working with EngagementInstance
The EngagementInstance represents a single engagement session:
- Android/Kotlin
- iOS/Swift
val engagement: EngagementInstance = engagementManager.qrEngagement.value!!
// Access engagement properties
val id: Uuid = engagement.id
val data: EngagementData = engagement.data
val isActive: StateFlow<Boolean> = engagement.isActive
// Get engagement URI for QR code
val uri: String = engagement.getEngagementUri()
// Get ephemeral key
val ephemeralKey: CoseKeyType = engagement.getEphemeralKey()
// Get device engagement object
val deviceEngagement: CborEncodedItem<DeviceEngagement> = engagement.getDeviceEngagement()
// Get current state
val state: MdocEngagementStateType = engagement.getCurrentState()
// Get available retrieval methods
val retrievalMethods: Set<DeviceRetrievalMethod> = engagement.getRetrievalMethods()
// Check if transfer is initialized
val isTransferReady: Boolean = engagement.isTransferInitialized()
// Subscribe to events
engagement.events.collect { event ->
// Handle engagement events
}
let engagement: EngagementInstance = engagementManager.qrEngagement.value!
// Access engagement properties
let id: Uuid = engagement.id
let data: EngagementData = engagement.data
let isActive: StateFlow<Bool> = engagement.isActive
// Get engagement URI for QR code
let uri: String = try await engagement.getEngagementUri()
// Get ephemeral key
let ephemeralKey: CoseKeyType = try await engagement.getEphemeralKey()
// Get device engagement object
let deviceEngagement: CborEncodedItem<DeviceEngagement> = try await engagement.getDeviceEngagement()
// Get current state
let state: MdocEngagementStateType = engagement.getCurrentState()
// Get available retrieval methods
let retrievalMethods: Set<DeviceRetrievalMethod> = engagement.getRetrievalMethods()
// Check if transfer is initialized
let isTransferReady: Bool = engagement.isTransferInitialized()
// Subscribe to events
for await event in engagement.events {
// Handle engagement events
}
Starting a Transfer
Once an engagement is established, start the transfer to get a TransferManager:
- Android/Kotlin
- iOS/Swift
// Start engagement and get transfer manager
val transferManager: TransferManager = engagement.start()
// Or using Try interface for result-based error handling
val result = engagement.tryOps().start()
when (result) {
is IdkResult.Success -> {
val transferManager = result.value
// Use transfer manager
}
is IdkResult.Failure -> {
val error = result.error
// Handle error
}
}
// Start engagement and get transfer manager
let transferManager: TransferManager = try await engagement.start()
// Or using Try interface for result-based error handling
let result = try await engagement.tryOps().start()
switch result {
case .success(let transferManager):
// Use transfer manager
case .failure(let error):
// Handle error
}
Event Hub Integration
The MdocEventHub is the primary integration point for UI:
- Android/Kotlin
- iOS/Swift
val eventHub: MdocEventHub = engagementManager.eventHub
// Combined stream of all events
eventHub.allEvents.collect { event ->
when (event) {
is MdocEngagementEvent -> handleEngagementEvent(event)
is MdocRetrievalEvent -> handleRetrievalEvent(event)
}
}
// Or subscribe to specific event types
eventHub.engagementEvents.collect { event ->
val state = event.state
val engagementId = event.engagementId
val isActive = event.isActive
// Handle engagement-specific events
}
eventHub.retrievalEvents.collect { event ->
// Handle transfer/retrieval events
}
// Get recent events for debugging
val recentEvents: List<SequencedEvent> = eventHub.getRecentEvents(count = 10)
// Get current state by engagement ID
val statesByEngagement: StateFlow<Map<Uuid, MdocEngagementState>> =
eventHub.getCurrentStateByEngagement()
let eventHub: MdocEventHub = engagementManager.eventHub
// Combined stream of all events
for await event in eventHub.allEvents {
if let engagementEvent = event as? MdocEngagementEvent {
handleEngagementEvent(event: engagementEvent)
} else if let retrievalEvent = event as? MdocRetrievalEvent {
handleRetrievalEvent(event: retrievalEvent)
}
}
// Or subscribe to specific event types
for await event in eventHub.engagementEvents {
let state = event.state
let engagementId = event.engagementId
let isActive = event.isActive
// Handle engagement-specific events
}
// Get recent events for debugging
let recentEvents: [SequencedEvent] = eventHub.getRecentEvents(count: 10)
// Get current state by engagement ID
let statesByEngagement: StateFlow<Map<Uuid, MdocEngagementState>> =
eventHub.getCurrentStateByEngagement()
Closing Engagements
Close engagements when done:
- Android/Kotlin
- iOS/Swift
// Close specific engagement types
engagementManager.closeNfcEngagement()
engagementManager.closeQrEngagement()
engagementManager.closeToAppEngagement()
// Close by instance
engagementManager.closeEngagementByInstance(engagementInstance)
// Close by ID
engagementManager.closeEngagementById(engagementId)
// Close all engagements
engagementManager.closeAll()
// Close specific engagement types
engagementManager.closeNfcEngagement()
engagementManager.closeQrEngagement()
engagementManager.closeToAppEngagement()
// Close by instance
engagementManager.closeEngagementByInstance(engagement: engagementInstance)
// Close by ID
engagementManager.closeEngagementById(id: engagementId)
// Close all engagements
engagementManager.closeAll()
Auto Restart Configuration
Enable automatic cleanup and NFC restart:
- Android/Kotlin
- iOS/Swift
// Enable auto-restart for NFC engagements
engagementManager.enableAutoRestart(backgroundNfcConfig)
// Enable auto-restart for NFC engagements
engagementManager.enableAutoRestart(config: backgroundNfcConfig)
Shared Parameters
The SharedParameters manage parameters shared across engagements:
- Android/Kotlin
- iOS/Swift
val sharedParams: SharedParameters = engagementManager.sharedParameters
// Access BLE UUIDs
val centralClientUuid: StateFlow<Uuid> = sharedParams.bleCentralClientUuid
val peripheralServerUuid: StateFlow<Uuid> = sharedParams.blePeripheralServerUuid
// Access shared ephemeral key
val ephemeralKey: StateFlow<CoseKeyType> = sharedParams.ephemeralKey
// Regenerate parameters for new session
sharedParams.regenerate()
// Use same UUID for both BLE modes
sharedParams.useSameUuidForBothModes()
let sharedParams: SharedParameters = engagementManager.sharedParameters
// Access BLE UUIDs
let centralClientUuid: StateFlow<Uuid> = sharedParams.bleCentralClientUuid
let peripheralServerUuid: StateFlow<Uuid> = sharedParams.blePeripheralServerUuid
// Access shared ephemeral key
let ephemeralKey: StateFlow<CoseKeyType> = sharedParams.ephemeralKey
// Regenerate parameters for new session
sharedParams.regenerate()
// Use same UUID for both BLE modes
sharedParams.useSameUuidForBothModes()
Engagement Lifecycle States
Engagements progress through these states:
| State | Description |
|---|---|
INITIALIZING | Engagement is being set up |
CONNECTING | Establishing transport connection |
CONNECTED | Transport connected, ready for transfer |
TRANSFERRING | Data exchange in progress |
COMPLETED | Transfer completed successfully |
ERROR | An error occurred |
Complete Example
Here's a complete example of handling an incoming mDoc URI:
- Android/Kotlin
- iOS/Swift
class MdocPresentationHandler(
private val engagementManager: MdocEngagementManager,
private val documentProvider: DocumentProvider
) {
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
fun handleIncomingUri(uri: String) {
scope.launch {
// Create engagement from URI
val result = engagementManager.toApp(mdocUri = uri, autoStart = false)
when (result) {
is IdkResult.Success -> {
val engagement = result.value
observeEngagement(engagement)
// Start the engagement
val transferManager = engagement.start()
handleTransfer(transferManager)
}
is IdkResult.Failure -> {
handleError(result.error)
}
}
}
}
private suspend fun observeEngagement(engagement: EngagementInstance) {
engagement.events.collect { event ->
when (event.state) {
is MdocEngagementState.Connected -> {
updateUi("Connected to verifier")
}
is MdocEngagementState.Error -> {
val error = (event.state as MdocEngagementState.Error).error
handleError(error)
}
}
}
}
private suspend fun handleTransfer(transferManager: TransferManager) {
// Receive device request
val deviceRequest = transferManager.receiveDeviceRequest()
// Show consent UI to user
val userApproved = showConsentDialog(deviceRequest)
if (userApproved) {
// Create and send response
val deviceResponse = transferManager.createResponse(
deviceRequest = deviceRequest,
documentProvider = documentProvider
)
transferManager.sendDeviceResponse(deviceResponse)
} else {
// Send error response
val errorResponse = DeviceResponse.Builder()
.withStatus(DeviceResponseStatus(20u)) // User cancelled
.build()
transferManager.sendDeviceResponse(errorResponse)
}
}
fun cleanup() {
engagementManager.closeAll()
scope.cancel()
}
}
class MdocPresentationHandler {
private let engagementManager: MdocEngagementManager
private let documentProvider: DocumentProvider
private var observationTask: Task<Void, Never>?
init(engagementManager: MdocEngagementManager, documentProvider: DocumentProvider) {
self.engagementManager = engagementManager
self.documentProvider = documentProvider
}
func handleIncomingUri(uri: String) async {
// Create engagement from URI
let result = engagementManager.toApp(mdocUri: uri, autoStart: false)
switch result {
case .success(let engagement):
observeEngagement(engagement: engagement)
do {
// Start the engagement
let transferManager = try await engagement.start()
try await handleTransfer(transferManager: transferManager)
} catch {
handleError(error: error)
}
case .failure(let error):
handleError(error: error)
}
}
private func observeEngagement(engagement: EngagementInstance) {
observationTask = Task {
for await event in engagement.events {
switch event.state {
case .connected:
await updateUi(message: "Connected to verifier")
case .error(let error):
handleError(error: error)
default:
break
}
}
}
}
private func handleTransfer(transferManager: TransferManager) async throws {
// Receive device request
let deviceRequest = try await transferManager.receiveDeviceRequest()
// Show consent UI to user
let userApproved = await showConsentDialog(request: deviceRequest)
if userApproved {
// Create and send response
let deviceResponse = try await transferManager.createResponse(
deviceRequest: deviceRequest,
documentProvider: documentProvider
)
try await transferManager.sendDeviceResponse(deviceResponse: deviceResponse)
} else {
// Send error response
let errorResponse = DeviceResponse.Builder()
.withStatus(DeviceResponseStatus(value: 20)) // User cancelled
.build()
try await transferManager.sendDeviceResponse(deviceResponse: errorResponse)
}
}
func cleanup() {
observationTask?.cancel()
engagementManager.closeAll()
}
}