diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/domain/controller/RqesController.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/domain/controller/RqesController.kt index 53ee9fb..dc43db5 100644 --- a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/domain/controller/RqesController.kt +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/domain/controller/RqesController.kt @@ -37,6 +37,7 @@ import eu.europa.ec.eudi.rqesui.infrastructure.config.data.DocumentData import eu.europa.ec.eudi.rqesui.infrastructure.config.data.QtspData import eu.europa.ec.eudi.rqesui.infrastructure.config.data.toCertificatesData import eu.europa.ec.eudi.rqesui.infrastructure.provider.ResourceProvider +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.net.URL @@ -78,6 +79,7 @@ internal interface RqesController { internal class RqesControllerImpl( private val eudiRQESUi: EudiRQESUi, private val resourceProvider: ResourceProvider, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : RqesController { private val genericErrorMsg @@ -164,7 +166,7 @@ internal class RqesControllerImpl( } override suspend fun getServiceAuthorizationUrl(rqesService: RQESService): EudiRqesGetServiceAuthorizationUrlPartialState { - return withContext(Dispatchers.IO) { + return withContext(dispatcher) { runCatching { val authorizationUrl = rqesService.getServiceAuthorizationUrl() .getOrThrow() @@ -181,7 +183,7 @@ internal class RqesControllerImpl( } override suspend fun authorizeService(): EudiRqesAuthorizeServicePartialState { - return withContext(Dispatchers.IO) { + return withContext(dispatcher) { runCatching { safeLet( eudiRQESUi.getRqesService(), @@ -216,7 +218,7 @@ internal class RqesControllerImpl( } override suspend fun getAvailableCertificates(authorizedService: Authorized): EudiRqesGetCertificatesPartialState { - return withContext(Dispatchers.IO) { + return withContext(dispatcher) { runCatching { val certificates = authorizedService.listCredentials() .getOrThrow() @@ -251,7 +253,7 @@ internal class RqesControllerImpl( authorizedService: Authorized, certificateData: CertificateData ): EudiRqesGetCredentialAuthorizationUrlPartialState { - return withContext(Dispatchers.IO) { + return withContext(dispatcher) { runCatching { eudiRQESUi.getSessionData().file?.let { safeSelectedFile -> @@ -295,7 +297,7 @@ internal class RqesControllerImpl( } override suspend fun authorizeCredential(): EudiRqesAuthorizeCredentialPartialState { - return withContext(Dispatchers.IO) { + return withContext(dispatcher) { runCatching { safeLet( getAuthorizedService(), @@ -323,7 +325,7 @@ internal class RqesControllerImpl( } override suspend fun signDocuments(authorizedCredential: RQESService.CredentialAuthorized): EudiRqesSignDocumentsPartialState { - return withContext(Dispatchers.IO) { + return withContext(dispatcher) { runCatching { val signedDocuments = authorizedCredential.signDocuments().getOrThrow() EudiRqesSignDocumentsPartialState.Success(signedDocuments = signedDocuments) @@ -341,7 +343,7 @@ internal class RqesControllerImpl( originalDocumentName: String, signedDocuments: SignedDocuments, ): EudiRqesSaveSignedDocumentsPartialState { - return withContext(Dispatchers.IO) { + return withContext(dispatcher) { runCatching { val uris = mutableListOf() diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/domain/entities/localization/LocalizableKey.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/domain/entities/localization/LocalizableKey.kt index bdc876a..5aab783 100644 --- a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/domain/entities/localization/LocalizableKey.kt +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/domain/entities/localization/LocalizableKey.kt @@ -44,6 +44,7 @@ enum class LocalizableKey { SigningCertificates, Success, SuccessfullySignedDocument, + SuccessDescription, SignedBy, View, Close, @@ -90,6 +91,7 @@ enum class LocalizableKey { SelectCertificateSubtitle -> "The signing certificate is used to verify your identity and is linked to your electronic signature." Success -> "Success!" SuccessfullySignedDocument -> "You successfully signed your document" + SuccessDescription -> "You have successfully signed your document." SignedBy -> "Signed by: $ARGUMENTS_SEPARATOR" View -> "VIEW" Close -> "Close" diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/entities/ButtonActionUi.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/entities/ButtonActionUi.kt new file mode 100644 index 0000000..2e121c7 --- /dev/null +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/entities/ButtonActionUi.kt @@ -0,0 +1,24 @@ +/* + * 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.eudi.rqesui.presentation.entities + +import eu.europa.ec.eudi.rqesui.presentation.architecture.ViewEvent + +internal data class ButtonActionUi( + val buttonText: String, + val event: T +) \ No newline at end of file diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/entities/SelectionItemUi.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/entities/SelectionOptionUi.kt similarity index 78% rename from rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/entities/SelectionItemUi.kt rename to rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/entities/SelectionOptionUi.kt index 5196951..5488214 100644 --- a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/entities/SelectionItemUi.kt +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/entities/SelectionOptionUi.kt @@ -17,21 +17,18 @@ package eu.europa.ec.eudi.rqesui.presentation.entities import androidx.compose.ui.graphics.Color -import eu.europa.ec.eudi.rqesui.infrastructure.config.data.DocumentData -import eu.europa.ec.eudi.rqesui.infrastructure.config.data.QtspData +import eu.europa.ec.eudi.rqesui.presentation.architecture.ViewEvent import eu.europa.ec.eudi.rqesui.presentation.ui.component.IconData -internal data class SelectionItemUi( - val documentData: DocumentData?, - val qtspData: QtspData?, - +internal data class SelectionOptionUi( val overlineText: String? = null, val mainText: String? = null, val subtitle: String? = null, - val action: String? = null, + val actionText: String? = null, val leadingIcon: IconData? = null, val leadingIconTint: Color? = null, val trailingIcon: IconData? = null, val trailingIconTint: Color? = null, - val enabled: Boolean = true + val enabled: Boolean = true, + val event: T?, ) \ No newline at end of file diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/AppIconAndText.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/AppIconAndText.kt new file mode 100644 index 0000000..36a0902 --- /dev/null +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/AppIconAndText.kt @@ -0,0 +1,65 @@ +/* + * 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.eudi.rqesui.presentation.ui.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import eu.europa.ec.eudi.rqesui.presentation.ui.component.preview.PreviewTheme +import eu.europa.ec.eudi.rqesui.presentation.ui.component.preview.ThemeModePreviews +import eu.europa.ec.eudi.rqesui.presentation.ui.component.utils.SPACING_SMALL +import eu.europa.ec.eudi.rqesui.presentation.ui.component.wrap.WrapImage + + +internal data class AppIconAndTextData( + val appIcon: IconData = AppIcons.LogoPlain, + val appText: IconData = AppIcons.LogoText, +) + +@Composable +internal fun AppIconAndText( + modifier: Modifier = Modifier, + appIconAndTextData: AppIconAndTextData +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy( + space = SPACING_SMALL.dp, + alignment = Alignment.CenterHorizontally + ), + verticalAlignment = Alignment.Top + ) { + WrapImage(iconData = appIconAndTextData.appIcon) + WrapImage(iconData = appIconAndTextData.appText) + } +} + +@ThemeModePreviews +@Composable +private fun AppIconAndTextPreview() { + PreviewTheme { + AppIconAndText( + appIconAndTextData = AppIconAndTextData( + appIcon = AppIcons.LogoPlain, + appText = AppIcons.LogoText, + ) + ) + } +} \ No newline at end of file diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/AppIcons.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/AppIcons.kt index 7ba8606..2c9e87e 100644 --- a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/AppIcons.kt +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/AppIcons.kt @@ -82,7 +82,7 @@ internal object AppIcons { ) val Verified: IconData = IconData( - resourceId = R.drawable.ic_verified, + resourceId = R.drawable.ic_rqes_verified, contentDescriptionId = R.string.content_description_verified_icon, imageVector = null ) @@ -110,4 +110,16 @@ internal object AppIcons { contentDescriptionId = R.string.content_description_selection_step_icon, imageVector = null ) + + val LogoPlain: IconData = IconData( + resourceId = R.drawable.ic_logo_plain, + contentDescriptionId = R.string.content_description_logo_plain_icon, + imageVector = null + ) + + val LogoText: IconData = IconData( + resourceId = R.drawable.ic_logo_text, + contentDescriptionId = R.string.content_description_logo_text_icon, + imageVector = null + ) } \ No newline at end of file diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/RelyingParty.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/RelyingParty.kt new file mode 100644 index 0000000..f6ae3d1 --- /dev/null +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/RelyingParty.kt @@ -0,0 +1,103 @@ +/* + * 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.eudi.rqesui.presentation.ui.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import eu.europa.ec.eudi.rqesui.infrastructure.theme.values.success +import eu.europa.ec.eudi.rqesui.presentation.ui.component.preview.PreviewTheme +import eu.europa.ec.eudi.rqesui.presentation.ui.component.preview.TextLengthPreviewProvider +import eu.europa.ec.eudi.rqesui.presentation.ui.component.preview.ThemeModePreviews +import eu.europa.ec.eudi.rqesui.presentation.ui.component.wrap.TextConfig +import eu.europa.ec.eudi.rqesui.presentation.ui.component.wrap.WrapIcon +import eu.europa.ec.eudi.rqesui.presentation.ui.component.wrap.WrapText + +/** + * Data class representing information about a Relying Party. + * + * @property isVerified A boolean indicating whether the Relying Party is verified. + * @property name The name of the Relying Party. + */ +data class RelyingPartyData( + val isVerified: Boolean, + val name: String, +) + +@Composable +fun RelyingParty( + modifier: Modifier = Modifier, + relyingPartyData: RelyingPartyData, +) { + val commonTextAlign = TextAlign.Center + + Column( + modifier = modifier, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + with(relyingPartyData) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + if (isVerified) { + WrapIcon( + modifier = Modifier.size(20.dp), + iconData = AppIcons.Verified, + customTint = MaterialTheme.colorScheme.success, + ) + } + WrapText( + modifier = Modifier.wrapContentWidth(), + text = name, + textConfig = TextConfig( + style = MaterialTheme.typography.titleMedium, + textAlign = commonTextAlign, + ) + ) + } + + } + } +} + +@ThemeModePreviews +@Composable +private fun RelyingPartyPreview( + @PreviewParameter(TextLengthPreviewProvider::class) text: String +) { + PreviewTheme { + RelyingParty( + relyingPartyData = RelyingPartyData( + isVerified = true, + name = "Relying Party Name: $text", + ) + ) + } +} \ No newline at end of file diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/SelectionItem.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/SelectionItem.kt index e22e075..18c1e2b 100644 --- a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/SelectionItem.kt +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/SelectionItem.kt @@ -33,10 +33,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import eu.europa.ec.eudi.rqesui.domain.extension.toUri import eu.europa.ec.eudi.rqesui.infrastructure.config.data.DocumentData -import eu.europa.ec.eudi.rqesui.presentation.entities.SelectionItemUi +import eu.europa.ec.eudi.rqesui.presentation.entities.SelectionOptionUi import eu.europa.ec.eudi.rqesui.presentation.ui.component.preview.PreviewTheme import eu.europa.ec.eudi.rqesui.presentation.ui.component.preview.TextLengthPreviewProvider import eu.europa.ec.eudi.rqesui.presentation.ui.component.preview.ThemeModePreviews @@ -46,16 +47,20 @@ import eu.europa.ec.eudi.rqesui.presentation.ui.component.utils.SPACING_LARGE import eu.europa.ec.eudi.rqesui.presentation.ui.component.utils.SPACING_MEDIUM import eu.europa.ec.eudi.rqesui.presentation.ui.component.wrap.WrapCard import eu.europa.ec.eudi.rqesui.presentation.ui.component.wrap.WrapIcon +import eu.europa.ec.eudi.rqesui.presentation.ui.options_selection.Event @Composable internal fun SelectionItem( modifier: Modifier = Modifier, - data: SelectionItemUi, - leadingIconTint: Color? = null, + selectionItemData: SelectionOptionUi<*>, colors: CardColors = CardDefaults.cardColors( containerColor = Color.Transparent ), shape: Shape = RoundedCornerShape(SIZE_SMALL.dp), + verticalPadding: Dp = SPACING_MEDIUM.dp, + horizontalPadding: Dp = SPACING_LARGE.dp, + trailingActionAlignment: Alignment.Vertical = Alignment.Top, + enabled: Boolean = true, onClick: (() -> Unit)?, ) { WrapCard( @@ -64,25 +69,28 @@ internal fun SelectionItem( throttleClicks = true, shape = shape, colors = colors, + enabled = enabled ) { Row( modifier = Modifier.padding( - horizontal = SPACING_LARGE.dp, - vertical = SPACING_MEDIUM.dp + horizontal = horizontalPadding, + vertical = verticalPadding ), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Top + verticalAlignment = trailingActionAlignment ) { - data.leadingIcon?.let { safeIcon -> + selectionItemData.leadingIcon?.let { safeIcon -> WrapIcon( modifier = Modifier.padding(end = SPACING_MEDIUM.dp), iconData = safeIcon, - customTint = leadingIconTint + customTint = selectionItemData.leadingIconTint ) } - Column(modifier = Modifier.weight(1f)) { - data.overlineText?.let { safeOverlineText -> + Column( + modifier = Modifier.weight(1f) + ) { + selectionItemData.overlineText?.let { safeOverlineText -> Text( text = safeOverlineText, style = MaterialTheme.typography.labelMedium, @@ -90,7 +98,7 @@ internal fun SelectionItem( ) } - data.mainText?.let { safeMainText -> + selectionItemData.mainText?.let { safeMainText -> Text( text = safeMainText, style = MaterialTheme.typography.bodyLarge, @@ -100,21 +108,11 @@ internal fun SelectionItem( ) } - data.documentData?.documentName?.let { safeDocumentName -> - Text( - text = safeDocumentName, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - } - - data.subtitle?.let { subtitle -> + selectionItemData.subtitle?.let { subtitle -> Text( text = subtitle, style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @@ -123,7 +121,7 @@ internal fun SelectionItem( modifier = Modifier.padding(start = SPACING_MEDIUM.dp), verticalAlignment = Alignment.CenterVertically ) { - data.action?.let { action -> + selectionItemData.actionText?.let { action -> Text( text = action, style = MaterialTheme.typography.labelSmall, @@ -133,7 +131,7 @@ internal fun SelectionItem( HSpacer.Small() - data.trailingIcon?.let { safeIcon -> + selectionItemData.trailingIcon?.let { safeIcon -> WrapIcon( iconData = safeIcon, customTint = MaterialTheme.colorScheme.primary @@ -144,22 +142,24 @@ internal fun SelectionItem( } } +private val DummyEventForPreview = Event.ViewDocumentItemPressed( + documentData = DocumentData( + documentName = "Document.pdf", + uri = "mockedUri".toUri() + ) +) + @ThemeModePreviews @Composable -private fun SelectionItemWithNoSubtitlePreview( - @PreviewParameter(TextLengthPreviewProvider::class) text: String -) { +private fun SelectionItemWithNoSubtitlePreview() { PreviewTheme { SelectionItem( modifier = Modifier.fillMaxWidth(), - data = SelectionItemUi( - documentData = DocumentData( - documentName = text, - uri = "test".toUri() - ), - action = "VIEW", - qtspData = null, - enabled = true + selectionItemData = SelectionOptionUi( + mainText = "Select document", + actionText = "VIEW", + enabled = true, + event = DummyEventForPreview ), onClick = {} ) @@ -174,17 +174,14 @@ private fun SelectionItemWithSubtitlePreview( PreviewTheme { SelectionItem( modifier = Modifier.fillMaxWidth(), - data = SelectionItemUi( - documentData = DocumentData( - documentName = text, - uri = "test".toUri() - ), - qtspData = null, + selectionItemData = SelectionOptionUi( + mainText = text, subtitle = text, - action = "VIEW", + actionText = "VIEW", leadingIcon = AppIcons.StepOne, trailingIcon = AppIcons.KeyboardArrowRight, - enabled = true + enabled = true, + event = DummyEventForPreview ), onClick = {} ) diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/content/ContentHeader.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/content/ContentHeader.kt new file mode 100644 index 0000000..74f2bfa --- /dev/null +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/content/ContentHeader.kt @@ -0,0 +1,159 @@ +/* + * 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.eudi.rqesui.presentation.ui.component.content + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import eu.europa.ec.eudi.rqesui.presentation.ui.component.AppIconAndText +import eu.europa.ec.eudi.rqesui.presentation.ui.component.AppIconAndTextData +import eu.europa.ec.eudi.rqesui.presentation.ui.component.AppIcons +import eu.europa.ec.eudi.rqesui.presentation.ui.component.RelyingParty +import eu.europa.ec.eudi.rqesui.presentation.ui.component.RelyingPartyData +import eu.europa.ec.eudi.rqesui.presentation.ui.component.preview.PreviewTheme +import eu.europa.ec.eudi.rqesui.presentation.ui.component.preview.TextLengthPreviewProvider +import eu.europa.ec.eudi.rqesui.presentation.ui.component.preview.ThemeModePreviews +import eu.europa.ec.eudi.rqesui.presentation.ui.component.utils.SPACING_LARGE +import eu.europa.ec.eudi.rqesui.presentation.ui.component.utils.SPACING_MEDIUM +import eu.europa.ec.eudi.rqesui.presentation.ui.component.utils.SPACING_SMALL +import eu.europa.ec.eudi.rqesui.presentation.ui.component.wrap.TextConfig +import eu.europa.ec.eudi.rqesui.presentation.ui.component.wrap.WrapText + +/** + * Data class representing the configuration for a content header. + * This header typically displays information like app icon, name, description, + * and potentially relying party details. + * + * @property appIconAndTextData Data for displaying the app icon and text. + * @property description A descriptive text for the content. + * @property descriptionTextConfig Configuration for the appearance of the description text. + * @property mainText The main title or heading text. + * @property mainTextConfig Configuration for the appearance of the main text. + * @property relyingPartyData Data for displaying information about the relying party, if applicable. + */ +internal data class ContentHeaderConfig( + val appIconAndTextData: AppIconAndTextData = AppIconAndTextData(), + val description: String?, + val descriptionTextConfig: TextConfig? = null, + val mainText: String? = null, + val mainTextConfig: TextConfig? = null, + val relyingPartyData: RelyingPartyData? = null, +) + +/** + * Composable function that displays the content header for the screen. + * + * This function displays the app icon and text, description, main text, and relying party information + * based on the provided [ContentHeaderConfig]. + * + * @param modifier Modifier used to adjust the layout of the header. + * @param config Configuration object containing data for the header content. + */ +@Composable +internal fun ContentHeader( + modifier: Modifier = Modifier, + config: ContentHeaderConfig, +) { + val commonTextAlign = TextAlign.Center + + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + with(config) { + // App icon and text section. + AppIconAndText( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = SPACING_LARGE.dp), + appIconAndTextData = appIconAndTextData, + ) + + // Description section. + description?.let { safeDescription -> + WrapText( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = SPACING_SMALL.dp), + text = safeDescription, + textConfig = descriptionTextConfig ?: TextConfig( + style = MaterialTheme.typography.bodyLarge, + textAlign = commonTextAlign, + maxLines = 3, + ) + ) + } + + // Main text section. + mainText?.let { safeMainText -> + WrapText( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = SPACING_MEDIUM.dp), + text = safeMainText, + textConfig = mainTextConfig ?: TextConfig( + style = MaterialTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.W600 + ), + textAlign = commonTextAlign, + ) + ) + } + + // Relying party section. + relyingPartyData?.let { safeRelyingPartyData -> + RelyingParty( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = SPACING_SMALL.dp), + relyingPartyData = safeRelyingPartyData, + ) + } + } + } +} + +@ThemeModePreviews +@Composable +private fun ContentHeaderPreview( + @PreviewParameter(TextLengthPreviewProvider::class) text: String +) { + PreviewTheme { + ContentHeader( + config = ContentHeaderConfig( + appIconAndTextData = AppIconAndTextData( + appIcon = AppIcons.LogoPlain, + appText = AppIcons.LogoText, + ), + description = "Description: $text", + mainText = "Title: $text", + relyingPartyData = RelyingPartyData( + isVerified = true, + name = "Relying Party Name: $text", + ) + ) + ) + } +} \ No newline at end of file diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/wrap/WrapImage.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/wrap/WrapImage.kt new file mode 100644 index 0000000..90c9291 --- /dev/null +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/wrap/WrapImage.kt @@ -0,0 +1,56 @@ +/* + * 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.eudi.rqesui.presentation.ui.component.wrap + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import eu.europa.ec.eudi.rqesui.presentation.ui.component.IconData + +@Composable +internal fun WrapImage( + iconData: IconData, + modifier: Modifier = Modifier, + colorFilter: ColorFilter? = null, + contentScale: ContentScale? = null, +) { + val iconContentDescription = stringResource(id = iconData.contentDescriptionId) + + iconData.resourceId?.let { resId -> + Image( + modifier = modifier, + painter = painterResource(id = resId), + contentDescription = iconContentDescription, + colorFilter = colorFilter, + contentScale = contentScale ?: ContentScale.FillBounds, + ) + } ?: run { + iconData.imageVector?.let { imageVector -> + Image( + modifier = modifier, + imageVector = imageVector, + contentDescription = iconContentDescription, + colorFilter = colorFilter, + contentScale = contentScale ?: ContentScale.FillBounds, + ) + } + } +} \ No newline at end of file diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/wrap/WrapText.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/wrap/WrapText.kt new file mode 100644 index 0000000..16ee2d0 --- /dev/null +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/component/wrap/WrapText.kt @@ -0,0 +1,84 @@ +/* + * 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.eudi.rqesui.presentation.ui.component.wrap + +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter +import eu.europa.ec.eudi.rqesui.presentation.ui.component.preview.PreviewTheme +import eu.europa.ec.eudi.rqesui.presentation.ui.component.preview.TextLengthPreviewProvider +import eu.europa.ec.eudi.rqesui.presentation.ui.component.preview.ThemeModePreviews + +/** + * Data class representing the configuration for text elements. + * + * This class provides options for customizing the appearance and behavior of text, + * such as style, color, alignment, maximum lines, and overflow handling. + * + * @property style The text style to apply. Defaults to null, which means `LocalTextStyle.current` will be used. + * @property color The color of the text. Defaults to null, which means `MaterialTheme.colorScheme.onSurface` color will be used. + * @property textAlign The horizontal alignment of the text. Defaults to [TextAlign.Start]. + * @property maxLines The maximum number of lines the text can occupy. Defaults to 2. + * @property overflow How visual overflow should be handled. Defaults to [TextOverflow.Ellipsis]. + */ +data class TextConfig( + val style: TextStyle? = null, + val color: Color? = null, + val textAlign: TextAlign = TextAlign.Start, + val maxLines: Int = 2, + val overflow: TextOverflow = TextOverflow.Ellipsis, +) + +@Composable +fun WrapText( + modifier: Modifier = Modifier, + text: String, + textConfig: TextConfig, +) { + Text( + modifier = modifier, + text = text, + style = textConfig.style ?: LocalTextStyle.current, + color = textConfig.color ?: MaterialTheme.colorScheme.onSurface, + textAlign = textConfig.textAlign, + maxLines = textConfig.maxLines, + overflow = textConfig.overflow, + ) +} + +@ThemeModePreviews +@Composable +private fun WrapTextConfigPreview( + @PreviewParameter(TextLengthPreviewProvider::class) text: String +) { + PreviewTheme { + WrapText( + text = text, + textConfig = TextConfig( + style = MaterialTheme.typography.bodyLarge, + maxLines = 1, + ) + ) + } +} \ No newline at end of file diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/options_selection/OptionsSelectionScreen.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/options_selection/OptionsSelectionScreen.kt index b5223c8..f108789 100644 --- a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/options_selection/OptionsSelectionScreen.kt +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/options_selection/OptionsSelectionScreen.kt @@ -23,6 +23,8 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme @@ -40,10 +42,12 @@ import androidx.navigation.NavController import eu.europa.ec.eudi.rqesui.domain.extension.toUri import eu.europa.ec.eudi.rqesui.domain.util.safeLet import eu.europa.ec.eudi.rqesui.infrastructure.config.data.DocumentData -import eu.europa.ec.eudi.rqesui.presentation.entities.SelectionItemUi +import eu.europa.ec.eudi.rqesui.presentation.entities.ButtonActionUi +import eu.europa.ec.eudi.rqesui.presentation.entities.SelectionOptionUi import eu.europa.ec.eudi.rqesui.presentation.entities.config.OptionsSelectionUiConfig import eu.europa.ec.eudi.rqesui.presentation.extension.finish import eu.europa.ec.eudi.rqesui.presentation.extension.openUrl +import eu.europa.ec.eudi.rqesui.presentation.ui.component.AppIcons import eu.europa.ec.eudi.rqesui.presentation.ui.component.SelectionItem import eu.europa.ec.eudi.rqesui.presentation.ui.component.content.ContentScreen import eu.europa.ec.eudi.rqesui.presentation.ui.component.content.ContentTitle @@ -86,16 +90,16 @@ internal fun OptionsSelectionScreen( onBack = { viewModel.setEvent(Event.Pop) }, contentErrorConfig = state.error, stickyBottom = { paddingValues -> - if (state.isContinueButtonVisible) { - state.authorizationUri?.let { safeUri -> + if (state.isBottomBarButtonVisible) { + state.bottomBarButtonAction?.let { safeButtonAction -> WrapBottomBarSecondaryButton( stickyBottomContentModifier = Modifier .fillMaxWidth() .padding(paddingValues), - buttonText = state.bottomBarButtonText, + buttonText = safeButtonAction.buttonText, onButtonClick = { viewModel.setEvent( - Event.BottomBarButtonPressed(uri = safeUri) + safeButtonAction.event ) } ) @@ -126,8 +130,7 @@ internal fun OptionsSelectionScreen( ) { OptionsSelectionSheetContent( sheetContent = state.sheetContent, - selectedQtspIndex = state.selectedQtspIndex, - selectedCertificateIndex = state.selectedCertificateIndex, + state = state, onEventSent = { event -> viewModel.setEvent(event) } @@ -157,10 +160,12 @@ private fun Content( ) { val context = LocalContext.current val coroutineScope = rememberCoroutineScope() + val scrollState = rememberScrollState() Column( modifier = Modifier .fillMaxSize() + .verticalScroll(scrollState) .padding( start = 0.dp, end = 0.dp, @@ -178,17 +183,14 @@ private fun Content( safeLet( state.documentSelectionItem, - state.documentSelectionItem?.documentData - ) { safeSelectionItem, documentData -> + state.documentSelectionItem?.event + ) { safeSelectionItem, selectionItemEvent -> SelectionItem( modifier = Modifier.fillMaxWidth(), - data = safeSelectionItem, - leadingIconTint = safeSelectionItem.leadingIconTint, + selectionItemData = safeSelectionItem, onClick = { onEventSend( - Event.ViewDocument( - documentData = documentData - ) + selectionItemEvent ) } ) @@ -198,31 +200,27 @@ private fun Content( ListDivider() SelectionItem( modifier = Modifier.padding(top = SPACING_MEDIUM.dp), - data = safeSelectionItem, - leadingIconTint = safeSelectionItem.leadingIconTint, + selectionItemData = safeSelectionItem, + enabled = safeSelectionItem.enabled, onClick = { - if (safeSelectionItem.enabled) { - onEventSend( - Event.RqesServiceSelectionItemPressed - ) - } + onEventSend( + Event.RqesServiceSelectionItemPressed + ) } ) } - AnimatedVisibility(visible = state.certificates.isNotEmpty()) { + AnimatedVisibility(visible = state.certificateDataList.isNotEmpty()) { state.certificateSelectionItem?.let { safeSelectionItem -> ListDivider() SelectionItem( modifier = Modifier.padding(top = SPACING_MEDIUM.dp), - data = safeSelectionItem, - leadingIconTint = safeSelectionItem.leadingIconTint, + selectionItemData = safeSelectionItem, + enabled = safeSelectionItem.enabled, onClick = { - if (safeSelectionItem.enabled) { - onEventSend( - Event.CertificateSelectionItemPressed - ) - } + onEventSend( + Event.CertificateSelectionItemPressed + ) } ) } @@ -268,13 +266,12 @@ private fun Content( @Composable private fun OptionsSelectionSheetContent( - sheetContent: SelectAndSignBottomSheetContent, + sheetContent: OptionsSelectionBottomSheetContent, + state: State, onEventSent: (event: Event) -> Unit, - selectedQtspIndex: Int, - selectedCertificateIndex: Int ) { when (sheetContent) { - is SelectAndSignBottomSheetContent.ConfirmCancellation -> { + is OptionsSelectionBottomSheetContent.ConfirmCancellation -> { DialogBottomSheet( textData = sheetContent.bottomSheetTextData, onPositiveClick = { @@ -286,7 +283,7 @@ private fun OptionsSelectionSheetContent( ) } - is SelectAndSignBottomSheetContent.SelectQTSP -> { + is OptionsSelectionBottomSheetContent.SelectQTSP -> { BottomSheetWithOptionsList( textData = sheetContent.bottomSheetTextData, options = sheetContent.options, @@ -298,10 +295,8 @@ private fun OptionsSelectionSheetContent( ) }, onPositiveClick = { - if (selectedQtspIndex < sheetContent.options.size) { - onEventSent( - sheetContent.options[selectedQtspIndex].event - ) + sheetContent.options.getOrNull(state.selectedQtspIndex)?.let { safeOption -> + onEventSent(safeOption.event) } }, onNegativeClick = { @@ -312,7 +307,7 @@ private fun OptionsSelectionSheetContent( ) } - is SelectAndSignBottomSheetContent.SelectCertificate -> { + is OptionsSelectionBottomSheetContent.SelectCertificate -> { BottomSheetWithOptionsList( textData = sheetContent.bottomSheetTextData, options = sheetContent.options, @@ -324,11 +319,10 @@ private fun OptionsSelectionSheetContent( ) }, onPositiveClick = { - if (selectedCertificateIndex < sheetContent.options.size) { - onEventSent( - sheetContent.options[selectedCertificateIndex].event - ) - } + sheetContent.options + .getOrNull(state.selectedCertificateIndex)?.let { safeOption -> + onEventSent(safeOption.event) + } }, onNegativeClick = { onEventSent( @@ -350,6 +344,13 @@ private fun ListDivider() { ) } +private val DummyEventForPreview = Event.ViewDocumentItemPressed( + documentData = DocumentData( + documentName = "File_to_be_signed.pdf", + uri = "mockedUri".toUri() + ) +) + @OptIn(ExperimentalMaterial3Api::class) @ThemeModePreviews @Composable @@ -357,23 +358,26 @@ private fun OptionsSelectionScreenContentPreview() { PreviewTheme { Content( state = State( - title = "Sign a document", - documentSelectionItem = SelectionItemUi( - documentData = DocumentData( - documentName = "Document name.PDF", - uri = "".toUri() - ), - action = "VIEW", - qtspData = null, - enabled = true + title = "Sign document", + documentSelectionItem = SelectionOptionUi( + actionText = "VIEW", + overlineText = "Document", + mainText = "File_to_be_signed.pdf", + trailingIcon = AppIcons.KeyboardArrowRight, + subtitle = "Choose a document from your device to sign electronically.", + enabled = true, + event = DummyEventForPreview ), - sheetContent = SelectAndSignBottomSheetContent.ConfirmCancellation( + sheetContent = OptionsSelectionBottomSheetContent.ConfirmCancellation( bottomSheetTextData = BottomSheetTextData( title = "title", message = "message", ) ), - bottomBarButtonText = "Sign", + bottomBarButtonAction = ButtonActionUi( + buttonText = "Continue", + event = Event.BottomBarButtonPressed(uri = "mockedUri".toUri()) + ), selectedQtspIndex = 0, selectedCertificateIndex = 0, config = OptionsSelectionUiConfig( diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/options_selection/OptionsSelectionViewModel.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/options_selection/OptionsSelectionViewModel.kt index 2835482..00bcd13 100644 --- a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/options_selection/OptionsSelectionViewModel.kt +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/options_selection/OptionsSelectionViewModel.kt @@ -38,8 +38,9 @@ import eu.europa.ec.eudi.rqesui.presentation.architecture.MviViewModel import eu.europa.ec.eudi.rqesui.presentation.architecture.ViewEvent import eu.europa.ec.eudi.rqesui.presentation.architecture.ViewSideEffect import eu.europa.ec.eudi.rqesui.presentation.architecture.ViewState +import eu.europa.ec.eudi.rqesui.presentation.entities.ButtonActionUi import eu.europa.ec.eudi.rqesui.presentation.entities.ModalOptionUi -import eu.europa.ec.eudi.rqesui.presentation.entities.SelectionItemUi +import eu.europa.ec.eudi.rqesui.presentation.entities.SelectionOptionUi import eu.europa.ec.eudi.rqesui.presentation.entities.config.OptionsSelectionUiConfig import eu.europa.ec.eudi.rqesui.presentation.entities.config.ViewDocumentUiConfig import eu.europa.ec.eudi.rqesui.presentation.navigation.SdkScreens @@ -57,25 +58,21 @@ internal data class State( val config: OptionsSelectionUiConfig, val currentScreenSelectionState: String = QTSP_SELECTION_STATE, - val documentSelectionItem: SelectionItemUi? = null, - val qtspServiceSelectionItem: SelectionItemUi? = null, - val certificateSelectionItem: SelectionItemUi? = null, + val documentSelectionItem: SelectionOptionUi? = null, + val qtspServiceSelectionItem: SelectionOptionUi? = null, + val certificateSelectionItem: SelectionOptionUi? = null, val error: ContentErrorConfig? = null, val isBottomSheetOpen: Boolean = false, val title: String, - val bottomBarButtonText: String, - - val sheetContent: SelectAndSignBottomSheetContent, - + val sheetContent: OptionsSelectionBottomSheetContent, val selectedQtspIndex: Int, val selectedCertificateIndex: Int, - val certificates: List = emptyList(), - val authorizationUri: Uri? = null, - - val isContinueButtonVisible: Boolean = false, + val certificateDataList: List = emptyList(), + val bottomBarButtonAction: ButtonActionUi? = null, + val isBottomBarButtonVisible: Boolean = false, ) : ViewState internal const val QTSP_SELECTION_STATE = "QTSP_SELECTION" @@ -88,13 +85,11 @@ internal sealed class Event : ViewEvent { data object DismissError : Event() data class BottomBarButtonPressed(val uri: Uri) : Event() + data class ViewDocumentItemPressed(val documentData: DocumentData) : Event() data object RqesServiceSelectionItemPressed : Event() data object CertificateSelectionItemPressed : Event() - data class ViewDocument(val documentData: DocumentData) : Event() - data object AuthorizeServiceAndFetchCertificates : Event() - data class FetchServiceAuthorizationUrl(val service: RQESService) : Event() sealed class BottomSheet : Event() { @@ -132,22 +127,22 @@ internal sealed class Effect : ViewSideEffect { data class OpenUrl(val uri: Uri) : Effect() } -internal sealed class SelectAndSignBottomSheetContent { +internal sealed class OptionsSelectionBottomSheetContent { data class ConfirmCancellation( val bottomSheetTextData: BottomSheetTextData, - ) : SelectAndSignBottomSheetContent() + ) : OptionsSelectionBottomSheetContent() data class SelectQTSP( val bottomSheetTextData: BottomSheetTextData, val options: List>, val selectedIndex: Int, - ) : SelectAndSignBottomSheetContent() + ) : OptionsSelectionBottomSheetContent() data class SelectCertificate( val bottomSheetTextData: BottomSheetTextData, val options: List>, - val selectedIndex: Int, - ) : SelectAndSignBottomSheetContent() + val selectedIndex: Int + ) : OptionsSelectionBottomSheetContent() } @KoinViewModel @@ -167,8 +162,7 @@ internal class OptionsSelectionViewModel( return State( title = resourceProvider.getLocalizedString(LocalizableKey.SignDocument), - bottomBarButtonText = resourceProvider.getLocalizedString(LocalizableKey.Continue), - sheetContent = SelectAndSignBottomSheetContent.ConfirmCancellation( + sheetContent = OptionsSelectionBottomSheetContent.ConfirmCancellation( bottomSheetTextData = getConfirmCancellationTextData() ), selectedQtspIndex = 0, @@ -189,14 +183,14 @@ internal class OptionsSelectionViewModel( CERTIFICATE_SELECTION_STATE -> { createQTSPSelectionItemOnSelectCertificateStep(event = event) - createCertificateSelectionItem(event = event) + createCertificateSelectionItemOnSelectCertificateStep(event = event) } } } is Event.Pop -> { showBottomSheet( - sheetContent = SelectAndSignBottomSheetContent.ConfirmCancellation( + sheetContent = OptionsSelectionBottomSheetContent.ConfirmCancellation( bottomSheetTextData = getConfirmCancellationTextData() ) ) @@ -228,8 +222,8 @@ internal class OptionsSelectionViewModel( } } - is Event.ViewDocument -> { - navigateToViewDocument(event.documentData) + is Event.ViewDocumentItemPressed -> { + navigateToViewDocument(documentData = event.documentData) } is Event.BottomBarButtonPressed -> { @@ -291,7 +285,7 @@ internal class OptionsSelectionViewModel( is Event.CertificateSelectionItemPressed -> { val bottomSheetOptions: List> = - viewState.value.certificates.mapIndexed { index, certificateData -> + viewState.value.certificateDataList.mapIndexed { index, certificateData -> ModalOptionUi( title = certificateData.name, trailingIcon = null, @@ -303,7 +297,7 @@ internal class OptionsSelectionViewModel( } showBottomSheet( - sheetContent = SelectAndSignBottomSheetContent.SelectCertificate( + sheetContent = OptionsSelectionBottomSheetContent.SelectCertificate( bottomSheetTextData = getSelectCertificateTextData(), options = bottomSheetOptions, selectedIndex = 0 @@ -320,10 +314,10 @@ internal class OptionsSelectionViewModel( is Event.BottomSheet.CertificateSelectedOnDoneButtonPressed -> { hideBottomSheet() with(viewState.value) { - if (certificates.isNotEmpty() && selectedCertificateIndex >= 0) { + if (certificateDataList.isNotEmpty() && selectedCertificateIndex >= 0) { getCertificateAuthorizationUrl( event, - certificates[selectedCertificateIndex], + certificateDataList[selectedCertificateIndex], ) } } @@ -356,16 +350,18 @@ internal class OptionsSelectionViewModel( is EudiRqesGetSelectedFilePartialState.Success -> { setState { copy( - documentSelectionItem = SelectionItemUi( - documentData = response.file, - qtspData = null, + documentSelectionItem = SelectionOptionUi( overlineText = resourceProvider.getLocalizedString(LocalizableKey.Document), + mainText = response.file.documentName, subtitle = resourceProvider.getLocalizedString(LocalizableKey.SelectDocumentSubtitle), - action = resourceProvider.getLocalizedString(LocalizableKey.View), + actionText = resourceProvider.getLocalizedString(LocalizableKey.View), leadingIcon = AppIcons.StepOne, leadingIconTint = ThemeColors.success, trailingIcon = AppIcons.KeyboardArrowRight, - enabled = true + enabled = true, + event = Event.ViewDocumentItemPressed( + documentData = response.file + ) ) ) } @@ -394,15 +390,15 @@ internal class OptionsSelectionViewModel( is EudiRqesGetSelectedFilePartialState.Success -> { setState { copy( - qtspServiceSelectionItem = SelectionItemUi( + qtspServiceSelectionItem = SelectionOptionUi( + overlineText = null, mainText = resourceProvider.getLocalizedString(LocalizableKey.SelectSigningService), - qtspData = null, - documentData = null, subtitle = resourceProvider.getLocalizedString(LocalizableKey.SelectSigningServiceSubtitle), - action = null, + actionText = null, leadingIcon = AppIcons.StepTwo, trailingIcon = AppIcons.KeyboardArrowRight, - enabled = true + enabled = true, + event = Event.RqesServiceSelectionItemPressed ) ) } @@ -431,19 +427,18 @@ internal class OptionsSelectionViewModel( is OptionsSelectionInteractorGetSelectedFileAndQtspPartialState.Success -> { setState { copy( - qtspServiceSelectionItem = SelectionItemUi( + qtspServiceSelectionItem = SelectionOptionUi( overlineText = resourceProvider.getLocalizedString( LocalizableKey.SigningService ), - mainText = resourceProvider.getLocalizedString(LocalizableKey.SelectSigningService), - qtspData = response.selectedQtsp, - documentData = null, + mainText = response.selectedQtsp.name, subtitle = resourceProvider.getLocalizedString(LocalizableKey.SelectSigningServiceSubtitle), - action = null, + actionText = null, leadingIcon = AppIcons.StepTwo, leadingIconTint = ThemeColors.success, trailingIcon = AppIcons.KeyboardArrowRight, - enabled = false + enabled = false, + event = Event.RqesServiceSelectionItemPressed ) ) } @@ -451,11 +446,13 @@ internal class OptionsSelectionViewModel( } } - private fun updateQTSPSelectionItem(qtspData: QtspData, rqesService: RQESService) { + private fun updateQTSPSelectionItem( + qtspData: QtspData, + rqesService: RQESService + ) { setState { copy( qtspServiceSelectionItem = qtspServiceSelectionItem?.copy( - qtspData = qtspData, overlineText = resourceProvider.getLocalizedString( LocalizableKey.SigningService ), @@ -470,7 +467,7 @@ internal class OptionsSelectionViewModel( } } - private fun createCertificateSelectionItem(event: Event) { + private fun createCertificateSelectionItemOnSelectCertificateStep(event: Event) { when (val response = optionsSelectionInteractor.getSelectedFile()) { is EudiRqesGetSelectedFilePartialState.Failure -> { setState { @@ -494,15 +491,14 @@ internal class OptionsSelectionViewModel( is EudiRqesGetSelectedFilePartialState.Success -> { setState { copy( - certificateSelectionItem = SelectionItemUi( - documentData = null, - qtspData = null, - overlineText = resourceProvider.getLocalizedString(LocalizableKey.SigningCertificate), + certificateSelectionItem = SelectionOptionUi( + overlineText = resourceProvider.getLocalizedString(LocalizableKey.SelectSigningCertificateTitle), mainText = null, subtitle = resourceProvider.getLocalizedString(LocalizableKey.SelectCertificateSubtitle), leadingIcon = AppIcons.StepThree, trailingIcon = AppIcons.KeyboardArrowRight, - enabled = true + enabled = true, + event = Event.CertificateSelectionItemPressed ) ) } @@ -542,7 +538,7 @@ internal class OptionsSelectionViewModel( } showBottomSheet( - sheetContent = SelectAndSignBottomSheetContent.SelectQTSP( + sheetContent = OptionsSelectionBottomSheetContent.SelectQTSP( bottomSheetTextData = getSelectQTSPTextData(), options = bottomSheetOptions, selectedIndex = viewState.value.selectedQtspIndex, @@ -600,7 +596,7 @@ internal class OptionsSelectionViewModel( is OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState.Failure -> { setState { copy( - certificates = emptyList(), + certificateDataList = emptyList(), error = ContentErrorConfig( onRetry = { setEvent(Event.DismissError) @@ -620,7 +616,7 @@ internal class OptionsSelectionViewModel( is OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState.Success -> { setState { copy( - certificates = response.certificates, + certificateDataList = response.certificates, isLoading = false, ) } @@ -660,10 +656,14 @@ internal class OptionsSelectionViewModel( is EudiRqesGetCredentialAuthorizationUrlPartialState.Success -> { setState { + val buttonAction = ButtonActionUi( + buttonText = resourceProvider.getLocalizedString(LocalizableKey.Continue), + event = Event.BottomBarButtonPressed(uri = response.authorizationUrl) + ) copy( isLoading = false, - authorizationUri = response.authorizationUrl, - isContinueButtonVisible = true + bottomBarButtonAction = buttonAction, + isBottomBarButtonVisible = true ) } updateCertificateSelectionItem(certificateData = certificateData) @@ -734,7 +734,7 @@ internal class OptionsSelectionViewModel( ) } - private fun showBottomSheet(sheetContent: SelectAndSignBottomSheetContent) { + private fun showBottomSheet(sheetContent: OptionsSelectionBottomSheetContent) { setState { copy(sheetContent = sheetContent) } diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/success/SuccessScreen.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/success/SuccessScreen.kt index d3bf4a2..da66bae 100644 --- a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/success/SuccessScreen.kt +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/success/SuccessScreen.kt @@ -27,33 +27,33 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SheetState -import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import eu.europa.ec.eudi.rqesui.domain.extension.toUri -import eu.europa.ec.eudi.rqesui.domain.util.safeLet import eu.europa.ec.eudi.rqesui.infrastructure.config.data.DocumentData -import eu.europa.ec.eudi.rqesui.infrastructure.theme.values.success -import eu.europa.ec.eudi.rqesui.presentation.entities.SelectionItemUi +import eu.europa.ec.eudi.rqesui.infrastructure.theme.values.ThemeColors +import eu.europa.ec.eudi.rqesui.presentation.entities.SelectionOptionUi import eu.europa.ec.eudi.rqesui.presentation.extension.finish import eu.europa.ec.eudi.rqesui.presentation.extension.openIntentChooser +import eu.europa.ec.eudi.rqesui.presentation.ui.component.AppIcons import eu.europa.ec.eudi.rqesui.presentation.ui.component.SelectionItem -import eu.europa.ec.eudi.rqesui.presentation.ui.component.TextWithBadge +import eu.europa.ec.eudi.rqesui.presentation.ui.component.content.ContentHeader import eu.europa.ec.eudi.rqesui.presentation.ui.component.content.ContentScreen -import eu.europa.ec.eudi.rqesui.presentation.ui.component.content.ContentTitle import eu.europa.ec.eudi.rqesui.presentation.ui.component.content.ScreenNavigateAction import eu.europa.ec.eudi.rqesui.presentation.ui.component.preview.PreviewTheme import eu.europa.ec.eudi.rqesui.presentation.ui.component.preview.ThemeModePreviews import eu.europa.ec.eudi.rqesui.presentation.ui.component.utils.OneTimeLaunchedEffect import eu.europa.ec.eudi.rqesui.presentation.ui.component.utils.SPACING_LARGE +import eu.europa.ec.eudi.rqesui.presentation.ui.component.utils.SPACING_MEDIUM import eu.europa.ec.eudi.rqesui.presentation.ui.component.wrap.BottomSheetTextData import eu.europa.ec.eudi.rqesui.presentation.ui.component.wrap.DialogBottomSheet import eu.europa.ec.eudi.rqesui.presentation.ui.component.wrap.WrapBottomBarSecondaryButton @@ -120,7 +120,7 @@ internal fun SuccessScreen( }, sheetState = bottomSheetState ) { - safeSelectionItem.documentData?.uri?.let { safeUri -> + safeSelectionItem.event?.documentData?.uri?.let { safeUri -> SuccessSheetContent( sheetContent = state.sheetContent, documentUri = safeUri, @@ -135,7 +135,7 @@ internal fun SuccessScreen( } OneTimeLaunchedEffect { - viewModel.setEvent(Event.Init) + viewModel.setEvent(Event.Initialize) } } @@ -158,59 +158,32 @@ private fun Content( .padding(paddingValues), verticalArrangement = Arrangement.spacedBy(SPACING_LARGE.dp) ) { - - ContentTitle( - title = state.title, - verticalPadding = PaddingValues(0.dp) + ContentHeader( + modifier = Modifier.fillMaxWidth(), + config = state.headerConfig, ) - state.headline?.let { safeHeadline -> - Text( - text = safeHeadline, - style = MaterialTheme.typography.headlineLarge.copy( - color = MaterialTheme.colorScheme.success - ) - ) - } - - safeLet( - state.subtitle, - state.selectionItem - ) { subtitle, selectionItem -> - - Column { - Text( - text = subtitle, - style = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.onSurface - ) - ) - - selectionItem.documentData?.documentName?.let { safeDocumentName -> - TextWithBadge( - message = safeDocumentName, - showBadge = true - ) - } - } - - selectionItem.documentData?.let { safeDocumentData -> + state.selectionItem?.let { safeSelectionItem -> + safeSelectionItem.event?.documentData?.let { safeDocumentData -> SelectionItem( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth(), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.tertiary ), - data = selectionItem, + selectionItemData = safeSelectionItem, + verticalPadding = SPACING_LARGE.dp, + horizontalPadding = SPACING_MEDIUM.dp, + trailingActionAlignment = Alignment.CenterVertically, onClick = { onEventSend( - Event.ViewDocument( + Event.ViewDocumentItemPressed( documentData = safeDocumentData ) ) } ) } - } } @@ -278,25 +251,26 @@ private fun SuccessSheetContent( } } +private val DummyEventForPreview = Event.ViewDocumentItemPressed( + documentData = DocumentData(documentName = "Document.pdf", uri = "mockedUri".toUri()) +) + @OptIn(ExperimentalMaterial3Api::class) @ThemeModePreviews @Composable private fun SuccessScreenPreview() { PreviewTheme { - val documentName = "Document name.PDF" Content( state = State( title = "Sign document", headline = "Success", subtitle = "You successfully signed your document", - selectionItem = SelectionItemUi( - documentData = DocumentData( - documentName = documentName, - uri = "".toUri() - ), - subtitle = "Signed by: Entrust", - action = "View", - qtspData = null, + selectionItem = SelectionOptionUi( + mainText = "Document name.PDF", + actionText = "VIEW", + leadingIcon = AppIcons.Verified, + leadingIconTint = ThemeColors.success, + event = DummyEventForPreview ), bottomBarButtonText = "Close", sheetContent = SuccessBottomSheetContent.ShareDocument( diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/success/SuccessViewModel.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/success/SuccessViewModel.kt index 6a617da..8da3ba8 100644 --- a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/success/SuccessViewModel.kt +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/success/SuccessViewModel.kt @@ -27,23 +27,32 @@ import eu.europa.ec.eudi.rqesui.domain.serializer.UiSerializer import eu.europa.ec.eudi.rqesui.infrastructure.config.data.DocumentData import eu.europa.ec.eudi.rqesui.infrastructure.config.data.QtspData import eu.europa.ec.eudi.rqesui.infrastructure.provider.ResourceProvider +import eu.europa.ec.eudi.rqesui.infrastructure.theme.values.ThemeColors import eu.europa.ec.eudi.rqesui.presentation.architecture.MviViewModel import eu.europa.ec.eudi.rqesui.presentation.architecture.ViewEvent import eu.europa.ec.eudi.rqesui.presentation.architecture.ViewSideEffect import eu.europa.ec.eudi.rqesui.presentation.architecture.ViewState -import eu.europa.ec.eudi.rqesui.presentation.entities.SelectionItemUi +import eu.europa.ec.eudi.rqesui.presentation.entities.SelectionOptionUi import eu.europa.ec.eudi.rqesui.presentation.entities.config.ViewDocumentUiConfig import eu.europa.ec.eudi.rqesui.presentation.navigation.SdkScreens import eu.europa.ec.eudi.rqesui.presentation.navigation.helper.generateComposableArguments import eu.europa.ec.eudi.rqesui.presentation.navigation.helper.generateComposableNavigationLink +import eu.europa.ec.eudi.rqesui.presentation.ui.component.AppIconAndTextData +import eu.europa.ec.eudi.rqesui.presentation.ui.component.AppIcons +import eu.europa.ec.eudi.rqesui.presentation.ui.component.RelyingPartyData import eu.europa.ec.eudi.rqesui.presentation.ui.component.content.ContentErrorConfig +import eu.europa.ec.eudi.rqesui.presentation.ui.component.content.ContentHeaderConfig import eu.europa.ec.eudi.rqesui.presentation.ui.component.wrap.BottomSheetTextData import kotlinx.coroutines.launch import org.koin.android.annotation.KoinViewModel internal data class State( val isLoading: Boolean = false, - val selectionItem: SelectionItemUi? = null, + val headerConfig: ContentHeaderConfig = ContentHeaderConfig( + appIconAndTextData = AppIconAndTextData(), + description = null, + ), + val selectionItem: SelectionOptionUi? = null, val error: ContentErrorConfig? = null, val isBottomSheetOpen: Boolean = false, val isBottomBarButtonEnabled: Boolean = false, @@ -57,7 +66,7 @@ internal data class State( ) : ViewState internal sealed class Event : ViewEvent { - data object Init : Event() + data object Initialize : Event() data class SignAndSaveDocument( val originalDocumentName: String, val qtspName: String, @@ -66,10 +75,9 @@ internal sealed class Event : ViewEvent { data object Pop : Event() data object DismissError : Event() + data class ViewDocumentItemPressed(val documentData: DocumentData) : Event() data object BottomBarButtonPressed : Event() - data class ViewDocument(val documentData: DocumentData) : Event() - sealed class BottomSheet : Event() { data class UpdateBottomSheetState(val isOpen: Boolean) : BottomSheet() @@ -126,7 +134,7 @@ internal class SuccessViewModel( override fun handleEvents(event: Event) { when (event) { - is Event.Init -> { + is Event.Initialize -> { getSelectedFileAndQtsp(event) } @@ -150,7 +158,7 @@ internal class SuccessViewModel( ) } - is Event.ViewDocument -> { + is Event.ViewDocumentItemPressed -> { navigateToViewDocument(event.documentData) } @@ -250,18 +258,23 @@ internal class SuccessViewModel( } is SuccessInteractorSignAndSaveDocumentPartialState.Success -> { - val selectionItem = SelectionItemUi( - documentData = response.savedDocument, - subtitle = resourceProvider.getLocalizedString( - LocalizableKey.SignedBy, - listOf(qtspName) - ), - action = resourceProvider.getLocalizedString(LocalizableKey.View), - qtspData = null + val headerConfig = ContentHeaderConfig( + appIconAndTextData = AppIconAndTextData(), + description = resourceProvider.getLocalizedString(LocalizableKey.SuccessDescription), + relyingPartyData = getHeaderConfigData(qtspName = qtspName) + ) + + val selectionItem = SelectionOptionUi( + mainText = response.savedDocument.documentName, + leadingIcon = AppIcons.Verified, + leadingIconTint = ThemeColors.success, + actionText = resourceProvider.getLocalizedString(LocalizableKey.View), + event = Event.ViewDocumentItemPressed(response.savedDocument) ) setState { copy( + headerConfig = headerConfig, selectionItem = selectionItem, headline = resourceProvider.getLocalizedString(LocalizableKey.Success), subtitle = resourceProvider.getLocalizedString(LocalizableKey.SuccessfullySignedDocument), @@ -317,4 +330,11 @@ internal class SuccessViewModel( Effect.CloseBottomSheet } } + + private fun getHeaderConfigData(qtspName: String): RelyingPartyData { + return RelyingPartyData( + isVerified = true, + name = qtspName, + ) + } } \ No newline at end of file diff --git a/rqes-ui-sdk/src/main/res/drawable/ic_logo_plain.xml b/rqes-ui-sdk/src/main/res/drawable/ic_logo_plain.xml new file mode 100644 index 0000000..122aeb6 --- /dev/null +++ b/rqes-ui-sdk/src/main/res/drawable/ic_logo_plain.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rqes-ui-sdk/src/main/res/drawable/ic_logo_text.xml b/rqes-ui-sdk/src/main/res/drawable/ic_logo_text.xml new file mode 100644 index 0000000..caeba23 --- /dev/null +++ b/rqes-ui-sdk/src/main/res/drawable/ic_logo_text.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rqes-ui-sdk/src/main/res/drawable/ic_verified.xml b/rqes-ui-sdk/src/main/res/drawable/ic_rqes_verified.xml similarity index 100% rename from rqes-ui-sdk/src/main/res/drawable/ic_verified.xml rename to rqes-ui-sdk/src/main/res/drawable/ic_rqes_verified.xml diff --git a/rqes-ui-sdk/src/main/res/values/strings.xml b/rqes-ui-sdk/src/main/res/values/strings.xml index d98c1ff..49f9634 100644 --- a/rqes-ui-sdk/src/main/res/values/strings.xml +++ b/rqes-ui-sdk/src/main/res/values/strings.xml @@ -23,4 +23,6 @@ More Items Next Selection step + Plain Logo icon + Text Logo icon \ No newline at end of file