Skip to main content
Version: v0.13

Events and UI Integration

The IDK provides a comprehensive 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:

val engagementManager = session.component.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)
}
}

Event Types

Engagement Events

Engagement events track the state of connection establishment:

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

Retrieval Events

Retrieval events track the data transfer phase:

eventHub.retrievalEvents.collect { event ->
// Handle transfer/retrieval specific events
println("Retrieval event: $event")
}

Tracking State by Engagement

Get current state organized by engagement ID:

// 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")
}
}
}

Recent Events for Debugging

Access recent events for debugging and diagnostics:

// Get recent events
val recentEvents: List<SequencedEvent> = eventHub.getRecentEvents(count = 10)

for (sequencedEvent in recentEvents) {
println("Event #${sequencedEvent.sequence}: ${sequencedEvent.event}")
}

When a verifier requests data, present a consent dialog:

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

Engagement Instance Events

Each engagement instance also provides its own event stream:

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

Transfer Instance Tracking

Track active transfer instances:

// Track all active transfers
lifecycleScope.launch {
engagementManager.transferInstances.collect { transfers ->
for ((id, transfer) in transfers) {
println("Transfer $id active")
}
}
}

Error Handling

Handle errors gracefully in your UI:

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

Best Practices

When building mDoc UIs:

  • 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 allEvents for comprehensive state tracking
  • Test on real devices: Bluetooth and NFC behavior varies across devices