diff --git a/business-logic/src/main/java/eu/europa/ec/businesslogic/validator/FormValidator.kt b/business-logic/src/main/java/eu/europa/ec/businesslogic/validator/FormValidator.kt index 12fc6ef2..433d0fff 100644 --- a/business-logic/src/main/java/eu/europa/ec/businesslogic/validator/FormValidator.kt +++ b/business-logic/src/main/java/eu/europa/ec/businesslogic/validator/FormValidator.kt @@ -22,15 +22,9 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil import eu.europa.ec.businesslogic.controller.log.LogController import eu.europa.ec.businesslogic.util.safeLet import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext interface FormValidator { - fun validateFormFlow(form: Form): Flow - fun validateFormsFlow(forms: List
): Flow - suspend fun validateForm(form: Form): FormValidationResult suspend fun validateForms(forms: List): FormsValidationResult } @@ -39,18 +33,6 @@ class FormValidatorImpl( private val logController: LogController ) : FormValidator { - override fun validateFormFlow(form: Form): Flow = flow { - form.inputs.forEach { (rules, value) -> - rules.forEach { rule -> - validateRule(rule, value)?.let { - emit(it) - return@flow - } - } - } - emit(FormValidationResult(isValid = true)) - }.flowOn(Dispatchers.IO) - override suspend fun validateForm(form: Form): FormValidationResult = withContext(Dispatchers.IO) { for (input in form.inputs) { @@ -64,22 +46,6 @@ class FormValidatorImpl( return@withContext FormValidationResult(isValid = true) } - override fun validateFormsFlow(forms: List): Flow = flow { - val errors = mutableListOf() - var isValid = true - forms.forEach { form -> - form.inputs.forEach { (rules, value) -> - rules.forEach { rule -> - validateRule(rule, value)?.let { - isValid = false - errors.add(it.message) - } - } - } - } - emit(FormsValidationResult(isValid, errors)) - }.flowOn(Dispatchers.IO) - override suspend fun validateForms(forms: List): FormsValidationResult = withContext(Dispatchers.IO) { val errorMessages = mutableListOf() diff --git a/business-logic/src/test/java/eu/europa/ec/businesslogic/validator/TestFormValidator.kt b/business-logic/src/test/java/eu/europa/ec/businesslogic/validator/TestFormValidator.kt index ab3919df..670c07ce 100644 --- a/business-logic/src/test/java/eu/europa/ec/businesslogic/validator/TestFormValidator.kt +++ b/business-logic/src/test/java/eu/europa/ec/businesslogic/validator/TestFormValidator.kt @@ -62,12 +62,12 @@ class TestFormValidator { validateForm( rules = rules, value = "", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "test", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) } @@ -79,59 +79,79 @@ class TestFormValidator { validateForm( rules = rules, value = "", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "invalid_url", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "123456789", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "http://example.com", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "https://notarealproject.com/otherpath", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "https://notarealproject.com/bad_query_param?", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "ftp://projectsite.com", - validationResult = validationError + expectedValidationResult = validationError ) // Test with valid URLs validateForm( rules = rules, value = "mocked_scheme://mocked_host?mocked_query_param=some_value", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "mocked_scheme://mocked_host?mocked_query_param1=some_value1&mocked_query_param2=some_value2", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "mocked-scheme://mocked-host?mocked-query-param=some-value", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "mocked.scheme://mocked.host?mocked.query.param=some.value", - validationResult = validationSuccess + expectedValidationResult = validationSuccess + ) + validateForm( + rules = rules, + value = "eudi-openid4vp%3A%2F%2Fdev.verifier-backend.eudiw.dev%3Fclient_id%3Ddev.verifier-backend.eudiw.dev%26request_uri%3Dhttps%3A%2F%2Fdev.verifier-backend.eudiw.dev%2Fwallet%2Frequest.jwt%2F1234", + expectedValidationResult = validationSuccess + ) + validateForm( + rules = rules, + value = "openid-credential-offer://credential_offer?credential_offer=%7B%22credential_issuer%22:%20%22https://dev.issuer.eudiw.dev%22%2C%20%22credential_configuration_ids%22:%20%5B%22eu.europa.ec.eudi.pid_mdoc%22%5D%2C%20%22grants%22:%20%7B%22urn:ietf:params:oauth:grant-type:pre-authorized_code%22:%20%7B%22pre-authorized_code%22:%20%22some_code%22%2C%20%22tx_code%22:%20%7B%22length%22:%205%2C%20%22input_mode%22:%20%22numeric%22%2C%20%22description%22:%20%22Please%20provide%20the%20one-time%20code.%22%7D%7D%7D%7D", + expectedValidationResult = validationSuccess + ) + validateForm( + rules = rules, + value = "eudi-openid4vp://dev.verifier-backend.eudiw.dev?client_id=dev.verifier-backend.eudiw.dev&request_uri=https://dev.verifier-backend.eudiw.dev/wallet/request.jwt/1234", + expectedValidationResult = validationSuccess + ) + validateForm( + rules = rules, + value = "openid-credential-offer://credential_offer?credential_offer={\"credential_issuer\": \"https://dev.issuer.eudiw.dev\", \"credential_configuration_ids\": [\"eu.europa.ec.eudi.pid_mdoc\"], \"grants\": {\"urn:ietf:params:oauth:grant-type:pre-authorized_code\": {\"pre-authorized_code\": \"some_code\", \"tx_code\": {\"length\": 5, \"input_mode\": \"numeric\", \"description\": \"Please provide the one-time code.\"}}}}", + expectedValidationResult = validationSuccess ) } @@ -142,12 +162,12 @@ class TestFormValidator { validateForm( rules = rules, value = "test@test", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "test@test.com", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) } @@ -158,27 +178,27 @@ class TestFormValidator { validateForm( rules = greekPhoneRule, value = "1111111111", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = greekPhoneRule, value = "15223433333", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = greekPhoneRule, value = "6941111111", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = usPhoneRule, value = "6941111111", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = usPhoneRule, value = "6102458772", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) } @@ -188,22 +208,22 @@ class TestFormValidator { validateForm( rules = rules, value = "123456789", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "12345678901", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "1234567890", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "123456789012", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) } @@ -213,22 +233,22 @@ class TestFormValidator { validateForm( rules = rules, value = "aaaaaaaaaaa", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = " ", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "aaaaaaaaa", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "aaaaaaaaaa", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) } @@ -238,22 +258,22 @@ class TestFormValidator { validateForm( rules = rules, value = "", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "aaaa", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "aaaaa", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "aaaaaaaaa", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) } @@ -269,64 +289,64 @@ class TestFormValidator { validateForm( rules = atLeast2DigitsRule, value = "Test12", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = atLeast2DigitsRule, value = "Test123", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = atLeast2DigitsRule, value = "Test1", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = atLeast2DigitsRule, value = "Test", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = atLeast2SpecialCharsRule, value = "Test!@", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = atLeast2SpecialCharsRule, value = "Test!@#", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = atLeast2SpecialCharsRule, value = "Test!", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = atLeast2SpecialCharsRule, value = "Test", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = atLeast3UppercaseCharactersRule, value = "TestAA", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = atLeast3UppercaseCharactersRule, value = "TAestAA", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = atLeast3UppercaseCharactersRule, value = "TestA", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = atLeast3UppercaseCharactersRule, value = "Test", - validationResult = validationError + expectedValidationResult = validationError ) } @@ -341,32 +361,32 @@ class TestFormValidator { validateForm( rules = atLeastOneCapitalLetterRule, value = "test", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = atLeastOneCapitalLetterRule, value = "Test", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = atLeastOneDigitRule, value = "test", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = atLeastOneDigitRule, value = "test1", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = atLeastOneSpecialCharRule, value = "test", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = atLeastOneSpecialCharRule, value = "test1@", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) } @@ -376,17 +396,17 @@ class TestFormValidator { validateForm( rules = rules, value = "aaaaaaaaaaa", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "aaaaaaaaaa", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) } @@ -397,37 +417,37 @@ class TestFormValidator { validateForm( rules = isNotCaseSensitiveRule, value = "testt", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = isNotCaseSensitiveRule, value = "t!@#$", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = isNotCaseSensitiveRule, value = "", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = isNotCaseSensitiveRule, value = "TEST", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = isCaseSensitiveRule, value = "TEST", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = isCaseSensitiveRule, value = "", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = isCaseSensitiveRule, value = "test", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) } @@ -439,42 +459,42 @@ class TestFormValidator { validateForm( rules = isNotCaseSensitiveRule, value = "test", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = isNotCaseSensitiveRule, value = "TEST", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = isNotCaseSensitiveRule, value = "", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = isNotCaseSensitiveRule, value = "testt", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = isCaseSensitiveRule, value = "test", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = isCaseSensitiveRule, value = "Testss", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = isCaseSensitiveRule, value = "TEST", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = isCaseSensitiveRule, value = "", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) } @@ -495,57 +515,57 @@ class TestFormValidator { validateForm( rules = maxTimesOfConsecutiveOrder2Rule, value = "1313", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = maxTimesOfConsecutiveOrder2Rule, value = "4356754", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = maxTimesOfConsecutiveOrder2Rule, value = "0191", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = maxTimesOfConsecutiveOrder2Rule, value = "1225", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = maxTimesOfConsecutiveOrder2Rule, value = "2332", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = maxTimesOfConsecutiveOrder2Rule, value = "454545454545454555444455455", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = maxTimesOfConsecutiveOrder2Rule, value = "0024", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = maxTimesOfConsecutiveOrder4Rule, value = "0024", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = maxTimesOfConsecutiveOrder4Rule, value = "0004", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = maxTimesOfConsecutiveOrder4Rule, value = "0000", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = maxTimesOfConsecutiveOrder4Rule, value = "35523456337777", - validationResult = validationError + expectedValidationResult = validationError ) } @@ -555,62 +575,62 @@ class TestFormValidator { validateForm( rules = rules, value = "1234", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "4321", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "9876", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "3210", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "0123", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "1235", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "9875", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "0124", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "0923", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "8834834835939534", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "TEST", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) } @@ -692,23 +712,22 @@ class TestFormValidator { ) ) - formValidation.validateFormsFlow(forms).collect { - assertEquals( - FormsValidationResult( - false, - listOf( - "ValidateDuplicateCharacterNotInConsecutiveOrder", - "ValidateEmail", - "ValidatePhoneNumber", - "ValidateRegex", - "ValidateEmail", - "ValidateStringNotMatch", - "ValidateDuplicateCharacterNotInConsecutiveOrder", - "ValidateRegex" - ) - ), it + val expectedValidationResult = FormsValidationResult( + isValid = false, + messages = listOf( + "ValidateDuplicateCharacterNotInConsecutiveOrder", + "ValidateEmail", + "ValidatePhoneNumber", + "ValidateRegex", + "ValidateEmail", + "ValidateStringNotMatch", + "ValidateDuplicateCharacterNotInConsecutiveOrder", + "ValidateRegex" ) - } + ) + val actualValidationResult = formValidation.validateForms(forms) + + assertEquals(expectedValidationResult, actualValidationResult) } @Test @@ -717,27 +736,27 @@ class TestFormValidator { validateForm( rules = rules, value = "1.", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "1,", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "1*", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "1.00", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "1,00", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) } @@ -755,23 +774,23 @@ class TestFormValidator { validateForm( rules = rules, value = "fileame.pdf", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "asdf", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "lalala.doc", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "lala.ppt", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) } @@ -788,18 +807,18 @@ class TestFormValidator { validateForm( rules = rules, value = "200", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "100", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "99", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) } @@ -815,25 +834,25 @@ class TestFormValidator { validateForm( rules = rules, value = "test", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "4", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "5", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "6", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) } @@ -849,37 +868,36 @@ class TestFormValidator { validateForm( rules = rules, value = "test", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "6", - validationResult = validationError + expectedValidationResult = validationError ) validateForm( rules = rules, value = "5", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) validateForm( rules = rules, value = "4", - validationResult = validationSuccess + expectedValidationResult = validationSuccess ) } private suspend fun validateForm( rules: List, value: String, - validationResult: FormValidationResult + expectedValidationResult: FormValidationResult ) { - formValidation.validateFormFlow( + val actualValidationResult = formValidation.validateForm( Form(mapOf(rules to value)) - ).collect { - assertEquals(validationResult, it) - } + ) + assertEquals(expectedValidationResult, actualValidationResult) } } \ No newline at end of file diff --git a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/pin/PinViewModel.kt b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/pin/PinViewModel.kt index 391f3ee4..8a434061 100644 --- a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/pin/PinViewModel.kt +++ b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/pin/PinViewModel.kt @@ -305,16 +305,15 @@ class PinViewModel( private fun validateForm(pin: String) { viewModelScope.launch { - interactor.validateFormFlow(getListOfRules(pin)).collect { - setState { - copy( - validationResult = it, - isButtonEnabled = it.isValid, - quickPinError = it.message, - pin = pin, - resetPin = false - ) - } + val validationResult = interactor.validateForm(getListOfRules(pin)) + setState { + copy( + validationResult = validationResult, + isButtonEnabled = validationResult.isValid, + quickPinError = validationResult.message, + pin = pin, + resetPin = false + ) } } } diff --git a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/qr_scan/QrScanScreen.kt b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/qr_scan/QrScanScreen.kt index 242ff203..6f81c314 100644 --- a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/qr_scan/QrScanScreen.kt +++ b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/qr_scan/QrScanScreen.kt @@ -22,7 +22,6 @@ import androidx.camera.core.ImageAnalysis import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -31,6 +30,7 @@ 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.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -57,13 +57,12 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.shouldShowRationale -import eu.europa.ec.commonfeature.config.QrScanFlow -import eu.europa.ec.commonfeature.config.QrScanUiConfig import eu.europa.ec.commonfeature.ui.qr_scan.component.QrCodeAnalyzer import eu.europa.ec.commonfeature.ui.qr_scan.component.qrBorderCanvas import eu.europa.ec.resourceslogic.R import eu.europa.ec.uilogic.component.AppIcons import eu.europa.ec.uilogic.component.ErrorInfo +import eu.europa.ec.uilogic.component.PreMeasuredContentWithAnimatedVisibility import eu.europa.ec.uilogic.component.content.ContentScreen import eu.europa.ec.uilogic.component.content.ContentTitle import eu.europa.ec.uilogic.component.content.ScreenNavigateAction @@ -168,7 +167,12 @@ private fun Content( VSpacer.Large() - InformativeText(state) + PreMeasuredContentWithAnimatedVisibility( + modifier = Modifier.fillMaxWidth(), + showContent = state.showInformativeText + ) { + InformativeText(text = state.informativeText) + } } } @@ -294,22 +298,20 @@ private fun OpenCamera( } @Composable -private fun InformativeText(state: State) { - AnimatedVisibility(state.showInformativeText) { - WrapCard { - Row( +private fun InformativeText(text: String) { + WrapCard { + Row( + modifier = Modifier.padding(all = SPACING_SMALL.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + WrapIcon(AppIcons.Error) + Text( modifier = Modifier.padding(all = SPACING_SMALL.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - WrapIcon(AppIcons.Error) - Text( - modifier = Modifier.padding(all = SPACING_SMALL.dp), - text = state.informativeText, - style = MaterialTheme.typography.headlineLarge, - textAlign = TextAlign.Center - ) - } + text = text, + style = MaterialTheme.typography.headlineLarge, + textAlign = TextAlign.Center + ) } } } @@ -319,11 +321,7 @@ private fun InformativeText(state: State) { private fun InformativeTextPreview() { PreviewTheme { InformativeText( - state = State( - qrScannedConfig = QrScanUiConfig("title", "subtitle", QrScanFlow.Presentation), - showInformativeText = true, - informativeText = stringResource(R.string.qr_scan_informative_text_presentation_flow) - ) + text = stringResource(R.string.qr_scan_informative_text_presentation_flow) ) } } \ No newline at end of file diff --git a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/qr_scan/QrScanViewModel.kt b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/qr_scan/QrScanViewModel.kt index 912939ea..51c963e5 100644 --- a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/qr_scan/QrScanViewModel.kt +++ b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/qr_scan/QrScanViewModel.kt @@ -55,7 +55,7 @@ data class State( val failedScanAttempts: Int = 0, val showInformativeText: Boolean = false, - val informativeText: String = "", + val informativeText: String, ) : ViewState sealed class Event : ViewEvent { @@ -82,13 +82,17 @@ class QrScanViewModel( @InjectedParam private val qrScannedConfig: String, ) : MviViewModel() { - override fun setInitialState(): State = State( - qrScannedConfig = uiSerializer.fromBase64( + override fun setInitialState(): State { + val deserializedConfig: QrScanUiConfig = uiSerializer.fromBase64( qrScannedConfig, QrScanUiConfig::class.java, QrScanUiConfig.Parser ) ?: throw RuntimeException("QrScanUiConfig:: is Missing or invalid") - ) + return State( + qrScannedConfig = deserializedConfig, + informativeText = calculateInformativeText(deserializedConfig.qrScanFlow) + ) + } override fun handleEvents(event: Event) { when (event) { @@ -123,33 +127,33 @@ class QrScanViewModel( private fun handleScannedQr(scannedQr: String) { viewModelScope.launch { + val currentState = viewState.value + + // Validate the scanned QR code val urlIsValid = validateForm( form = Form( inputs = mapOf( - listOf( - Rule.ValidateProjectUrl(errorMessage = "") - ) to scannedQr + listOf(Rule.ValidateProjectUrl(errorMessage = "")) to scannedQr ) ) ) + // Handle valid QR code if (urlIsValid) { calculateNextStep( - qrScanFlow = viewState.value.qrScannedConfig.qrScanFlow, + qrScanFlow = currentState.qrScannedConfig.qrScanFlow, scanResult = scannedQr ) - } else if (viewState.value.failedScanAttempts < MAX_ALLOWED_FAILED_SCANS) { - setState { - copy( - failedScanAttempts = viewState.value.failedScanAttempts + 1, - finishedScanning = false - ) - } } else { + // Increment failed attempts + val updatedFailedAttempts = currentState.failedScanAttempts + 1 + val maxFailedAttemptsExceeded = updatedFailedAttempts > MAX_ALLOWED_FAILED_SCANS + setState { copy( - showInformativeText = true, - informativeText = calculateInformativeText(viewState.value.qrScannedConfig.qrScanFlow) + failedScanAttempts = updatedFailedAttempts, + showInformativeText = maxFailedAttemptsExceeded, + finishedScanning = false, ) } } diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/PreMeasuredContentWithAnimatedVisibility.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/PreMeasuredContentWithAnimatedVisibility.kt new file mode 100644 index 00000000..edc95ebd --- /dev/null +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/PreMeasuredContentWithAnimatedVisibility.kt @@ -0,0 +1,87 @@ +/* + * 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.uilogic.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.platform.LocalDensity + +/** + * A composable function that measures the height of its content when it's not yet visible + * and dynamically reserves space for it. This ensures smooth animations when the content + * becomes visible, as the layout doesn't need to adjust to the content's size. + + * @param modifier Modifier to be applied to the layout. + * @param showContent Whether the content should be visible. + * @param content The composable content to be measured and displayed. + */ +@Composable +fun PreMeasuredContentWithAnimatedVisibility( + modifier: Modifier = Modifier, + showContent: Boolean, + content: @Composable () -> Unit +) { + // State to store the measured height of the content + var contentHeight by remember { mutableIntStateOf(0) } + var isHeightMeasured by remember { mutableStateOf(false) } + + // Box to reserve space dynamically + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + // SubcomposeLayout to pre-measure the content only if not already measured + if (!isHeightMeasured) { + SubcomposeLayout { constraints -> + // Subcompose the content when it's not yet visible to measure its height + val contentPlaceables = subcompose("Content") { + content() + }.map { it.measure(constraints) } + + // Update height only if it is not yet measured + if (contentHeight == 0 && contentPlaceables.isNotEmpty()) { + contentHeight = contentPlaceables.maxOf { it.height } + isHeightMeasured = true // Cache height to prevent further measurement + } + + // Reserve the required space dynamically based on measured height + layout(width = constraints.maxWidth, height = contentHeight) { + // We don't draw anything here, just measure the space + } + } + } + + // The actual content that is visible when showContent is true + AnimatedVisibility(visible = showContent) { + content() + } + + // Spacer to dynamically reserve space based on the height of the content + Spacer(modifier = Modifier.height(with(LocalDensity.current) { contentHeight.toDp() })) + } +} \ No newline at end of file