diff --git a/assembly-logic/build.gradle.kts b/assembly-logic/build.gradle.kts index 22e62d09..8ac366d4 100644 --- a/assembly-logic/build.gradle.kts +++ b/assembly-logic/build.gradle.kts @@ -36,6 +36,7 @@ import project.convention.logic.kover.koverModules plugins { id("project.android.library") id("project.android.library.compose") + id("project.rqes.sdk") } android { diff --git a/assembly-logic/src/main/AndroidManifest.xml b/assembly-logic/src/main/AndroidManifest.xml index 61b72626..c4f8d240 100644 --- a/assembly-logic/src/main/AndroidManifest.xml +++ b/assembly-logic/src/main/AndroidManifest.xml @@ -166,6 +166,18 @@ android:scheme="${openId4VciAuthorizationScheme}" /> + + + + + + + + + diff --git a/assembly-logic/src/main/java/eu/europa/ec/assemblylogic/Application.kt b/assembly-logic/src/main/java/eu/europa/ec/assemblylogic/Application.kt index 9422434a..a6b81fe0 100644 --- a/assembly-logic/src/main/java/eu/europa/ec/assemblylogic/Application.kt +++ b/assembly-logic/src/main/java/eu/europa/ec/assemblylogic/Application.kt @@ -19,22 +19,38 @@ package eu.europa.ec.assemblylogic import android.app.Application import eu.europa.ec.analyticslogic.controller.AnalyticsController import eu.europa.ec.assemblylogic.di.setupKoin +import eu.europa.ec.businesslogic.config.ConfigLogic import eu.europa.ec.corelogic.config.WalletCoreConfig +import eu.europa.ec.eudi.rqesui.infrastructure.EudiRQESUi import eu.europa.ec.eudi.wallet.EudiWallet import org.koin.android.ext.android.inject +import org.koin.core.KoinApplication class Application : Application() { private val configWalletCore: WalletCoreConfig by inject() private val analyticsController: AnalyticsController by inject() + private val configLogic: ConfigLogic by inject() override fun onCreate() { super.onCreate() - setupKoin() + initializeKoin().initializeRqes() initializeReporting() initializeEudiWallet() } + private fun KoinApplication.initializeRqes() { + EudiRQESUi.setup( + application = this@Application, + config = configLogic.rqesConfig, + koinApplication = this@initializeRqes + ) + } + + private fun initializeKoin(): KoinApplication { + return setupKoin() + } + private fun initializeReporting() { analyticsController.initialize(this) } diff --git a/assembly-logic/src/main/java/eu/europa/ec/assemblylogic/di/AssemblyModule.kt b/assembly-logic/src/main/java/eu/europa/ec/assemblylogic/di/AssemblyModule.kt index e1289259..108a9c7e 100644 --- a/assembly-logic/src/main/java/eu/europa/ec/assemblylogic/di/AssemblyModule.kt +++ b/assembly-logic/src/main/java/eu/europa/ec/assemblylogic/di/AssemblyModule.kt @@ -33,6 +33,7 @@ import eu.europa.ec.startupfeature.di.FeatureStartupModule import eu.europa.ec.uilogic.di.LogicUiModule import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger +import org.koin.core.KoinApplication import org.koin.core.context.GlobalContext.startKoin import org.koin.ksp.generated.module @@ -57,8 +58,8 @@ private val assembledModules = listOf( FeatureIssuanceModule().module ) -internal fun Application.setupKoin() { - startKoin { +internal fun Application.setupKoin(): KoinApplication { + return startKoin { androidContext(this@setupKoin) androidLogger() modules(assembledModules) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 1da129b7..0f29df25 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -112,5 +112,9 @@ gradlePlugin { id = "project.android.base.profile" implementationClass = "AndroidBaseLineProfilePlugin" } + register("eudiRqes") { + id = "project.rqes.sdk" + implementationClass = "EudiRqesPlugin" + } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt index a1ba855b..924da9fb 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -58,6 +58,10 @@ class AndroidLibraryConventionPlugin : Plugin { val openId4VciAuthorizationScheme = "eu.europa.ec.euidi" val openId4VciAuthorizationHost = "authorization" + val rqesScheme = "rqes" + val rqesHost = "oauth" + val rqesPath = "/callback" + with(pluginManager) { apply("com.android.library") apply("project.android.library.kover") @@ -87,6 +91,9 @@ class AndroidLibraryConventionPlugin : Plugin { "ISSUE_AUTHORIZATION_DEEPLINK", "$openId4VciAuthorizationScheme://$openId4VciAuthorizationHost" ) + addConfigField("RQES_SCHEME", rqesScheme) + addConfigField("RQES_HOST", rqesHost) + addConfigField("RQES_DEEPLINK", "$rqesScheme://$rqesHost$rqesPath") // Manifest placeholders for Wallet deepLink manifestPlaceholders["deepLinkScheme"] = walletScheme @@ -109,6 +116,11 @@ class AndroidLibraryConventionPlugin : Plugin { openId4VciAuthorizationScheme manifestPlaceholders["openId4VciAuthorizationHost"] = openId4VciAuthorizationHost + + // Manifest placeholders used for RQES + manifestPlaceholders["rqesHost"] = rqesHost + manifestPlaceholders["rqesScheme"] = rqesScheme + manifestPlaceholders["rqesPath"] = rqesPath } configureFlavors(this) configureGradleManagedDevices(this) diff --git a/build-logic/convention/src/main/kotlin/EudiRqesPlugin.kt b/build-logic/convention/src/main/kotlin/EudiRqesPlugin.kt new file mode 100644 index 00000000..f56f8bc9 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/EudiRqesPlugin.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European + * Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work + * except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the Licence for the specific language + * governing permissions and limitations under the Licence. + */ + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies +import project.convention.logic.libs + +class EudiRqesPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + dependencies { + add("implementation", libs.findLibrary("rqes-ui-sdk").get()) + } + } + } +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/project/convention/logic/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/project/convention/logic/KotlinAndroid.kt index 0e0aab66..11f0b5ac 100644 --- a/build-logic/convention/src/main/kotlin/project/convention/logic/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/project/convention/logic/KotlinAndroid.kt @@ -37,7 +37,7 @@ internal fun Project.configureKotlinAndroid( compileSdk = 35 defaultConfig { - minSdk = 26 + minSdk = 28 } buildFeatures { diff --git a/business-logic/build.gradle.kts b/business-logic/build.gradle.kts index c6755284..f6c4f42f 100644 --- a/business-logic/build.gradle.kts +++ b/business-logic/build.gradle.kts @@ -20,6 +20,7 @@ import project.convention.logic.kover.excludeFromKoverReport plugins { id("project.android.library") + id("project.rqes.sdk") } android { diff --git a/business-logic/src/demo/java/eu/europa/ec/businesslogic/config/ConfigLogicImpl.kt b/business-logic/src/demo/java/eu/europa/ec/businesslogic/config/ConfigLogicImpl.kt index c315cda1..ba978601 100644 --- a/business-logic/src/demo/java/eu/europa/ec/businesslogic/config/ConfigLogicImpl.kt +++ b/business-logic/src/demo/java/eu/europa/ec/businesslogic/config/ConfigLogicImpl.kt @@ -16,12 +16,24 @@ package eu.europa.ec.businesslogic.config +import eu.europa.ec.businesslogic.BuildConfig +import eu.europa.ec.eudi.rqes.HashAlgorithmOID +import eu.europa.ec.eudi.rqes.SigningAlgorithmOID +import eu.europa.ec.eudi.rqesui.domain.extension.toUri +import eu.europa.ec.eudi.rqesui.infrastructure.config.EudiRQESUiConfig +import eu.europa.ec.eudi.rqesui.infrastructure.config.RqesServiceConfig +import eu.europa.ec.eudi.rqesui.infrastructure.config.data.QtspData +import java.net.URI + class ConfigLogicImpl : ConfigLogic { override val appFlavor: AppFlavor get() = AppFlavor.DEMO override val environmentConfig: EnvironmentConfig get() = DemoEnvironmentConfig() + + override val rqesConfig: EudiRQESUiConfig + get() = RqesConfig() } private class DemoEnvironmentConfig : EnvironmentConfig() { @@ -29,4 +41,25 @@ private class DemoEnvironmentConfig : EnvironmentConfig() { ServerConfig.Debug -> "" ServerConfig.Release -> "" } +} + +private class RqesConfig : EudiRQESUiConfig { + + override val rqesServiceConfig: RqesServiceConfig + get() = RqesServiceConfig( + clientId = "wallet-client", + clientSecret = "somesecret2", + authFlowRedirectionURI = URI.create(BuildConfig.RQES_DEEPLINK), + signingAlgorithm = SigningAlgorithmOID.RSA, + hashAlgorithm = HashAlgorithmOID.SHA_256, + ) + + override val qtsps: List + get() = listOf( + QtspData( + name = "Wallet-Centric", + endpoint = "https://walletcentric.signer.eudiw.dev/csc/v2".toUri(), + scaUrl = "https://walletcentric.signer.eudiw.dev", + ) + ) } \ No newline at end of file diff --git a/business-logic/src/dev/java/eu/europa/ec/businesslogic/config/ConfigLogicImpl.kt b/business-logic/src/dev/java/eu/europa/ec/businesslogic/config/ConfigLogicImpl.kt index 7a76f345..6dea9144 100644 --- a/business-logic/src/dev/java/eu/europa/ec/businesslogic/config/ConfigLogicImpl.kt +++ b/business-logic/src/dev/java/eu/europa/ec/businesslogic/config/ConfigLogicImpl.kt @@ -16,12 +16,24 @@ package eu.europa.ec.businesslogic.config +import eu.europa.ec.businesslogic.BuildConfig +import eu.europa.ec.eudi.rqes.HashAlgorithmOID +import eu.europa.ec.eudi.rqes.SigningAlgorithmOID +import eu.europa.ec.eudi.rqesui.domain.extension.toUri +import eu.europa.ec.eudi.rqesui.infrastructure.config.EudiRQESUiConfig +import eu.europa.ec.eudi.rqesui.infrastructure.config.RqesServiceConfig +import eu.europa.ec.eudi.rqesui.infrastructure.config.data.QtspData +import java.net.URI + class ConfigLogicImpl : ConfigLogic { override val appFlavor: AppFlavor get() = AppFlavor.DEV override val environmentConfig: EnvironmentConfig get() = DevEnvironmentConfig() + + override val rqesConfig: EudiRQESUiConfig + get() = RqesConfig() } private class DevEnvironmentConfig : EnvironmentConfig() { @@ -29,4 +41,25 @@ private class DevEnvironmentConfig : EnvironmentConfig() { ServerConfig.Debug -> "" ServerConfig.Release -> "" } +} + +private class RqesConfig : EudiRQESUiConfig { + + override val rqesServiceConfig: RqesServiceConfig + get() = RqesServiceConfig( + clientId = "wallet-client", + clientSecret = "somesecret2", + authFlowRedirectionURI = URI.create(BuildConfig.RQES_DEEPLINK), + signingAlgorithm = SigningAlgorithmOID.RSA, + hashAlgorithm = HashAlgorithmOID.SHA_256, + ) + + override val qtsps: List + get() = listOf( + QtspData( + name = "Wallet-Centric", + endpoint = "https://walletcentric.signer.eudiw.dev/csc/v2".toUri(), + scaUrl = "https://walletcentric.signer.eudiw.dev", + ) + ) } \ No newline at end of file diff --git a/business-logic/src/main/java/eu/europa/ec/businesslogic/config/ConfigLogic.kt b/business-logic/src/main/java/eu/europa/ec/businesslogic/config/ConfigLogic.kt index 79ccc041..42eddfb9 100644 --- a/business-logic/src/main/java/eu/europa/ec/businesslogic/config/ConfigLogic.kt +++ b/business-logic/src/main/java/eu/europa/ec/businesslogic/config/ConfigLogic.kt @@ -17,6 +17,7 @@ package eu.europa.ec.businesslogic.config import eu.europa.ec.businesslogic.BuildConfig +import eu.europa.ec.eudi.rqesui.infrastructure.config.EudiRQESUiConfig interface ConfigLogic { @@ -39,6 +40,11 @@ interface ConfigLogic { * Application version. */ val appVersion: String get() = BuildConfig.APP_VERSION + + /** + * RQES Config. + */ + val rqesConfig: EudiRQESUiConfig } enum class AppFlavor { diff --git a/dashboard-feature/build.gradle.kts b/dashboard-feature/build.gradle.kts index 7f603193..e09f32f4 100644 --- a/dashboard-feature/build.gradle.kts +++ b/dashboard-feature/build.gradle.kts @@ -20,6 +20,7 @@ import project.convention.logic.kover.excludeFromKoverReport plugins { id("project.android.feature") + id("project.rqes.sdk") } android { diff --git a/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/di/FeatureDashboardModule.kt b/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/di/FeatureDashboardModule.kt index ff70f9a4..b93aabb3 100644 --- a/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/di/FeatureDashboardModule.kt +++ b/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/di/FeatureDashboardModule.kt @@ -22,6 +22,8 @@ import eu.europa.ec.corelogic.config.WalletCoreConfig import eu.europa.ec.corelogic.controller.WalletCoreDocumentsController import eu.europa.ec.dashboardfeature.interactor.DashboardInteractor import eu.europa.ec.dashboardfeature.interactor.DashboardInteractorImpl +import eu.europa.ec.dashboardfeature.interactor.DocumentSignInteractor +import eu.europa.ec.dashboardfeature.interactor.DocumentSignInteractorImpl import eu.europa.ec.resourceslogic.provider.ResourceProvider import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Factory @@ -37,7 +39,7 @@ fun provideDashboardInteractor( walletCoreDocumentsController: WalletCoreDocumentsController, walletCoreConfig: WalletCoreConfig, configLogic: ConfigLogic, - logController: LogController + logController: LogController, ): DashboardInteractor = DashboardInteractorImpl( resourceProvider, @@ -45,4 +47,8 @@ fun provideDashboardInteractor( walletCoreConfig, configLogic, logController - ) \ No newline at end of file + ) + +@Factory +fun provideDocumentSignInteractor(): DocumentSignInteractor = + DocumentSignInteractorImpl() \ No newline at end of file diff --git a/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/interactor/DocumentSignInteractor.kt b/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/interactor/DocumentSignInteractor.kt new file mode 100644 index 00000000..50feb729 --- /dev/null +++ b/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/interactor/DocumentSignInteractor.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European + * Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work + * except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the Licence for the specific language + * governing permissions and limitations under the Licence. + */ + +package eu.europa.ec.dashboardfeature.interactor + +import android.content.Context +import android.net.Uri +import eu.europa.ec.eudi.rqesui.infrastructure.EudiRQESUi + +interface DocumentSignInteractor { + fun launchRQESSdk(context: Context, uri: Uri) +} + +class DocumentSignInteractorImpl : DocumentSignInteractor { + override fun launchRQESSdk(context: Context, uri: Uri) { + EudiRQESUi.initiate(context, uri) + } +} \ No newline at end of file diff --git a/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/router/Graph.kt b/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/router/Graph.kt index 11ff3551..da904107 100644 --- a/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/router/Graph.kt +++ b/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/router/Graph.kt @@ -23,6 +23,7 @@ import androidx.navigation.compose.navigation import androidx.navigation.navDeepLink import eu.europa.ec.dashboardfeature.BuildConfig import eu.europa.ec.dashboardfeature.ui.dashboard.DashboardScreen +import eu.europa.ec.dashboardfeature.ui.sign.DocumentSignScreen import eu.europa.ec.uilogic.navigation.DashboardScreens import eu.europa.ec.uilogic.navigation.ModuleRoute import org.koin.androidx.compose.koinViewModel @@ -43,5 +44,11 @@ fun NavGraphBuilder.featureDashboardGraph(navController: NavController) { ) { DashboardScreen(navController, koinViewModel()) } + + composable( + route = DashboardScreens.SignDocument.screenRoute + ) { + DocumentSignScreen(navController, koinViewModel()) + } } } \ 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 5487db98..e0367109 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 @@ -107,6 +107,7 @@ sealed class Event : ViewEvent { sealed class Options : BottomSheet() { data object OpenChangeQuickPin : Options() + data object OpenSignDocument : Options() data object OpenScanQr : Options() data object RetrieveLogs : Options() } @@ -224,6 +225,11 @@ class DashboardViewModel( icon = AppIcons.QrScanner, event = Event.BottomSheet.Options.OpenScanQr ), + ModalOptionUi( + title = resourceProvider.getString(R.string.dashboard_bottom_sheet_options_action_4), + icon = AppIcons.Sign, + event = Event.BottomSheet.Options.OpenSignDocument + ), ModalOptionUi( title = resourceProvider.getString(R.string.dashboard_bottom_sheet_options_action_3), icon = AppIcons.OpenNew, @@ -270,6 +276,12 @@ class DashboardViewModel( navigateToChangeQuickPin() } + is Event.BottomSheet.Options.OpenSignDocument -> { + hideBottomSheet() + navigateToDocumentSign() + + } + is Event.BottomSheet.Options.OpenScanQr -> { hideBottomSheet() navigateToQrScan() @@ -648,6 +660,14 @@ class DashboardViewModel( } } + private fun navigateToDocumentSign() { + setEffect { + Effect.Navigation.SwitchScreen( + screenRoute = DashboardScreens.SignDocument.screenRoute + ) + } + } + private fun startProximityFlow() { setState { copy(bleAvailability = BleAvailability.AVAILABLE) } // Create Koin scope for presentation diff --git a/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/ui/sign/DocumentSignScreen.kt b/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/ui/sign/DocumentSignScreen.kt new file mode 100644 index 00000000..a7634e9f --- /dev/null +++ b/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/ui/sign/DocumentSignScreen.kt @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European + * Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work + * except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the Licence for the specific language + * governing permissions and limitations under the Licence. + */ + +package eu.europa.ec.dashboardfeature.ui.sign + +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import eu.europa.ec.resourceslogic.R +import eu.europa.ec.resourceslogic.theme.values.backgroundDefault +import eu.europa.ec.resourceslogic.theme.values.textPrimaryDark +import eu.europa.ec.uilogic.component.AppIcons +import eu.europa.ec.uilogic.component.content.ContentScreen +import eu.europa.ec.uilogic.component.content.ContentTitle +import eu.europa.ec.uilogic.component.content.ScreenNavigateAction +import eu.europa.ec.uilogic.component.utils.ALPHA_ENABLED +import eu.europa.ec.uilogic.component.utils.SPACING_MEDIUM +import eu.europa.ec.uilogic.component.utils.VSpacer +import eu.europa.ec.uilogic.component.wrap.WrapCard +import eu.europa.ec.uilogic.component.wrap.WrapIcon +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach + +@Composable +internal fun DocumentSignScreen( + navController: NavController, + viewModel: DocumentSignViewModel, +) { + val state = viewModel.viewState.value + + ContentScreen( + isLoading = state.isLoading, + navigatableAction = ScreenNavigateAction.CANCELABLE, + onBack = { viewModel.setEvent(Event.Pop) }, + contentErrorConfig = state.error + ) { contentPadding -> + Content( + state = state, + effectFlow = viewModel.effect, + onEventSend = { viewModel.setEvent(it) }, + onNavigationRequested = { navigationEffect -> + when (navigationEffect) { + Effect.Navigation.Pop -> navController.popBackStack() + } + }, + paddingValues = contentPadding + ) + } +} + + +@Composable +private fun Content( + state: State, + effectFlow: Flow, + onEventSend: (Event) -> Unit, + onNavigationRequested: (Effect.Navigation) -> Unit, + paddingValues: PaddingValues, +) { + + val context = LocalContext.current + + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + ContentTitle( + title = state.title, + subtitle = state.subtitle, + ) + + VSpacer.Medium() + + SignButton(onEventSend) + } + + val selectPdfLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument(), + ) { uri -> + uri?.let { + onEventSend(Event.DocumentUriRetrieved(context, it)) + } + } + + LaunchedEffect(Unit) { + effectFlow.onEach { effect -> + when (effect) { + is Effect.Navigation.Pop -> onNavigationRequested(effect) + is Effect.OpenDocumentSelection -> selectPdfLauncher.launch(effect.selection) + is Effect.LaunchedRQES -> { + Toast.makeText( + context, + "Launched with: ${effect.uri}", + Toast.LENGTH_LONG + ).show() + } + } + }.collect() + } +} + +@Composable +private fun SignButton(onEventSend: (Event) -> Unit) { + WrapCard( + onClick = { + onEventSend( + Event.OnSelectDocument + ) + }, + throttleClicks = true, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.backgroundDefault, + ) + ) { + Row( + modifier = Modifier.padding(SPACING_MEDIUM.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + val iconsColor = MaterialTheme.colorScheme.primary + val iconsAlpha = ALPHA_ENABLED + val textColor = MaterialTheme.colorScheme.textPrimaryDark + + Text( + modifier = Modifier.weight(1f), + text = stringResource(R.string.document_sign_select_document), + style = MaterialTheme.typography.titleMedium, + color = textColor + ) + + WrapIcon( + iconData = AppIcons.Add, + customTint = iconsColor, + contentAlpha = iconsAlpha + ) + } + } +} diff --git a/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/ui/sign/DocumentSignViewModel.kt b/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/ui/sign/DocumentSignViewModel.kt new file mode 100644 index 00000000..74d11df4 --- /dev/null +++ b/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/ui/sign/DocumentSignViewModel.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European + * Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work + * except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the Licence for the specific language + * governing permissions and limitations under the Licence. + */ + +package eu.europa.ec.dashboardfeature.ui.sign + +import android.content.Context +import android.net.Uri +import eu.europa.ec.dashboardfeature.interactor.DocumentSignInteractor +import eu.europa.ec.resourceslogic.provider.ResourceProvider +import eu.europa.ec.uilogic.component.content.ContentErrorConfig +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 org.koin.android.annotation.KoinViewModel + +data class State( + val isLoading: Boolean = false, + val error: ContentErrorConfig? = null, + val title: String, + val subtitle: String, +) : ViewState + +sealed class Event : ViewEvent { + data object Pop : Event() + data object OnSelectDocument : Event() + data class DocumentUriRetrieved(val context: Context, val uri: Uri) : Event() +} + +sealed class Effect : ViewSideEffect { + sealed class Navigation : Effect() { + data object Pop : Navigation() + } + + data class OpenDocumentSelection(val selection: Array) : Effect() + data class LaunchedRQES(val uri: Uri) : Effect() +} + +@KoinViewModel +class DocumentSignViewModel( + private val documentSignInteractor: DocumentSignInteractor, + private val resourceProvider: ResourceProvider, +) : MviViewModel() { + + override fun setInitialState(): State = State( + title = resourceProvider.getString(eu.europa.ec.resourceslogic.R.string.document_sign_title), + subtitle = resourceProvider.getString(eu.europa.ec.resourceslogic.R.string.document_sign_subtitle) + ) + + override fun handleEvents(event: Event) { + when (event) { + is Event.OnSelectDocument -> { + setEffect { Effect.OpenDocumentSelection(arrayOf("application/pdf")) } + } + + is Event.Pop -> setEffect { Effect.Navigation.Pop } + is Event.DocumentUriRetrieved -> documentSignInteractor.launchRQESSdk( + event.context, + event.uri + ) + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 235e7ba8..cdf0d358 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,6 +67,7 @@ sonar = "5.0.0.4638" baselineprofile = "1.3.3" timber = "5.0.1" treessence = "1.1.2" +rqesUiSDK = "0.0.3-SNAPSHOT" [libraries] accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" } @@ -154,6 +155,7 @@ appcenter-crashes = { group = "com.microsoft.appcenter", name = "appcenter-crash appcenter-distribute = { group = "com.microsoft.appcenter", name = "appcenter-distribute", version.ref = "appCenter" } timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } treessence = { group = "com.github.bastienpaulfr", name = "Treessence", version.ref = "treessence" } +rqes-ui-sdk = { group = "eu.europa.ec.eudi", name = "eudi-lib-android-rqes-ui", version.ref = "rqesUiSDK" } # Dependencies of the included build-logic android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } diff --git a/resources-logic/src/main/res/drawable/ic_sign_document.xml b/resources-logic/src/main/res/drawable/ic_sign_document.xml new file mode 100644 index 00000000..96c87573 --- /dev/null +++ b/resources-logic/src/main/res/drawable/ic_sign_document.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources-logic/src/main/res/values/strings.xml b/resources-logic/src/main/res/values/strings.xml index 057d1db9..df898d8c 100644 --- a/resources-logic/src/main/res/values/strings.xml +++ b/resources-logic/src/main/res/values/strings.xml @@ -164,6 +164,7 @@ Change quick pin Scan a QR code Retrieve logs + Sign document Enable bluetooth? Enable bluetooth to share your information using the QR/TAP option @@ -179,6 +180,11 @@ New documents have been added to your wallet. Share logs + + Sign Document + Select a document from your device to sign electronically. + Select document + Camera permission not provided\nOpen App Settings Having trouble scanning the QR Code?\nPlease try an alternative way diff --git a/ui-logic/build.gradle.kts b/ui-logic/build.gradle.kts index ee5a8919..2328d558 100644 --- a/ui-logic/build.gradle.kts +++ b/ui-logic/build.gradle.kts @@ -21,6 +21,7 @@ import project.convention.logic.kover.excludeFromKoverReport plugins { id("project.android.library") id("project.android.library.compose") + id("project.rqes.sdk") } android { diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/AppIcons.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/AppIcons.kt index e2b52970..afc3baac 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/AppIcons.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/AppIcons.kt @@ -178,6 +178,12 @@ object AppIcons { imageVector = null ) + val Sign: IconData = IconData( + resourceId = R.drawable.ic_sign_document, + contentDescriptionId = R.string.content_description_edit_icon, + imageVector = null + ) + val QrScanner: IconData = IconData( resourceId = R.drawable.ic_qr_scanner, contentDescriptionId = R.string.content_description_qr_scanner_icon, 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 9cef3087..56f3b451 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 @@ -74,7 +74,7 @@ open class EudiComponentActivity : FragmentActivity() { } } - override fun onNewIntent(intent: Intent?) { + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) if (flowStarted) { handleDeepLink(intent) diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/navigation/RouterContract.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/navigation/RouterContract.kt index a46f5676..e7d707bd 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/navigation/RouterContract.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/navigation/RouterContract.kt @@ -45,6 +45,8 @@ sealed class CommonScreens { sealed class DashboardScreens { data object Dashboard : Screen(name = "DASHBOARD") + data object SignDocument : + Screen(name = "SIGN_DOCUMENT") } sealed class LoginScreens { 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 b44430c5..97d7f7c4 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 @@ -25,6 +25,7 @@ import androidx.core.os.bundleOf import androidx.navigation.NavController import eu.europa.ec.businesslogic.util.safeLet import eu.europa.ec.corelogic.util.CoreActions +import eu.europa.ec.eudi.rqesui.infrastructure.EudiRQESUi import eu.europa.ec.uilogic.BuildConfig import eu.europa.ec.uilogic.container.EudiComponentActivity import eu.europa.ec.uilogic.extension.openUrl @@ -137,6 +138,16 @@ fun handleDeepLinkAction( ) return } + + DeepLinkType.RQES -> { + action.link.getQueryParameter("code")?.let { + EudiRQESUi.resume( + context = navController.context, + authorizationCode = it + ) + } + return + } } val navigationLink = arguments?.let { @@ -173,6 +184,10 @@ enum class DeepLinkType(val schemas: List, val host: String? = null) { ), DYNAMIC_PRESENTATION( emptyList() + ), + RQES( + schemas = listOf(BuildConfig.RQES_SCHEME), + host = BuildConfig.RQES_HOST ); companion object { @@ -190,6 +205,10 @@ enum class DeepLinkType(val schemas: List, val host: String? = null) { ISSUANCE } + RQES.schemas.contains(scheme) && host == RQES.host -> { + RQES + } + else -> EXTERNAL } } @@ -197,7 +216,7 @@ enum class DeepLinkType(val schemas: List, val host: String? = null) { private fun notify(context: Context, action: String, bundle: Bundle? = null) { Intent().also { intent -> - intent.setAction(action) + intent.action = action bundle?.let { intent.putExtras(it) } context.sendBroadcast(intent) }