@@ -88,6 +88,9 @@ class MdocEngagementPresenterImpl(
8888 // Collect active engagement from manager - single source of truth
8989 val activeEngagement by engagementManager.activeEngagement.collectAsState()
9090
91+ // Track QR scanner mode
92+ var showQrScanner by remember { mutableStateOf(false ) }
93+
9194 // Log state on every recomposition to understand state transitions
9295 log.debug(" === PRESENTER RECOMPOSITION ===" )
9396 log.debug(" Engagement Manager Instance: ${engagementManager.hashCode()} " )
@@ -152,6 +155,7 @@ class MdocEngagementPresenterImpl(
152155 when (event) {
153156 UiStateEvent .ShowQr -> {
154157 log.debug(" User clicked 'Show QR' - creating QR engagement via manager" )
158+ showQrScanner = false
155159 presenterScope.launch {
156160 val engagementResult = engagementManager.createEngagement {
157161 engagement { qr {} }
@@ -166,7 +170,15 @@ class MdocEngagementPresenterImpl(
166170 }
167171 }
168172
173+ UiStateEvent .ShowQrScanner -> {
174+ log.debug(" User clicked 'Scan QR' - enabling QR scanner mode" )
175+ showQrScanner = true
176+ // Don't create engagement yet - wait for QR scan
177+ // The scanner will trigger onQrScanned when a code is detected
178+ }
179+
169180 UiStateEvent .Stopped -> {
181+ showQrScanner = false
170182 log.debug(" Stopped event - closing all engagements and navigating back" )
171183 presenterScope.launch {
172184 engagementManager.closeAll()
@@ -188,6 +200,92 @@ class MdocEngagementPresenterImpl(
188200 }
189201 }
190202
203+ // Handle QR code scanned for reverse engagement
204+ val onQrScanned = remember(presenterScope) {
205+ { scannedData: String ->
206+ log.info(" 📷 QR SCANNED! Length: ${scannedData.length} , First 30 chars: '${scannedData.take(30 )} '" )
207+ log.debug(" Full scanned data: $scannedData " )
208+ when {
209+ // 18013-7 website via deeplink/QR
210+ scannedData.startsWith(" mdoc://" ) -> {
211+ log.info(" VALID mdoc:// URI - Initiating toApp with website retrieval (18013-7)" )
212+ showQrScanner = false
213+ presenterScope.launch {
214+ runCatching {
215+ engagementManager.toApp(scannedData)
216+ .onSuccess {
217+ log.debug(" toApp (website) initiated successfully" )
218+ }
219+ .onFailure { error ->
220+ log.error(
221+ " toApp (website) failed: ${error.message} " ,
222+ exception = (error as ? com.sphereon.core.api.error.IdkError )?.exception
223+ )
224+ showQrScanner = false
225+ }
226+ }.onFailure { e ->
227+ log.error(" Exception calling toApp (website)" , exception = e)
228+ showQrScanner = false
229+ }
230+ }
231+ }
232+
233+ // 18013-5 reverse engagement with BLE transfer
234+ scannedData.startsWith(" mdoc:" ) && ! scannedData.startsWith(" mdoc://" ) && ! scannedData.startsWith(" mdoc-openid4vp://" ) -> {
235+ log.info(" VALID mdoc: URI - Initiating toApp with BLE retrieval (18013-5)" )
236+ showQrScanner = false
237+ presenterScope.launch {
238+ runCatching {
239+ engagementManager.toApp(scannedData)
240+ .onSuccess {
241+ log.debug(" toApp (BLE) initiated successfully" )
242+ }
243+ .onFailure { error ->
244+ log.error(
245+ " toApp (BLE) failed: ${error.message} " ,
246+ exception = (error as ? com.sphereon.core.api.error.IdkError )?.exception
247+ )
248+ showQrScanner = false
249+ }
250+ }.onFailure { e ->
251+ log.error(" Exception calling toApp (BLE)" , exception = e)
252+ showQrScanner = false
253+ }
254+ }
255+ }
256+
257+ // OpenID4VP
258+ scannedData.startsWith(" mdoc-openid4vp://" ) -> {
259+ log.info(" VALID mdoc-openid4vp:// URI - Initiating toApp with OID4VP retrieval" )
260+ showQrScanner = false
261+ presenterScope.launch {
262+ runCatching {
263+ engagementManager.toApp(scannedData)
264+ .onSuccess {
265+ log.debug(" toApp (OID4VP) initiated successfully" )
266+ }
267+ .onFailure { error ->
268+ log.error(
269+ " toApp (OID4VP) failed: ${error.message} " ,
270+ exception = (error as ? com.sphereon.core.api.error.IdkError )?.exception
271+ )
272+ showQrScanner = false
273+ }
274+ }.onFailure { e ->
275+ log.error(" Exception calling toApp (OID4VP)" , exception = e)
276+ showQrScanner = false
277+ }
278+ }
279+ }
280+
281+ else -> {
282+ log.warn(" ⚠️ Invalid QR code: Expected 'mdoc://', 'mdoc:', or 'mdoc-openid4vp://' - got prefix: ${scannedData.take(20 )} " )
283+ }
284+ }
285+ Unit
286+ }
287+ }
288+
191289 val onContinue = remember(presenterScope) {
192290 { selector: MapDrivenDocRequestSelector ->
193291 log.debug(" Document selection confirmed, starting sharing" )
@@ -244,26 +342,38 @@ class MdocEngagementPresenterImpl(
244342
245343 // Build model based purely on SessionUiState - it's the source of truth
246344 // Special case: If TERMINAL state but no active engagement AND we never had an engagement,
247- // this is stale state from a previous session - treat as INITIAL
345+ // this is stale state from a previous session - treat as INITIAL with toggle UI
248346 val model = when {
249347 sessionState.phase == UiPhase .TERMINAL && activeEngagement == null && ! hasHadEngagement.value -> {
250- log.debug(" >>> Stale TERMINAL state detected (never had engagement in this session) - treating as INITIAL" )
251- MdocEngagementPresenter .Model .Initial (onEvent)
348+ log.debug(" >>> Stale TERMINAL state detected (never had engagement in this session) - returning Initial with toggle" )
349+ MdocEngagementPresenter .Model .Initial (
350+ showQr = false ,
351+ showQrScanner = showQrScanner,
352+ onQrScanned = onQrScanned,
353+ onStateEvent = onEvent
354+ )
252355 }
253356
254357 // If phase is ENGAGEMENT but there's no active engagement, it's stale state - treat as INITIAL
255358 sessionState.phase == UiPhase .ENGAGEMENT && activeEngagement == null -> {
256- log.debug(" >>> Stale ENGAGEMENT state detected (no active engagement) - treating as INITIAL" )
257- MdocEngagementPresenter .Model .Initial (onEvent)
359+ log.debug(" >>> Stale ENGAGEMENT state detected (no active engagement) - returning Initial with toggle" )
360+ MdocEngagementPresenter .Model .Initial (
361+ showQr = false ,
362+ showQrScanner = showQrScanner,
363+ onQrScanned = onQrScanned,
364+ onStateEvent = onEvent
365+ )
258366 }
259367
260368 sessionState.phase == UiPhase .ENGAGEMENT -> {
261- log.debug(" >>> Returning Model: ENGAGEMENT (showQr=${sessionState.qrMode == QrMode .DISPLAY } )" )
369+ log.debug(" >>> Returning Model: ENGAGEMENT (showQr=${sessionState.qrMode == QrMode .DISPLAY } , showQrScanner= $showQrScanner )" )
262370 // Show QR or NFC prompt based on SessionUiState
263371 MdocEngagementPresenter .Model .Engagement (
264372 qrImage = if (sessionState.qrMode == QrMode .DISPLAY ) qrImage else null ,
265373 engagementEvent = null ,
266374 showQr = sessionState.qrMode == QrMode .DISPLAY ,
375+ showQrScanner = showQrScanner,
376+ onQrScanned = onQrScanned,
267377 onStateEvent = onEvent
268378 )
269379 }
@@ -330,14 +440,24 @@ class MdocEngagementPresenterImpl(
330440
331441 null -> {
332442 log.debug(" >>> Returning Model: INITIAL (phase=TERMINAL, terminalOutcome=null)" )
333- MdocEngagementPresenter .Model .Initial (onEvent)
443+ MdocEngagementPresenter .Model .Initial (
444+ showQr = false ,
445+ showQrScanner = showQrScanner,
446+ onQrScanned = onQrScanned,
447+ onStateEvent = onEvent
448+ )
334449 }
335450 }
336451 }
337452
338453 else -> {
339454 log.debug(" >>> Returning Model: INITIAL (phase=${sessionState.phase} )" )
340- MdocEngagementPresenter .Model .Initial (onEvent)
455+ MdocEngagementPresenter .Model .Initial (
456+ showQr = false ,
457+ showQrScanner = showQrScanner,
458+ onQrScanned = onQrScanned,
459+ onStateEvent = onEvent
460+ )
341461 }
342462 }
343463
0 commit comments