Events and UI Integration
The IDK provides an event system for building responsive user interfaces during mDoc presentations. This guide covers how to observe and handle events using the MdocEventHub.
Event Hub
The MdocEventHub is the central point for observing engagement and transfer events:
- Android/Kotlin
- iOS/Swift
val engagementManager = session.graph.mdocEngagementManager
val eventHub: MdocEventHub = engagementManager.eventHub
// Combined stream of all events
eventHub.allEvents.collect { event ->
when (event) {
is MdocEngagementEvent -> handleEngagementEvent(event)
is MdocRetrievalEvent -> handleRetrievalEvent(event)
}
}
let engagementManager = session.graph.mdocEngagementManager
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)
}
}
Event Types
Engagement Events
Engagement events track the state of connection establishment:
- Android/Kotlin
eventHub.engagementEvents.collect { event ->
val state: MdocEngagementState = event.state
val engagementId: Uuid = event.engagementId
val isActive: Boolean = event.isActive
when (state) {
is MdocEngagementState.Initializing -> {
showMessage("Setting up engagement...")
}
is MdocEngagementState.Connecting -> {
showMessage("Connecting to verifier...")
}
is MdocEngagementState.Connected -> {
showMessage("Connected to verifier")
}
is MdocEngagementState.Transferring -> {
showMessage("Transferring data...")
}
is MdocEngagementState.Completed -> {
showMessage("Transfer completed")
}
is MdocEngagementState.Error -> {
val error = state.error
showError("Error: ${error.message}")
}
}
}
MdocEngagementEvent Subtypes
The following table lists all MdocEngagementEvent subtypes and their meanings:
| Event | Description |
|---|---|
Initializing | Engagement created |
Start | Engagement started |
QrShow | QR code should be displayed |
QrHide | QR code should be hidden |
NfcEngagement | NFC engagement detected |
RestApiEngagement | REST API engagement started |
Connecting | Transport connection in progress |
Connected | Transport connected |
Disconnected | Transport disconnected |
Canceled | User or system canceled |
Error | Error occurred |
Transfer | Transfer phase started |
Debug | Debug information |
Data | Data exchange (direction: IN/OUT) |
- iOS/Swift
for await event in eventHub.engagementEvents {
let state = event.state
let engagementId = event.engagementId
let isActive = event.isActive
switch state {
case .initializing:
showMessage(text: "Setting up engagement...")
case .connecting:
showMessage(text: "Connecting to verifier...")
case .connected:
showMessage(text: "Connected to verifier")
case .transferring:
showMessage(text: "Transferring data...")
case .completed:
showMessage(text: "Transfer completed")
case .error(let error):
showError(text: "Error: \(error.message)")
}
}
Retrieval Events
Retrieval events track the data transfer phase:
- Android/Kotlin
- iOS/Swift
eventHub.retrievalEvents.collect { event ->
// Handle transfer/retrieval specific events
println("Retrieval event: $event")
}
for await event in eventHub.retrievalEvents {
// Handle transfer/retrieval specific events
print("Retrieval event: \(event)")
}
MdocRetrievalEvent Subtypes
The following table lists all MdocRetrievalEvent subtypes and their meanings:
| Event | Description |
|---|---|
Initializing | Transfer initialized |
TransmissionTypeSelected | Transport method selected |
SessionEstablishmentReceived | Session setup received from reader |
DeviceRequestReceived | Request from reader received |
DeviceResponseSent | Response sent to reader |
Completed | Transfer completed |
Error | Transfer error |
Tracking State by Engagement
Get current state organized by engagement ID:
- Android/Kotlin
- iOS/Swift
// Get current state by engagement ID
val statesByEngagement: StateFlow<Map<Uuid, MdocEngagementState>> =
eventHub.getCurrentStateByEngagement()
lifecycleScope.launch {
statesByEngagement.collect { states ->
for ((id, state) in states) {
println("Engagement $id: $state")
}
}
}
// Get current state by engagement ID
let statesByEngagement = eventHub.getCurrentStateByEngagement()
Task {
for await states in statesByEngagement {
for (id, state) in states {
print("Engagement \(id): \(state)")
}
}
}
Recent Events for Debugging
Access recent events for debugging and diagnostics:
- Android/Kotlin
- iOS/Swift
// Get recent events
val recentEvents: List<SequencedEvent> = eventHub.getRecentEvents(count = 10)
for (sequencedEvent in recentEvents) {
println("Event #${sequencedEvent.sequence}: ${sequencedEvent.event}")
}
// Get recent events
let recentEvents: [SequencedEvent] = eventHub.getRecentEvents(count: 10)
for sequencedEvent in recentEvents {
print("Event #\(sequencedEvent.sequence): \(sequencedEvent.event)")
}
Building a Consent UI
When a verifier requests data, present a consent dialog:
- Android/Kotlin
- iOS/Swift
class MdocConsentHandler(
private val engagementManager: MdocEngagementManager
) {
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
fun start() {
scope.launch {
engagementManager.eventHub.allEvents.collect { event ->
if (event is MdocEngagementEvent && event.state is MdocEngagementState.Connected) {
// Get the active engagement
val engagement = engagementManager.activeEngagement.value
if (engagement != null) {
handleConnectedEngagement(engagement)
}
}
}
}
}
private suspend fun handleConnectedEngagement(engagement: EngagementInstance) {
// Start transfer to receive request
val transferManager = engagement.start()
// Receive the device request
val deviceRequest = transferManager.receiveDeviceRequest()
// Show consent dialog
val approved = showConsentDialog(deviceRequest)
if (approved) {
// Create and send response
val deviceResponse = transferManager.createResponse(
deviceRequest = deviceRequest,
documentProvider = documentProvider
)
transferManager.sendDeviceResponse(deviceResponse)
} else {
// Send cancellation
val errorResponse = DeviceResponse.Builder()
.withStatus(DeviceResponseStatus(20u)) // User cancelled
.build()
transferManager.sendDeviceResponse(errorResponse)
}
}
}
class MdocConsentHandler {
private let engagementManager: MdocEngagementManager
private var observationTask: Task<Void, Never>?
init(engagementManager: MdocEngagementManager) {
self.engagementManager = engagementManager
}
func start() {
observationTask = Task {
for await event in engagementManager.eventHub.allEvents {
if let engagementEvent = event as? MdocEngagementEvent,
case .connected = engagementEvent.state {
// Get the active engagement
if let engagement = engagementManager.activeEngagement.value {
await handleConnectedEngagement(engagement: engagement)
}
}
}
}
}
private func handleConnectedEngagement(engagement: EngagementInstance) async {
do {
// Start transfer to receive request
let transferManager = try await engagement.start()
// Receive the device request
let deviceRequest = try await transferManager.receiveDeviceRequest()
// Show consent dialog
let approved = await showConsentDialog(request: deviceRequest)
if approved {
// Create and send response
let deviceResponse = try await transferManager.createResponse(
deviceRequest: deviceRequest,
documentProvider: documentProvider
)
try await transferManager.sendDeviceResponse(deviceResponse: deviceResponse)
} else {
// Send cancellation
let errorResponse = DeviceResponse.Builder()
.withStatus(DeviceResponseStatus(value: 20)) // User cancelled
.build()
try await transferManager.sendDeviceResponse(deviceResponse: errorResponse)
}
} catch {
handleError(error: error)
}
}
}
Engagement Instance Events
Each engagement instance also provides its own event stream:
- Android/Kotlin
- iOS/Swift
val engagement = engagementManager.activeEngagement.value
engagement?.events?.collect { event ->
val state = event.state
val isActive = engagement.isActive.value
when (state) {
is MdocEngagementState.Connected -> {
updateUI("Connected - ready to transfer")
}
is MdocEngagementState.Error -> {
updateUI("Error occurred")
}
}
}
if let engagement = engagementManager.activeEngagement.value {
for await event in engagement.events {
let state = event.state
let isActive = engagement.isActive.value
switch state {
case .connected:
updateUI(message: "Connected - ready to transfer")
case .error:
updateUI(message: "Error occurred")
default:
break
}
}
}
Transfer Instance Tracking
Track active transfer instances:
- Android/Kotlin
- iOS/Swift
// Track all active transfers
lifecycleScope.launch {
engagementManager.transferInstances.collect { transfers ->
for ((id, transfer) in transfers) {
println("Transfer $id active")
}
}
}
// Track all active transfers
Task {
for await transfers in engagementManager.transferInstances {
for (id, transfer) in transfers {
print("Transfer \(id) active")
}
}
}
Simplified UI State
The event hub also provides a simplified, deterministic state model suitable for binding directly to UI frameworks:
eventHub.sessionEvents: SharedFlow<SessionEvent>emits discrete session lifecycle events.eventHub.sessionState: StateFlow<SessionUiState>exposes a singleSessionUiStatevalue that always reflects the current phase of the session.
Because sessionState is a StateFlow, your UI layer can collect it and render a single view per state, without tracking multiple event streams or reconciling race conditions.
Error Handling
Handle errors gracefully in your UI:
- Android/Kotlin
- iOS/Swift
eventHub.engagementEvents.collect { event ->
if (event.state is MdocEngagementState.Error) {
val error = (event.state as MdocEngagementState.Error).error
val message = when {
error.message.contains("timeout", ignoreCase = true) ->
"Connection timed out. Please try again."
error.message.contains("cancelled", ignoreCase = true) ->
"Operation was cancelled."
error.message.contains("bluetooth", ignoreCase = true) ->
"Bluetooth error. Please check your connection."
else -> error.message
}
showErrorDialog(message)
// Clean up
engagementManager.closeAll()
}
}
for await event in eventHub.engagementEvents {
if case .error(let error) = event.state {
let message: String
if error.message.lowercased().contains("timeout") {
message = "Connection timed out. Please try again."
} else if error.message.lowercased().contains("cancelled") {
message = "Operation was cancelled."
} else if error.message.lowercased().contains("bluetooth") {
message = "Bluetooth error. Please check your connection."
} else {
message = error.message
}
showErrorDialog(message: message)
// Clean up
engagementManager.closeAll()
}
}
Filtering Events
The event hub provides helper methods for filtering events by state order, which is useful for narrowing down to specific lifecycle phases:
eventHub.eventsInStateRange(minOrder, maxOrder): returns events whose state order falls within the given range (inclusive). Useful for isolating a specific phase of the engagement or transfer lifecycle.eventHub.finalStateEvents(): returns only terminal-state events (order >= 200). These represent completed, canceled, or errored engagements.eventHub.activeTransferEvents(): returns events in the active transfer range (order 100-199). Useful for tracking in-progress data exchanges.eventHub.onTransferCompletion: a convenience property that emits once when a transfer reaches a completion state. Ideal for triggering post-transfer logic such as navigation or cleanup.
Tips
- Provide clear feedback. Users should understand whether they need to show a QR code, tap their device, or wait for a connection.
- Make consent explicit. Show exactly what data will be shared and allow users to decline.
- Handle all states. Success, error, and cancellation should all have appropriate UI responses.
- Use the event hub. Subscribe to
allEventsfor full state tracking. - Test on real devices. Bluetooth and NFC behavior varies across devices.