diff --git a/common-feature/src/main/java/eu/europa/ec/commonfeature/config/RequestUriConfig.kt b/common-feature/src/main/java/eu/europa/ec/commonfeature/config/RequestUriConfig.kt index cf5cddd5..a65b0aaf 100644 --- a/common-feature/src/main/java/eu/europa/ec/commonfeature/config/RequestUriConfig.kt +++ b/common-feature/src/main/java/eu/europa/ec/commonfeature/config/RequestUriConfig.kt @@ -24,8 +24,8 @@ import eu.europa.ec.uilogic.serializer.UiSerializableParser import eu.europa.ec.uilogic.serializer.adapter.SerializableTypeAdapter sealed interface PresentationMode { - data class OpenId4Vp(val uri: String) : PresentationMode - data object Ble : PresentationMode + data class OpenId4Vp(val uri: String, val initiatorRoute: String) : PresentationMode + data class Ble(val initiatorRoute: String) : PresentationMode } data class RequestUriConfig( @@ -46,7 +46,10 @@ data class RequestUriConfig( fun RequestUriConfig.toDomainConfig(): PresentationControllerConfig { return when (mode) { - is PresentationMode.Ble -> PresentationControllerConfig.Ble - is PresentationMode.OpenId4Vp -> PresentationControllerConfig.OpenId4VP(mode.uri) + is PresentationMode.Ble -> PresentationControllerConfig.Ble(mode.initiatorRoute) + is PresentationMode.OpenId4Vp -> PresentationControllerConfig.OpenId4VP( + mode.uri, + mode.initiatorRoute + ) } } \ No newline at end of file diff --git a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/biometric/BiometricScreen.kt b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/biometric/BiometricScreen.kt index d0f4b40c..7dd6523a 100644 --- a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/biometric/BiometricScreen.kt +++ b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/biometric/BiometricScreen.kt @@ -52,7 +52,7 @@ import eu.europa.ec.uilogic.extension.resetBackStack import eu.europa.ec.uilogic.extension.setBackStackFlowCancelled import eu.europa.ec.uilogic.extension.setBackStackFlowSuccess import eu.europa.ec.uilogic.navigation.CommonScreens -import eu.europa.ec.uilogic.navigation.DashboardScreens +import eu.europa.ec.uilogic.navigation.helper.handleDeepLinkAction import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect @@ -122,19 +122,22 @@ fun BiometricScreen( } is Effect.Navigation.Deeplink -> { - - context.cacheDeepLink(navigationEffect.link) - - if (navigationEffect.isPreAuthorization) { - navController.navigate(DashboardScreens.Dashboard.screenRoute) { - popUpTo(CommonScreens.Biometric.screenRoute) { inclusive = true } + navigationEffect.routeToPop?.let { route -> + context.cacheDeepLink(navigationEffect.link) + if (navigationEffect.isPreAuthorization) { + navController.navigate(route) { + popUpTo(CommonScreens.Biometric.screenRoute) { + inclusive = true + } + } + } else { + navController.popBackStack( + route = route, + inclusive = false + ) } - } else { - navController.popBackStack( - route = DashboardScreens.Dashboard.screenRoute, - inclusive = false - ) - } + } ?: handleDeepLinkAction(navController, navigationEffect.link) + } is Effect.Navigation.Pop -> navController.popBackStack() diff --git a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/biometric/BiometricViewModel.kt b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/biometric/BiometricViewModel.kt index 37d4d289..ae96b178 100644 --- a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/biometric/BiometricViewModel.kt +++ b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/biometric/BiometricViewModel.kt @@ -80,7 +80,12 @@ sealed class Effect : ViewSideEffect { ) : Navigation() data object LaunchBiometricsSystemScreen : Navigation() - data class Deeplink(val link: Uri, val isPreAuthorization: Boolean) : Navigation() + data class Deeplink( + val link: Uri, + val isPreAuthorization: Boolean, + val routeToPop: String? = null + ) : Navigation() + data object Pop : Navigation() data object Finish : Navigation() } @@ -272,7 +277,8 @@ class BiometricViewModel( is NavigationType.Deeplink -> Effect.Navigation.Deeplink( nav.link.toUri(), - viewState.value.config.isPreAuthorization + viewState.value.config.isPreAuthorization, + nav.routeToPop ) is NavigationType.Pop -> Effect.Navigation.Pop diff --git a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/qr_scan/QrScanViewModel.kt b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/qr_scan/QrScanViewModel.kt index b29ab321..ae54b39b 100644 --- a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/qr_scan/QrScanViewModel.kt +++ b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/qr_scan/QrScanViewModel.kt @@ -128,7 +128,12 @@ class QrScanViewModel( arguments = generateComposableArguments( mapOf( RequestUriConfig.serializedKeyName to uiSerializer.toBase64( - RequestUriConfig(PresentationMode.OpenId4Vp(uri = scanResult)), + RequestUriConfig( + PresentationMode.OpenId4Vp( + uri = scanResult, + initiatorRoute = DashboardScreens.Dashboard.screenRoute + ) + ), RequestUriConfig.Parser ) ) diff --git a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/success/SuccessScreen.kt b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/success/SuccessScreen.kt index e9e55305..ee374585 100644 --- a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/success/SuccessScreen.kt +++ b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/success/SuccessScreen.kt @@ -53,7 +53,6 @@ import eu.europa.ec.uilogic.config.ConfigNavigation import eu.europa.ec.uilogic.config.NavigationType import eu.europa.ec.uilogic.extension.cacheDeepLink import eu.europa.ec.uilogic.navigation.CommonScreens -import eu.europa.ec.uilogic.navigation.DashboardScreens import eu.europa.ec.uilogic.navigation.StartupScreens import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow @@ -96,10 +95,12 @@ fun SuccessScreen( is Effect.Navigation.DeepLink -> { context.cacheDeepLink(navigationEffect.link) - navController.popBackStack( - route = DashboardScreens.Dashboard.screenRoute, - inclusive = false - ) + navigationEffect.routeToPop?.let { + navController.popBackStack( + route = it, + inclusive = false + ) + } ?: navController.popBackStack() } is Effect.Navigation.Pop -> navController.popBackStack() diff --git a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/success/SuccessViewModel.kt b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/success/SuccessViewModel.kt index 96d01767..51279289 100644 --- a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/success/SuccessViewModel.kt +++ b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/success/SuccessViewModel.kt @@ -54,7 +54,8 @@ sealed class Effect : ViewSideEffect { data object Pop : Navigation() data class DeepLink( - val link: Uri + val link: Uri, + val routeToPop: String? ) : Navigation() } } @@ -108,7 +109,8 @@ class SuccessViewModel( } is NavigationType.Deeplink -> Effect.Navigation.DeepLink( - nav.link.toUri() + nav.link.toUri(), + nav.routeToPop ) is NavigationType.Pop, NavigationType.Finish -> Effect.Navigation.Pop diff --git a/core-logic/src/main/java/eu/europa/ec/corelogic/controller/WalletCorePresentationController.kt b/core-logic/src/main/java/eu/europa/ec/corelogic/controller/WalletCorePresentationController.kt index 888095b7..a49d4bee 100644 --- a/core-logic/src/main/java/eu/europa/ec/corelogic/controller/WalletCorePresentationController.kt +++ b/core-logic/src/main/java/eu/europa/ec/corelogic/controller/WalletCorePresentationController.kt @@ -45,9 +45,11 @@ import org.koin.core.annotation.Scope import org.koin.core.annotation.Scoped import java.net.URI -sealed class PresentationControllerConfig { - data class OpenId4VP(val uri: String) : PresentationControllerConfig() - data object Ble : PresentationControllerConfig() +sealed class PresentationControllerConfig(val initiatorRoute: String) { + data class OpenId4VP(val uri: String, val initiator: String) : + PresentationControllerConfig(initiator) + + data class Ble(val initiator: String) : PresentationControllerConfig(initiator) } sealed class TransferEventPartialState { @@ -120,6 +122,14 @@ interface WalletCorePresentationController { * */ val verifierName: String? + /** + * Who started the presentation + * */ + val initiatorRoute: String + + /** + * Set [PresentationControllerConfig] + * */ fun setConfig(config: PresentationControllerConfig) /** @@ -187,9 +197,16 @@ class WalletCorePresentationControllerImpl( override var disclosedDocuments: DisclosedDocuments? = null private set + override var verifierName: String? = null private set + override val initiatorRoute: String + get() { + val config = requireInit { _config } + return config.initiatorRoute + } + override fun setConfig(config: PresentationControllerConfig) { _config = config } diff --git a/core-logic/src/main/java/eu/europa/ec/corelogic/util/Constants.kt b/core-logic/src/main/java/eu/europa/ec/corelogic/util/Constants.kt index 959a2711..1fce2409 100644 --- a/core-logic/src/main/java/eu/europa/ec/corelogic/util/Constants.kt +++ b/core-logic/src/main/java/eu/europa/ec/corelogic/util/Constants.kt @@ -18,4 +18,5 @@ package eu.europa.ec.corelogic.util object CoreActions { const val VCI_RESUME_ACTION = "vci.resume.eudi.action" + const val VCI_DYNAMIC_PRESENTATION = "vci.dynamic.presentation.eudi.action" } \ No newline at end of file diff --git a/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/ui/dashboard/DashboardViewModel.kt b/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/ui/dashboard/DashboardViewModel.kt index b47a712c..7a7162cc 100644 --- a/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/ui/dashboard/DashboardViewModel.kt +++ b/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/ui/dashboard/DashboardViewModel.kt @@ -304,7 +304,12 @@ class DashboardViewModel( generateComposableArguments( mapOf( RequestUriConfig.serializedKeyName to uiSerializer.toBase64( - RequestUriConfig(PresentationMode.OpenId4Vp(uri.toString())), + RequestUriConfig( + PresentationMode.OpenId4Vp( + uri.toString(), + DashboardScreens.Dashboard.screenRoute + ) + ), RequestUriConfig.Parser ) ) @@ -366,7 +371,7 @@ class DashboardViewModel( arguments = generateComposableArguments( mapOf( RequestUriConfig.serializedKeyName to uiSerializer.toBase64( - RequestUriConfig(PresentationMode.Ble), + RequestUriConfig(PresentationMode.Ble(DashboardScreens.Dashboard.screenRoute)), RequestUriConfig.Parser ) ) diff --git a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/add/AddDocumentScreen.kt b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/add/AddDocumentScreen.kt index 626000dd..3af5f525 100644 --- a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/add/AddDocumentScreen.kt +++ b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/add/AddDocumentScreen.kt @@ -132,8 +132,18 @@ fun AddDocumentScreen( viewModel.setEvent(Event.Init(context.getPendingDeepLink())) } - SystemBroadcastReceiver(action = CoreActions.VCI_RESUME_ACTION) { - viewModel.setEvent(Event.OnResumeIssuance) + SystemBroadcastReceiver( + actions = listOf( + CoreActions.VCI_RESUME_ACTION, + CoreActions.VCI_DYNAMIC_PRESENTATION + ) + ) { + when (it?.action) { + CoreActions.VCI_RESUME_ACTION -> viewModel.setEvent(Event.OnResumeIssuance) + CoreActions.VCI_DYNAMIC_PRESENTATION -> it.extras?.getString("uri")?.let { link -> + viewModel.setEvent(Event.OnDynamicPresentation(link)) + } + } } } diff --git a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/add/AddDocumentViewModel.kt b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/add/AddDocumentViewModel.kt index 3fa65e18..aa0b3ccd 100644 --- a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/add/AddDocumentViewModel.kt +++ b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/add/AddDocumentViewModel.kt @@ -22,12 +22,15 @@ import androidx.lifecycle.viewModelScope import eu.europa.ec.authenticationlogic.controller.authentication.DeviceAuthenticationResult import eu.europa.ec.commonfeature.config.IssuanceFlowUiConfig import eu.europa.ec.commonfeature.config.OfferUiConfig +import eu.europa.ec.commonfeature.config.PresentationMode import eu.europa.ec.commonfeature.config.QrScanFlow import eu.europa.ec.commonfeature.config.QrScanUiConfig +import eu.europa.ec.commonfeature.config.RequestUriConfig import eu.europa.ec.commonfeature.model.DocumentOptionItemUi import eu.europa.ec.corelogic.controller.AddSampleDataPartialState import eu.europa.ec.corelogic.controller.IssuanceMethod import eu.europa.ec.corelogic.controller.IssueDocumentPartialState +import eu.europa.ec.corelogic.di.getOrCreatePresentationScope import eu.europa.ec.corelogic.model.DocType import eu.europa.ec.corelogic.model.DocumentIdentifier import eu.europa.ec.issuancefeature.interactor.document.AddDocumentInteractor @@ -45,6 +48,7 @@ import eu.europa.ec.uilogic.mvi.ViewState import eu.europa.ec.uilogic.navigation.CommonScreens import eu.europa.ec.uilogic.navigation.DashboardScreens import eu.europa.ec.uilogic.navigation.IssuanceScreens +import eu.europa.ec.uilogic.navigation.PresentationScreens import eu.europa.ec.uilogic.navigation.helper.DeepLinkType import eu.europa.ec.uilogic.navigation.helper.generateComposableArguments import eu.europa.ec.uilogic.navigation.helper.generateComposableNavigationLink @@ -72,6 +76,7 @@ sealed class Event : ViewEvent { data object Pop : Event() data object OnPause : Event() data object OnResumeIssuance : Event() + data class OnDynamicPresentation(val uri: String) : Event() data object Finish : Event() data object DismissError : Event() data class IssueDocument( @@ -147,6 +152,31 @@ class AddDocumentViewModel( is Event.OnResumeIssuance -> setState { copy(isLoading = true) } + + is Event.OnDynamicPresentation -> { + getOrCreatePresentationScope() + setEffect { + Effect.Navigation.SwitchScreen( + generateComposableNavigationLink( + PresentationScreens.PresentationRequest, + generateComposableArguments( + mapOf( + RequestUriConfig.serializedKeyName to uiSerializer.toBase64( + RequestUriConfig( + PresentationMode.OpenId4Vp( + event.uri, + IssuanceScreens.AddDocument.screenRoute + ) + ), + RequestUriConfig.Parser + ) + ) + ) + ), + inclusive = false + ) + } + } } } @@ -394,6 +424,15 @@ class AddDocumentViewModel( } } + DeepLinkType.EXTERNAL -> { + setEffect { + Effect.Navigation.OpenDeepLinkAction( + deepLinkUri = uri, + arguments = null + ) + } + } + else -> {} } } diff --git a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/offer/DocumentOfferScreen.kt b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/offer/DocumentOfferScreen.kt index 56ead4ab..c7b36630 100644 --- a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/offer/DocumentOfferScreen.kt +++ b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/offer/DocumentOfferScreen.kt @@ -51,7 +51,6 @@ import eu.europa.ec.uilogic.component.content.ContentTitle import eu.europa.ec.uilogic.component.content.GradientEdge import eu.europa.ec.uilogic.component.content.ScreenNavigateAction import eu.europa.ec.uilogic.component.utils.LifecycleEffect -import eu.europa.ec.uilogic.component.utils.OneTimeLaunchedEffect import eu.europa.ec.uilogic.component.utils.SPACING_EXTRA_LARGE import eu.europa.ec.uilogic.component.utils.SPACING_MEDIUM import eu.europa.ec.uilogic.component.utils.SPACING_SMALL @@ -61,8 +60,9 @@ import eu.europa.ec.uilogic.component.wrap.WrapModalBottomSheet import eu.europa.ec.uilogic.component.wrap.WrapPrimaryButton import eu.europa.ec.uilogic.component.wrap.WrapSecondaryButton import eu.europa.ec.uilogic.extension.cacheDeepLink -import eu.europa.ec.uilogic.navigation.DashboardScreens +import eu.europa.ec.uilogic.extension.getPendingDeepLink import eu.europa.ec.uilogic.navigation.IssuanceScreens +import eu.europa.ec.uilogic.navigation.helper.handleDeepLinkAction import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect @@ -130,12 +130,25 @@ fun DocumentOfferScreen( viewModel.setEvent(Event.OnPause) } - OneTimeLaunchedEffect { - viewModel.setEvent(Event.Init) + LifecycleEffect( + lifecycleOwner = LocalLifecycleOwner.current, + lifecycleEvent = Lifecycle.Event.ON_RESUME + ) { + viewModel.setEvent(Event.Init(context.getPendingDeepLink())) } - SystemBroadcastReceiver(action = CoreActions.VCI_RESUME_ACTION) { - viewModel.setEvent(Event.OnResumeIssuance) + SystemBroadcastReceiver( + actions = listOf( + CoreActions.VCI_RESUME_ACTION, + CoreActions.VCI_DYNAMIC_PRESENTATION + ) + ) { + when (it?.action) { + CoreActions.VCI_RESUME_ACTION -> viewModel.setEvent(Event.OnResumeIssuance) + CoreActions.VCI_DYNAMIC_PRESENTATION -> it.extras?.getString("uri")?.let { link -> + viewModel.setEvent(Event.OnDynamicPresentation(link)) + } + } } } @@ -301,11 +314,13 @@ private fun handleNavigationEffect( } is Effect.Navigation.DeepLink -> { - context.cacheDeepLink(navigationEffect.link) - navController.popBackStack( - route = DashboardScreens.Dashboard.screenRoute, - inclusive = false - ) + navigationEffect.routeToPop?.let { + context.cacheDeepLink(navigationEffect.link) + navController.popBackStack( + route = it, + inclusive = false + ) + } ?: handleDeepLinkAction(navController, navigationEffect.link) } is Effect.Navigation.Pop -> navController.popBackStack() diff --git a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/offer/DocumentOfferViewModel.kt b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/offer/DocumentOfferViewModel.kt index 81bf1ef0..7a9bbf11 100644 --- a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/offer/DocumentOfferViewModel.kt +++ b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/offer/DocumentOfferViewModel.kt @@ -22,7 +22,10 @@ import androidx.lifecycle.viewModelScope import eu.europa.ec.businesslogic.extension.toUri import eu.europa.ec.commonfeature.config.OfferCodeUiConfig import eu.europa.ec.commonfeature.config.OfferUiConfig +import eu.europa.ec.commonfeature.config.PresentationMode +import eu.europa.ec.commonfeature.config.RequestUriConfig import eu.europa.ec.commonfeature.ui.request.model.DocumentItemUi +import eu.europa.ec.corelogic.di.getOrCreatePresentationScope import eu.europa.ec.issuancefeature.interactor.document.DocumentOfferInteractor import eu.europa.ec.issuancefeature.interactor.document.IssueDocumentsInteractorPartialState import eu.europa.ec.issuancefeature.interactor.document.ResolveDocumentOfferInteractorPartialState @@ -36,8 +39,11 @@ import eu.europa.ec.uilogic.mvi.ViewEvent import eu.europa.ec.uilogic.mvi.ViewSideEffect import eu.europa.ec.uilogic.mvi.ViewState import eu.europa.ec.uilogic.navigation.IssuanceScreens +import eu.europa.ec.uilogic.navigation.PresentationScreens +import eu.europa.ec.uilogic.navigation.helper.DeepLinkType import eu.europa.ec.uilogic.navigation.helper.generateComposableArguments import eu.europa.ec.uilogic.navigation.helper.generateComposableNavigationLink +import eu.europa.ec.uilogic.navigation.helper.hasDeepLink import eu.europa.ec.uilogic.serializer.UiSerializer import kotlinx.coroutines.launch import org.koin.android.annotation.KoinViewModel @@ -60,10 +66,11 @@ data class State( ) : ViewState sealed class Event : ViewEvent { - data object Init : Event() + data class Init(val deepLink: Uri?) : Event() data object Pop : Event() data object OnPause : Event() data object OnResumeIssuance : Event() + data class OnDynamicPresentation(val uri: String) : Event() data object DismissError : Event() data class PrimaryButtonPressed(val context: Context) : Event() @@ -94,7 +101,8 @@ sealed class Effect : ViewSideEffect { data object Pop : Navigation() data class DeepLink( - val link: Uri + val link: Uri, + val routeToPop: String? = null ) : Navigation() } @@ -132,7 +140,14 @@ class DocumentOfferViewModel( override fun handleEvents(event: Event) { when (event) { is Event.Init -> { - resolveDocumentOffer(offerUri = viewState.value.offerUiConfig.offerURI) + if (viewState.value.documents.isEmpty()) { + resolveDocumentOffer( + offerUri = viewState.value.offerUiConfig.offerURI, + deepLink = event.deepLink + ) + } else { + handleDeepLink(event.deepLink) + } } is Event.Pop -> { @@ -182,10 +197,35 @@ class DocumentOfferViewModel( is Event.OnResumeIssuance -> setState { copy(isLoading = true) } + + is Event.OnDynamicPresentation -> { + getOrCreatePresentationScope() + setEffect { + Effect.Navigation.SwitchScreen( + generateComposableNavigationLink( + PresentationScreens.PresentationRequest, + generateComposableArguments( + mapOf( + RequestUriConfig.serializedKeyName to uiSerializer.toBase64( + RequestUriConfig( + PresentationMode.OpenId4Vp( + event.uri, + IssuanceScreens.DocumentOffer.screenRoute + ) + ), + RequestUriConfig + ) + ) + ) + ), + shouldPopToSelf = false + ) + } + } } } - private fun resolveDocumentOffer(offerUri: String) { + private fun resolveDocumentOffer(offerUri: String, deepLink: Uri? = null) { setState { copy( isLoading = documents.isEmpty(), @@ -226,6 +266,8 @@ class DocumentOfferViewModel( txCodeLength = response.txCodeLength ) } + + handleDeepLink(deepLink) } is ResolveDocumentOfferInteractorPartialState.NoDocument -> { @@ -340,7 +382,8 @@ class DocumentOfferViewModel( } is NavigationType.Deeplink -> Effect.Navigation.DeepLink( - nav.link.toUri() + nav.link.toUri(), + nav.routeToPop ) is NavigationType.Pop, NavigationType.Finish -> Effect.Navigation.Pop @@ -414,4 +457,21 @@ class DocumentOfferViewModel( ) ) } + + private fun handleDeepLink(deepLinkUri: Uri?) { + deepLinkUri?.let { uri -> + hasDeepLink(uri)?.let { + when (it.type) { + + DeepLinkType.EXTERNAL -> { + setEffect { + Effect.Navigation.DeepLink(uri) + } + } + + else -> {} + } + } + } + } } \ No newline at end of file diff --git a/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/interactor/PresentationLoadingInteractor.kt b/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/interactor/PresentationLoadingInteractor.kt index d4847a34..736a67a0 100644 --- a/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/interactor/PresentationLoadingInteractor.kt +++ b/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/interactor/PresentationLoadingInteractor.kt @@ -40,6 +40,7 @@ sealed class PresentationLoadingObserveResponsePartialState { interface PresentationLoadingInteractor { val verifierName: String? + val initiatorRoute: String fun stopPresentation() fun observeResponse(): Flow fun handleUserAuthentication( @@ -56,6 +57,9 @@ class PresentationLoadingInteractorImpl( override val verifierName: String? = walletCorePresentationController.verifierName + override val initiatorRoute: String = + walletCorePresentationController.initiatorRoute + override fun observeResponse(): Flow = walletCorePresentationController.observeSentDocumentsRequest().mapNotNull { response -> when (response) { diff --git a/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/ui/loading/PresentationLoadingViewModel.kt b/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/ui/loading/PresentationLoadingViewModel.kt index bfea2f77..92eb53d0 100644 --- a/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/ui/loading/PresentationLoadingViewModel.kt +++ b/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/ui/loading/PresentationLoadingViewModel.kt @@ -141,7 +141,7 @@ class PresentationLoadingViewModel( private fun getSuccessConfig(uri: URI?): Map { val deepLinkWithUriOrPopToDashboard = ConfigNavigation( navigationType = uri?.let { - NavigationType.Deeplink(it.toString()) + NavigationType.Deeplink(it.toString(), interactor.initiatorRoute) } ?: NavigationType.PopTo(DashboardScreens.Dashboard) ) diff --git a/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/qr/ProximityQRViewModel.kt b/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/qr/ProximityQRViewModel.kt index 35d6b5d7..fbd32bb5 100644 --- a/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/qr/ProximityQRViewModel.kt +++ b/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/qr/ProximityQRViewModel.kt @@ -28,6 +28,7 @@ import eu.europa.ec.uilogic.mvi.MviViewModel import eu.europa.ec.uilogic.mvi.ViewEvent import eu.europa.ec.uilogic.mvi.ViewSideEffect import eu.europa.ec.uilogic.mvi.ViewState +import eu.europa.ec.uilogic.navigation.DashboardScreens import eu.europa.ec.uilogic.navigation.ProximityScreens import eu.europa.ec.uilogic.navigation.helper.generateComposableArguments import eu.europa.ec.uilogic.navigation.helper.generateComposableNavigationLink @@ -149,7 +150,11 @@ class ProximityQRViewModel( arguments = generateComposableArguments( mapOf( RequestUriConfig.serializedKeyName to uiSerializer.toBase64( - RequestUriConfig(PresentationMode.Ble), + RequestUriConfig( + PresentationMode.Ble( + DashboardScreens.Dashboard.screenRoute + ) + ), RequestUriConfig.Parser ) ) diff --git a/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityQRInteractor.kt b/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityQRInteractor.kt index 51eb93a0..d896c25a 100644 --- a/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityQRInteractor.kt +++ b/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityQRInteractor.kt @@ -35,6 +35,7 @@ import eu.europa.ec.testlogic.extension.runTest import eu.europa.ec.testlogic.extension.toFlow import eu.europa.ec.testlogic.rule.CoroutineTestRule import eu.europa.ec.uilogic.container.EudiComponentActivity +import eu.europa.ec.uilogic.navigation.DashboardScreens import junit.framework.TestCase.assertEquals import kotlinx.coroutines.flow.flow import org.junit.After @@ -358,8 +359,9 @@ class TestProximityQRInteractor { @Test fun `Given a RequestUriConfig with Ble mode, When setConfig is called, Then it calls walletCorePresentationController#setConfig with PresentationControllerConfig_Ble`() { // Given + val initiator = DashboardScreens.Dashboard.screenRoute val config = RequestUriConfig( - mode = PresentationMode.Ble + mode = PresentationMode.Ble(initiator) ) // When @@ -367,14 +369,15 @@ class TestProximityQRInteractor { // Then verify(walletCorePresentationController, times(1)) - .setConfig(PresentationControllerConfig.Ble) + .setConfig(PresentationControllerConfig.Ble(initiator)) } @Test fun `Given a RequestUriConfig with OpenId4Vp mode, When setConfig is called, Then it calls walletCorePresentationController#setConfig with PresentationControllerConfig_OpenId4Vp`() { // Given + val initiator = DashboardScreens.Dashboard.screenRoute val config = RequestUriConfig( - mode = PresentationMode.OpenId4Vp(uri = "") + mode = PresentationMode.OpenId4Vp(uri = "", initiator) ) // When @@ -382,7 +385,7 @@ class TestProximityQRInteractor { // Then verify(walletCorePresentationController, times(1)) - .setConfig(PresentationControllerConfig.OpenId4VP(uri = "")) + .setConfig(PresentationControllerConfig.OpenId4VP(uri = "", initiator)) } //endregion diff --git a/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityRequestInteractor.kt b/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityRequestInteractor.kt index 11a874a1..f92d3ed8 100644 --- a/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityRequestInteractor.kt +++ b/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityRequestInteractor.kt @@ -51,6 +51,7 @@ import eu.europa.ec.testlogic.extension.runFlowTest import eu.europa.ec.testlogic.extension.runTest import eu.europa.ec.testlogic.extension.toFlow import eu.europa.ec.testlogic.rule.CoroutineTestRule +import eu.europa.ec.uilogic.navigation.DashboardScreens import junit.framework.TestCase.assertEquals import kotlinx.coroutines.flow.flow import org.junit.After @@ -656,8 +657,9 @@ class TestProximityRequestInteractor { @Test fun `Given a RequestUriConfig with Ble mode, When setConfig is called, Then it calls walletCorePresentationController#setConfig with PresentationControllerConfig_Ble`() { // Given + val initiator = DashboardScreens.Dashboard.screenRoute val config = RequestUriConfig( - mode = PresentationMode.Ble + mode = PresentationMode.Ble(initiator) ) // When @@ -665,14 +667,15 @@ class TestProximityRequestInteractor { // Then verify(walletCorePresentationController, times(1)) - .setConfig(PresentationControllerConfig.Ble) + .setConfig(PresentationControllerConfig.Ble(initiator)) } @Test fun `Given a RequestUriConfig with OpenId4Vp mode, When setConfig is called, Then it calls walletCorePresentationController#setConfig with PresentationControllerConfig_OpenId4Vp`() { // Given + val initiator = DashboardScreens.Dashboard.screenRoute val config = RequestUriConfig( - mode = PresentationMode.OpenId4Vp(uri = "") + mode = PresentationMode.OpenId4Vp(uri = "", initiator) ) // When @@ -680,7 +683,7 @@ class TestProximityRequestInteractor { // Then verify(walletCorePresentationController, times(1)) - .setConfig(PresentationControllerConfig.OpenId4VP(uri = "")) + .setConfig(PresentationControllerConfig.OpenId4VP(uri = "", initiator)) } //endregion diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/BroadcastReceiver.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/BroadcastReceiver.kt index e4d7652d..d6992494 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/BroadcastReceiver.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/BroadcastReceiver.kt @@ -27,14 +27,18 @@ import androidx.core.content.ContextCompat @Composable fun SystemBroadcastReceiver( - action: String, + actions: List, onEvent: (intent: Intent?) -> Unit ) { val context = LocalContext.current // If either context or Action changes, unregister and register again - DisposableEffect(context, action) { - val intentFilter = IntentFilter(action) + DisposableEffect(context, actions) { + val intentFilter = IntentFilter().apply { + actions.forEach { + addAction(it) + } + } val broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { onEvent(intent) diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/config/ConfigNavigation.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/config/ConfigNavigation.kt index f620732d..b1461202 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/config/ConfigNavigation.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/config/ConfigNavigation.kt @@ -35,7 +35,7 @@ sealed interface NavigationType { data class PushRoute(val route: String) : NavigationType data class PopTo(val screen: Screen) : NavigationType - data class Deeplink(val link: String) : NavigationType + data class Deeplink(val link: String, val routeToPop: String? = null) : NavigationType } enum class FlowCompletion { diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/container/EudiComponentActivity.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/container/EudiComponentActivity.kt index 3400b00c..9cef3087 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/container/EudiComponentActivity.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/container/EudiComponentActivity.kt @@ -28,7 +28,9 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import eu.europa.ec.resourceslogic.theme.ThemeManager +import eu.europa.ec.uilogic.navigation.IssuanceScreens import eu.europa.ec.uilogic.navigation.RouterHost +import eu.europa.ec.uilogic.navigation.helper.DeepLinkAction import eu.europa.ec.uilogic.navigation.helper.DeepLinkType import eu.europa.ec.uilogic.navigation.helper.handleDeepLinkAction import eu.europa.ec.uilogic.navigation.helper.hasDeepLink @@ -108,6 +110,15 @@ open class EudiComponentActivity : FragmentActivity() { ) { cacheDeepLink(intent) routerHost.popToIssuanceOnboardingScreen() + } else if (it.type == DeepLinkType.OPENID4VP + && routerHost.userIsLoggedInWithDocuments() + && (routerHost.isScreenOnBackStackOrForeground(IssuanceScreens.AddDocument) + || routerHost.isScreenOnBackStackOrForeground(IssuanceScreens.DocumentOffer)) + ) { + handleDeepLinkAction( + routerHost.getNavController(), + DeepLinkAction(it.link, DeepLinkType.DYNAMIC_PRESENTATION) + ) } else if (it.type != DeepLinkType.ISSUANCE) { cacheDeepLink(intent) if (routerHost.userIsLoggedInWithDocuments()) { diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/navigation/helper/DeepLinkHelper.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/navigation/helper/DeepLinkHelper.kt index f81fbe3c..3c3d31c6 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/navigation/helper/DeepLinkHelper.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/navigation/helper/DeepLinkHelper.kt @@ -19,7 +19,9 @@ package eu.europa.ec.uilogic.navigation.helper import android.content.Context import android.content.Intent import android.net.Uri +import android.os.Bundle import androidx.core.net.toUri +import androidx.core.os.bundleOf import androidx.navigation.NavController import eu.europa.ec.businesslogic.util.safeLet import eu.europa.ec.corelogic.util.CoreActions @@ -94,41 +96,57 @@ fun handleDeepLinkAction( arguments: String? = null ) { hasDeepLink(uri)?.let { action -> + handleDeepLinkAction(navController, action, arguments) + } +} - val screen: Screen +fun handleDeepLinkAction( + navController: NavController, + action: DeepLinkAction, + arguments: String? = null +) { + val screen: Screen - when (action.type) { - DeepLinkType.OPENID4VP -> { - screen = PresentationScreens.PresentationRequest - } + when (action.type) { + DeepLinkType.OPENID4VP -> { + screen = PresentationScreens.PresentationRequest + } - DeepLinkType.CREDENTIAL_OFFER -> { - screen = IssuanceScreens.DocumentOffer - } + DeepLinkType.CREDENTIAL_OFFER -> { + screen = IssuanceScreens.DocumentOffer + } - DeepLinkType.ISSUANCE -> { - EudiWallet.resumeOpenId4VciWithAuthorization(action.link) - notifyOnResumeIssuance(navController.context) - return@let - } + DeepLinkType.ISSUANCE -> { + EudiWallet.resumeOpenId4VciWithAuthorization(action.link) + notify(navController.context, CoreActions.VCI_RESUME_ACTION) + return + } - DeepLinkType.EXTERNAL -> { - navController.context.openUrl(action.link) - return@let - } + DeepLinkType.EXTERNAL -> { + navController.context.openUrl(action.link) + return } - val navigationLink = arguments?.let { - generateComposableNavigationLink( - screen = screen, - arguments = arguments + DeepLinkType.DYNAMIC_PRESENTATION -> { + notify( + navController.context, + CoreActions.VCI_DYNAMIC_PRESENTATION, + bundleOf(Pair("uri", action.link.toString())) ) - } ?: screen.screenRoute - - navController.navigate(navigationLink) { - popUpTo(screen.screenRoute) { inclusive = true } + return } } + + val navigationLink = arguments?.let { + generateComposableNavigationLink( + screen = screen, + arguments = arguments + ) + } ?: screen.screenRoute + + navController.navigate(navigationLink) { + popUpTo(screen.screenRoute) { inclusive = true } + } } data class DeepLinkAction(val link: Uri, val type: DeepLinkType) @@ -148,7 +166,12 @@ enum class DeepLinkType(val schemas: List, val host: String? = null) { schemas = listOf(BuildConfig.ISSUE_AUTHORIZATION_SCHEME), host = BuildConfig.ISSUE_AUTHORIZATION_HOST ), - EXTERNAL(listOf("external")); + EXTERNAL( + emptyList() + ), + DYNAMIC_PRESENTATION( + emptyList() + ); companion object { fun parse(scheme: String, host: String? = null): DeepLinkType = when { @@ -170,9 +193,10 @@ enum class DeepLinkType(val schemas: List, val host: String? = null) { } } -private fun notifyOnResumeIssuance(context: Context) { +private fun notify(context: Context, action: String, bundle: Bundle? = null) { Intent().also { intent -> - intent.setAction(CoreActions.VCI_RESUME_ACTION) + intent.setAction(action) + bundle?.let { intent.putExtras(it) } context.sendBroadcast(intent) } } \ No newline at end of file