From 66bf88e29f9e00878d6fa18ca8f7b823b1899f75 Mon Sep 17 00:00:00 2001 From: Giannis Stamatopoulos Date: Tue, 6 Feb 2024 12:45:42 +0200 Subject: [PATCH] Adds logic to show a Badge if the Verifier is trusted, in the Request Screen. --- .../WalletCorePresentationController.kt | 11 ++-- .../commonfeature/ui/request/RequestScreen.kt | 21 +++++- .../ui/request/RequestViewModel.kt | 20 +++++- .../PresentationRequestInteractor.kt | 11 +++- .../loading/PresentationLoadingViewModel.kt | 10 ++- .../request/PresentationRequestViewModel.kt | 29 +++++++-- .../interactor/ProximityRequestInteractor.kt | 10 ++- .../ui/loading/ProximityLoadingViewModel.kt | 10 ++- .../ui/qr/ProximityQRViewModel.kt | 1 + .../ui/request/ProximityRequestViewModel.kt | 29 +++++++-- .../src/main/res/drawable/ic_verified.xml | 25 ++++++++ .../src/main/res/values/strings.xml | 8 ++- .../europa/ec/uilogic/component/AppIcons.kt | 6 ++ .../uilogic/component/content/ContentTitle.kt | 64 ++++++++++++++++++- 14 files changed, 211 insertions(+), 44 deletions(-) create mode 100644 resources-logic/src/main/res/drawable/ic_verified.xml diff --git a/business-logic/src/main/java/eu/europa/ec/businesslogic/controller/walletcore/WalletCorePresentationController.kt b/business-logic/src/main/java/eu/europa/ec/businesslogic/controller/walletcore/WalletCorePresentationController.kt index 370b0e17..18e6af69 100644 --- a/business-logic/src/main/java/eu/europa/ec/businesslogic/controller/walletcore/WalletCorePresentationController.kt +++ b/business-logic/src/main/java/eu/europa/ec/businesslogic/controller/walletcore/WalletCorePresentationController.kt @@ -53,9 +53,9 @@ sealed class TransferEventPartialState { data class QrEngagementReady(val qrCode: String) : TransferEventPartialState() data class RequestReceived( val requestData: List, - val verifierName: String? - ) : - TransferEventPartialState() + val verifierName: String?, + val verifierIsTrusted: Boolean, + ) : TransferEventPartialState() data object ResponseSent : TransferEventPartialState() data class Redirect(val uri: URI) : TransferEventPartialState() @@ -211,10 +211,13 @@ class WalletCorePresentationControllerImpl( onRequestReceived = { requestDocuments -> verifierName = requestDocuments.firstOrNull()?.docRequest?.readerAuth?.readerCommonName + val verifierIsTrusted = + requestDocuments.firstOrNull()?.docRequest?.readerAuth?.readerSignIsValid == true trySendBlocking( TransferEventPartialState.RequestReceived( requestData = requestDocuments, - verifierName = verifierName + verifierName = verifierName, + verifierIsTrusted = verifierIsTrusted ) ) }, diff --git a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/request/RequestScreen.kt b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/request/RequestScreen.kt index 6b2efa6e..460d5a9a 100644 --- a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/request/RequestScreen.kt +++ b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/request/RequestScreen.kt @@ -42,6 +42,7 @@ 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.content.TitleWithBadge import eu.europa.ec.uilogic.component.preview.PreviewTheme import eu.europa.ec.uilogic.component.preview.ThemeModePreviews import eu.europa.ec.uilogic.component.utils.OneTimeLaunchedEffect @@ -153,7 +154,12 @@ private fun Content( ) { // Screen Title. ContentTitle( - title = state.screenTitle, + titleWithBadge = state.screenTitle, + onTitleWithBadgeClick = if (state.screenTitle.isTrusted) { + { onEventSend(Event.BadgeClicked) } + } else { + null + }, subtitle = state.screenSubtitle, clickableSubtitle = state.screenClickableSubtitle, onSubtitleClick = { onEventSend(Event.SubtitleClicked) }, @@ -218,6 +224,15 @@ private fun SheetContent( onEventSent: (event: Event) -> Unit ) { when (sheetContent) { + RequestBottomSheetContent.BADGE -> { + DialogBottomSheet( + title = stringResource(id = R.string.request_bottom_sheet_badge_title), + message = stringResource(id = R.string.request_bottom_sheet_badge_subtitle), + positiveButtonText = stringResource(id = R.string.request_bottom_sheet_badge_primary_button_text), + onPositiveClick = { onEventSent(Event.BottomSheet.Badge.PrimaryButtonPressed) }, + ) + } + RequestBottomSheetContent.SUBTITLE -> { DialogBottomSheet( title = stringResource(id = R.string.request_bottom_sheet_subtitle_title), @@ -285,7 +300,7 @@ private fun ContentPreview() { PreviewTheme { Content( state = State( - screenTitle = "Title", + screenTitle = TitleWithBadge(isTrusted = false), screenSubtitle = "Subtitle ", screenClickableSubtitle = "clickable subtitle", warningText = "Warning", @@ -328,7 +343,7 @@ private fun StickyBottomSectionPreview() { PreviewTheme { StickyBottomSection( state = State( - screenTitle = "Title", + screenTitle = TitleWithBadge(isTrusted = false), screenSubtitle = "Subtitle ", screenClickableSubtitle = "clickable subtitle", warningText = "Warning", diff --git a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/request/RequestViewModel.kt b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/request/RequestViewModel.kt index 378c0a45..145ed4fd 100644 --- a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/request/RequestViewModel.kt +++ b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/request/RequestViewModel.kt @@ -19,6 +19,7 @@ package eu.europa.ec.commonfeature.ui.request import eu.europa.ec.businesslogic.di.getOrCreatePresentationScope import eu.europa.ec.commonfeature.ui.request.model.RequestDataUi import eu.europa.ec.uilogic.component.content.ContentErrorConfig +import eu.europa.ec.uilogic.component.content.TitleWithBadge import eu.europa.ec.uilogic.config.NavigationType import eu.europa.ec.uilogic.mvi.MviViewModel import eu.europa.ec.uilogic.mvi.ViewEvent @@ -34,7 +35,7 @@ data class State( val sheetContent: RequestBottomSheetContent = RequestBottomSheetContent.SUBTITLE, val verifierName: String? = null, - val screenTitle: String, + val screenTitle: TitleWithBadge, val screenSubtitle: String, val screenClickableSubtitle: String?, val warningText: String, @@ -51,6 +52,7 @@ sealed class Event : ViewEvent { data class ExpandOrCollapseRequiredDataList(val id: Int) : Event() data class UserIdentificationClicked(val itemId: String) : Event() + data object BadgeClicked : Event() data object SubtitleClicked : Event() data object PrimaryButtonPressed : Event() data object SecondaryButtonPressed : Event() @@ -66,6 +68,10 @@ sealed class Event : ViewEvent { sealed class Subtitle : BottomSheet() { data object PrimaryButtonPressed : Subtitle() } + + sealed class Badge : BottomSheet() { + data object PrimaryButtonPressed : Subtitle() + } } } @@ -86,7 +92,7 @@ sealed class Effect : ViewSideEffect { } enum class RequestBottomSheetContent { - SUBTITLE, CANCEL + BADGE, SUBTITLE, CANCEL } abstract class RequestViewModel : MviViewModel() { @@ -117,7 +123,7 @@ abstract class RequestViewModel : MviViewModel() { override fun setInitialState(): State { return State( - screenTitle = "", + screenTitle = TitleWithBadge(isTrusted = false), screenSubtitle = getScreenSubtitle(), screenClickableSubtitle = getScreenClickableSubtitle(), warningText = getWarningText(), @@ -157,6 +163,10 @@ abstract class RequestViewModel : MviViewModel() { updateUserIdentificationItem(id = event.itemId) } + is Event.BadgeClicked -> { + showBottomSheet(sheetContent = RequestBottomSheetContent.BADGE) + } + is Event.SubtitleClicked -> { showBottomSheet(sheetContent = RequestBottomSheetContent.SUBTITLE) } @@ -187,6 +197,10 @@ abstract class RequestViewModel : MviViewModel() { is Event.BottomSheet.Subtitle.PrimaryButtonPressed -> { hideBottomSheet() } + + is Event.BottomSheet.Badge.PrimaryButtonPressed -> { + hideBottomSheet() + } } } diff --git a/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/interactor/PresentationRequestInteractor.kt b/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/interactor/PresentationRequestInteractor.kt index fc495349..f80247b1 100644 --- a/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/interactor/PresentationRequestInteractor.kt +++ b/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/interactor/PresentationRequestInteractor.kt @@ -33,11 +33,14 @@ import kotlinx.coroutines.flow.mapNotNull sealed class PresentationRequestInteractorPartialState { data class Success( val verifierName: String? = null, + val verifierIsTrusted: Boolean, val requestDocuments: List> ) : PresentationRequestInteractorPartialState() - data class NoData(val verifierName: String? = null) : - PresentationRequestInteractorPartialState() + data class NoData( + val verifierName: String? = null, + val verifierIsTrusted: Boolean, + ) : PresentationRequestInteractorPartialState() data class Failure(val error: String) : PresentationRequestInteractorPartialState() data object Disconnect : PresentationRequestInteractorPartialState() @@ -69,7 +72,8 @@ class PresentationRequestInteractorImpl( is TransferEventPartialState.RequestReceived -> { if (response.requestData.all { it.docRequest.requestItems.isEmpty() }) { PresentationRequestInteractorPartialState.NoData( - verifierName = response.verifierName + verifierName = response.verifierName, + verifierIsTrusted = response.verifierIsTrusted, ) } else { val requestDataUi = RequestTransformer.transformToUiItems( @@ -80,6 +84,7 @@ class PresentationRequestInteractorImpl( ) PresentationRequestInteractorPartialState.Success( verifierName = response.verifierName, + verifierIsTrusted = response.verifierIsTrusted, requestDocuments = requestDataUi ) } diff --git a/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/ui/loading/PresentationLoadingViewModel.kt b/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/ui/loading/PresentationLoadingViewModel.kt index e6b02ea8..961975e3 100644 --- a/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/ui/loading/PresentationLoadingViewModel.kt +++ b/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/ui/loading/PresentationLoadingViewModel.kt @@ -48,13 +48,11 @@ class PresentationLoadingViewModel( override fun getTitle(): String { return if (interactor.verifierName.isNullOrBlank()) { - resourceProvider.getString(R.string.request_title) + resourceProvider.getString(R.string.request_title_before_badge) + + resourceProvider.getString(R.string.request_title_after_badge) } else { - resourceProvider.getString( - R.string.request_title_with_verifier_name, - interactor.verifierName - ?: resourceProvider.getString(R.string.presentation_loading_success_config_verifier) - ) + interactor.verifierName + + resourceProvider.getString(R.string.request_title_after_badge) } } diff --git a/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/ui/request/PresentationRequestViewModel.kt b/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/ui/request/PresentationRequestViewModel.kt index f5955deb..04ab9a24 100644 --- a/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/ui/request/PresentationRequestViewModel.kt +++ b/presentation-feature/src/main/java/eu/europa/ec/presentationfeature/ui/request/PresentationRequestViewModel.kt @@ -27,6 +27,7 @@ import eu.europa.ec.presentationfeature.interactor.PresentationRequestInteractor import eu.europa.ec.resourceslogic.R import eu.europa.ec.resourceslogic.provider.ResourceProvider import eu.europa.ec.uilogic.component.content.ContentErrorConfig +import eu.europa.ec.uilogic.component.content.TitleWithBadge import eu.europa.ec.uilogic.config.ConfigNavigation import eu.europa.ec.uilogic.config.NavigationType import eu.europa.ec.uilogic.navigation.CommonScreens @@ -65,7 +66,7 @@ class PresentationRequestViewModel( mapOf( BiometricUiConfig.serializedKeyName to uiSerializer.toBase64( BiometricUiConfig( - title = viewState.value.screenTitle, + title = viewState.value.screenTitle.plainText, subTitle = resourceProvider.getString(R.string.loading_biometry_share_subtitle), quickPinOnlySubTitle = resourceProvider.getString(R.string.loading_quick_pin_share_subtitle), isPreAuthorization = false, @@ -123,7 +124,10 @@ class PresentationRequestViewModel( isLoading = false, error = null, verifierName = response.verifierName, - screenTitle = getScreenTitle(verifierName), + screenTitle = getScreenTitle( + verifierName = response.verifierName, + verifierIsTrusted = response.verifierIsTrusted + ), items = response.requestDocuments ) } @@ -139,7 +143,10 @@ class PresentationRequestViewModel( isLoading = false, error = null, verifierName = response.verifierName, - screenTitle = getScreenTitle(verifierName), + screenTitle = getScreenTitle( + verifierName = response.verifierName, + verifierIsTrusted = response.verifierIsTrusted + ), noItems = true, ) } @@ -159,11 +166,19 @@ class PresentationRequestViewModel( interactor.stopPresentation() } - private fun getScreenTitle(verifierName: String?): String { - return if (verifierName.isNullOrBlank()) { - resourceProvider.getString(R.string.request_title) + private fun getScreenTitle(verifierName: String?, verifierIsTrusted: Boolean): TitleWithBadge { + val textBeforeBadge = if (verifierName.isNullOrBlank()) { + resourceProvider.getString(R.string.request_title_before_badge) } else { - resourceProvider.getString(R.string.request_title_with_verifier_name, verifierName) + verifierName } + + val textAfterBadge = resourceProvider.getString(R.string.request_title_after_badge) + + return TitleWithBadge( + textBeforeBadge = textBeforeBadge, + textAfterBadge = textAfterBadge, + isTrusted = verifierIsTrusted + ) } } \ No newline at end of file diff --git a/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/interactor/ProximityRequestInteractor.kt b/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/interactor/ProximityRequestInteractor.kt index 4653950b..ecab6def 100644 --- a/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/interactor/ProximityRequestInteractor.kt +++ b/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/interactor/ProximityRequestInteractor.kt @@ -33,10 +33,14 @@ import kotlinx.coroutines.flow.mapNotNull sealed class ProximityRequestInteractorPartialState { data class Success( val verifierName: String? = null, + val verifierIsTrusted: Boolean, val requestDocuments: List> ) : ProximityRequestInteractorPartialState() - data class NoData(val verifierName: String? = null) : ProximityRequestInteractorPartialState() + data class NoData( + val verifierName: String? = null, + val verifierIsTrusted: Boolean, + ) : ProximityRequestInteractorPartialState() data class Failure(val error: String) : ProximityRequestInteractorPartialState() data object Disconnect : ProximityRequestInteractorPartialState() @@ -68,7 +72,8 @@ class ProximityRequestInteractorImpl( is TransferEventPartialState.RequestReceived -> { if (response.requestData.all { it.docRequest.requestItems.isEmpty() }) { ProximityRequestInteractorPartialState.NoData( - verifierName = response.verifierName + verifierName = response.verifierName, + verifierIsTrusted = response.verifierIsTrusted, ) } else { val requestDataUi = RequestTransformer.transformToUiItems( @@ -79,6 +84,7 @@ class ProximityRequestInteractorImpl( ) ProximityRequestInteractorPartialState.Success( verifierName = response.verifierName, + verifierIsTrusted = response.verifierIsTrusted, requestDocuments = requestDataUi ) } diff --git a/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/loading/ProximityLoadingViewModel.kt b/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/loading/ProximityLoadingViewModel.kt index 4f7afc25..faba587d 100644 --- a/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/loading/ProximityLoadingViewModel.kt +++ b/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/loading/ProximityLoadingViewModel.kt @@ -47,13 +47,11 @@ class ProximityLoadingViewModel( override fun getTitle(): String { return if (interactor.verifierName.isNullOrBlank()) { - resourceProvider.getString(R.string.request_title) + resourceProvider.getString(R.string.request_title_before_badge) + + resourceProvider.getString(R.string.request_title_after_badge) } else { - resourceProvider.getString( - R.string.request_title_with_verifier_name, - interactor.verifierName - ?: resourceProvider.getString(R.string.presentation_loading_success_config_verifier) - ) + interactor.verifierName + + resourceProvider.getString(R.string.request_title_after_badge) } } diff --git a/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/qr/ProximityQRViewModel.kt b/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/qr/ProximityQRViewModel.kt index faa4e998..23a26640 100644 --- a/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/qr/ProximityQRViewModel.kt +++ b/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/qr/ProximityQRViewModel.kt @@ -83,6 +83,7 @@ class ProximityQRViewModel( is Event.GoBack -> { cleanUp() + setState { copy(error = null) } setEffect { Effect.Navigation.Pop } } diff --git a/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/request/ProximityRequestViewModel.kt b/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/request/ProximityRequestViewModel.kt index e3175a54..67794142 100644 --- a/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/request/ProximityRequestViewModel.kt +++ b/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/request/ProximityRequestViewModel.kt @@ -27,6 +27,7 @@ import eu.europa.ec.proximityfeature.interactor.ProximityRequestInteractorPartia import eu.europa.ec.resourceslogic.R import eu.europa.ec.resourceslogic.provider.ResourceProvider import eu.europa.ec.uilogic.component.content.ContentErrorConfig +import eu.europa.ec.uilogic.component.content.TitleWithBadge import eu.europa.ec.uilogic.config.ConfigNavigation import eu.europa.ec.uilogic.config.NavigationType import eu.europa.ec.uilogic.navigation.CommonScreens @@ -65,7 +66,7 @@ class ProximityRequestViewModel( mapOf( BiometricUiConfig.serializedKeyName to uiSerializer.toBase64( BiometricUiConfig( - title = viewState.value.screenTitle, + title = viewState.value.screenTitle.plainText, subTitle = resourceProvider.getString(R.string.loading_biometry_share_subtitle), quickPinOnlySubTitle = resourceProvider.getString(R.string.loading_quick_pin_share_subtitle), isPreAuthorization = false, @@ -123,7 +124,10 @@ class ProximityRequestViewModel( isLoading = false, error = null, verifierName = response.verifierName, - screenTitle = getScreenTitle(verifierName), + screenTitle = getScreenTitle( + verifierName = response.verifierName, + verifierIsTrusted = response.verifierIsTrusted + ), items = response.requestDocuments ) } @@ -139,7 +143,10 @@ class ProximityRequestViewModel( isLoading = false, error = null, verifierName = response.verifierName, - screenTitle = getScreenTitle(verifierName), + screenTitle = getScreenTitle( + verifierName = response.verifierName, + verifierIsTrusted = response.verifierIsTrusted + ), noItems = true, ) } @@ -159,11 +166,19 @@ class ProximityRequestViewModel( interactor.stopPresentation() } - private fun getScreenTitle(verifierName: String?): String { - return if (verifierName.isNullOrBlank()) { - resourceProvider.getString(R.string.request_title) + private fun getScreenTitle(verifierName: String?, verifierIsTrusted: Boolean): TitleWithBadge { + val textBeforeBadge = if (verifierName.isNullOrBlank()) { + resourceProvider.getString(R.string.request_title_before_badge) } else { - resourceProvider.getString(R.string.request_title_with_verifier_name, verifierName) + verifierName } + + val textAfterBadge = resourceProvider.getString(R.string.request_title_after_badge) + + return TitleWithBadge( + textBeforeBadge = textBeforeBadge, + textAfterBadge = textAfterBadge, + isTrusted = verifierIsTrusted + ) } } \ No newline at end of file diff --git a/resources-logic/src/main/res/drawable/ic_verified.xml b/resources-logic/src/main/res/drawable/ic_verified.xml new file mode 100644 index 00000000..ddcadf24 --- /dev/null +++ b/resources-logic/src/main/res/drawable/ic_verified.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/resources-logic/src/main/res/values/strings.xml b/resources-logic/src/main/res/values/strings.xml index ba4c1e17..d8c1e79c 100644 --- a/resources-logic/src/main/res/values/strings.xml +++ b/resources-logic/src/main/res/values/strings.xml @@ -57,6 +57,7 @@ Hide Add Edit + Verified National ID @@ -107,8 +108,8 @@ - Verifier requests the following - %1$s requests the following + Verifier + \ requests the following The requested document is not available in your EUDI Wallet. Please contact the authorised issuer for further information. Please review carefully before sharing your data. @@ -123,6 +124,9 @@ Why we need your data It allows us to verify your identity with the requesting party @string/generic_ok + Trusted relying party + A relying party is considered trusted when it meets predefined criteria for security, data protection, compliance, and responsible data handling. Trust is reinforced through assessments, audits, and certifications. + @string/generic_ok Verification Data male female 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 016a7048..321c87ce 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 @@ -175,4 +175,10 @@ object AppIcons { contentDescriptionId = R.string.content_description_qr_scanner_icon, imageVector = null ) + + val Verified: IconData = IconData( + resourceId = R.drawable.ic_verified, + contentDescriptionId = R.string.content_description_verified_icon, + imageVector = null + ) } \ No newline at end of file diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/content/ContentTitle.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/content/ContentTitle.kt index 2adb6793..2848c82b 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/content/ContentTitle.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/content/ContentTitle.kt @@ -26,22 +26,31 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import eu.europa.ec.businesslogic.util.safeLet import eu.europa.ec.resourceslogic.theme.values.textPrimaryDark import eu.europa.ec.resourceslogic.theme.values.textSecondaryDark +import eu.europa.ec.uilogic.component.AppIcons import eu.europa.ec.uilogic.component.utils.SPACING_MEDIUM import eu.europa.ec.uilogic.component.utils.VSpacer +import eu.europa.ec.uilogic.component.wrap.WrapIcon +import eu.europa.ec.uilogic.extension.clickableNoRipple import eu.europa.ec.uilogic.extension.throttledClickable /** @@ -56,6 +65,8 @@ import eu.europa.ec.uilogic.extension.throttledClickable @Composable fun ContentTitle( title: String? = null, + titleWithBadge: TitleWithBadge? = null, + onTitleWithBadgeClick: (() -> Unit)? = null, titleStyle: TextStyle = MaterialTheme.typography.headlineSmall.copy( color = MaterialTheme.colorScheme.textPrimaryDark ), @@ -92,7 +103,29 @@ fun ContentTitle( Column( horizontalAlignment = Alignment.Start ) { - if (!title.isNullOrEmpty()) { + if (titleWithBadge != null) { + val inlineContentMap = mapOf( + "badgeIconId" to InlineTextContent( + Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter) + ) { + WrapIcon( + iconData = AppIcons.Verified, + customTint = Color.Green + ) + } + ) + + Text( + modifier = onTitleWithBadgeClick?.let { + Modifier.clickableNoRipple( + onClick = it + ) + } ?: Modifier, + text = titleWithBadge.annotatedString, + style = titleStyle, + inlineContent = inlineContentMap, + ) + } else if (!title.isNullOrEmpty()) { Text( text = title, style = titleStyle, @@ -170,4 +203,33 @@ fun ContentTitle( ) } } +} + +data class TitleWithBadge( + private val textBeforeBadge: String? = null, + private val textAfterBadge: String? = null, + val isTrusted: Boolean +) { + val annotatedString = buildAnnotatedString { + if (!textBeforeBadge.isNullOrEmpty()) { + append(textBeforeBadge) + } + if (isTrusted) { + append(" ") + appendInlineContent(id = "badgeIconId") + } + if (!textAfterBadge.isNullOrEmpty()) { + append(textAfterBadge) + } + } + + val plainText: String + get() = buildString { + if (!textBeforeBadge.isNullOrEmpty()) { + append(textBeforeBadge) + } + if (!textAfterBadge.isNullOrEmpty()) { + append(textAfterBadge) + } + } } \ No newline at end of file