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