From c1433a11eb2dbfa115993ad5e983f70383bd9233 Mon Sep 17 00:00:00 2001 From: Stilianos Tzouvaras Date: Thu, 12 Sep 2024 03:24:57 +0300 Subject: [PATCH 1/7] WrapPinTextField Refactor to improve user experience --- .../ui/biometric/BiometricScreen.kt | 3 +- .../commonfeature/ui/success/SuccessScreen.kt | 5 +- .../uilogic/component/content/ContentError.kt | 3 +- .../ec/uilogic/component/snackbar/Snackbar.kt | 8 +- .../ec/uilogic/component/wrap/WrapCard.kt | 3 +- .../component/wrap/WrapPinTextField.kt | 77 ++++++++++++------- .../uilogic/component/wrap/WrapTextField.kt | 7 +- 7 files changed, 68 insertions(+), 38 deletions(-) diff --git a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/biometric/BiometricScreen.kt b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/biometric/BiometricScreen.kt index 7dd6523a..63e5bbf8 100644 --- a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/biometric/BiometricScreen.kt +++ b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/biometric/BiometricScreen.kt @@ -41,6 +41,7 @@ import eu.europa.ec.uilogic.component.content.ScreenNavigateAction import eu.europa.ec.uilogic.component.preview.PreviewTheme import eu.europa.ec.uilogic.component.preview.ThemeModePreviews import eu.europa.ec.uilogic.component.utils.OneTimeLaunchedEffect +import eu.europa.ec.uilogic.component.utils.SIZE_MEDIUM import eu.europa.ec.uilogic.component.wrap.WrapIconButton import eu.europa.ec.uilogic.component.wrap.WrapPinTextField import eu.europa.ec.uilogic.config.ConfigNavigation @@ -281,7 +282,7 @@ private fun PreviewBiometricScreen() { effectFlow = Channel().receiveAsFlow(), onEventSent = {}, onNavigationRequested = {}, - padding = PaddingValues(16.dp) + padding = PaddingValues(SIZE_MEDIUM.dp) ) } } \ No newline at end of file diff --git a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/success/SuccessScreen.kt b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/success/SuccessScreen.kt index 1cd00336..d9e343bb 100644 --- a/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/success/SuccessScreen.kt +++ b/common-feature/src/main/java/eu/europa/ec/commonfeature/ui/success/SuccessScreen.kt @@ -50,6 +50,7 @@ import eu.europa.ec.uilogic.component.content.ContentTitle import eu.europa.ec.uilogic.component.content.ScreenNavigateAction import eu.europa.ec.uilogic.component.preview.PreviewTheme import eu.europa.ec.uilogic.component.preview.ThemeModePreviews +import eu.europa.ec.uilogic.component.utils.SIZE_MEDIUM import eu.europa.ec.uilogic.component.wrap.WrapPrimaryButton import eu.europa.ec.uilogic.component.wrap.WrapSecondaryButton import eu.europa.ec.uilogic.config.ConfigNavigation @@ -260,7 +261,7 @@ private fun SuccessDefaultPreview() { effectFlow = Channel().receiveAsFlow(), onEventSent = {}, onNavigationRequested = {}, - paddingValues = PaddingValues(16.dp) + paddingValues = PaddingValues(SIZE_MEDIUM.dp) ) } } @@ -300,7 +301,7 @@ private fun SuccessDrawablePreview() { effectFlow = Channel().receiveAsFlow(), onEventSent = {}, onNavigationRequested = {}, - paddingValues = PaddingValues(16.dp) + paddingValues = PaddingValues(SIZE_MEDIUM.dp) ) } } \ No newline at end of file diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/content/ContentError.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/content/ContentError.kt index dd80a5cd..2aaa443d 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/content/ContentError.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/content/ContentError.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.unit.dp import eu.europa.ec.resourceslogic.R import eu.europa.ec.uilogic.component.preview.PreviewTheme import eu.europa.ec.uilogic.component.preview.ThemeModePreviews +import eu.europa.ec.uilogic.component.utils.SIZE_MEDIUM import eu.europa.ec.uilogic.component.wrap.WrapPrimaryButton @Composable @@ -82,7 +83,7 @@ private fun PreviewContentErrorScreen() { PreviewTheme { ContentError( config = ContentErrorConfig(onCancel = {}), - paddingValues = PaddingValues(16.dp) + paddingValues = PaddingValues(SIZE_MEDIUM.dp) ) } } \ No newline at end of file diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/snackbar/Snackbar.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/snackbar/Snackbar.kt index cef309ea..9c58f2d6 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/snackbar/Snackbar.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/snackbar/Snackbar.kt @@ -58,6 +58,8 @@ import eu.europa.ec.resourceslogic.theme.values.onSuccess import eu.europa.ec.resourceslogic.theme.values.success import eu.europa.ec.uilogic.component.snackbar.Snackbar.Builder import eu.europa.ec.uilogic.component.snackbar.Snackbar.SnackbarType +import eu.europa.ec.uilogic.component.utils.SIZE_MEDIUM +import eu.europa.ec.uilogic.component.utils.SIZE_SMALL import eu.europa.ec.uilogic.component.utils.Z_SNACKBAR import kotlinx.coroutines.launch import java.util.UUID @@ -262,8 +264,8 @@ class Snackbar private constructor(private val data: SnackbarValue) { ) { Box( modifier = Modifier.padding( - vertical = 8.dp, - horizontal = 16.dp + vertical = SIZE_SMALL.dp, + horizontal = SIZE_MEDIUM.dp ), ) { if (data.hasAction()) SnackbarSimpleAction(data) @@ -334,7 +336,7 @@ class Snackbar private constructor(private val data: SnackbarValue) { @Composable private fun SnackbarSimple(data: SnackbarValue) { return Text( - modifier = Modifier.padding(vertical = 8.dp), + modifier = Modifier.padding(vertical = SIZE_SMALL.dp), text = data.message, maxLines = 2, overflow = TextOverflow.Ellipsis diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapCard.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapCard.kt index 7097752f..10144449 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapCard.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapCard.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp import eu.europa.ec.resourceslogic.theme.values.backgroundDefault import eu.europa.ec.uilogic.component.preview.PreviewTheme import eu.europa.ec.uilogic.component.preview.ThemeModePreviews +import eu.europa.ec.uilogic.component.utils.SIZE_MEDIUM import eu.europa.ec.uilogic.extension.throttledClickable @Composable @@ -44,7 +45,7 @@ fun WrapCard( colors: CardColors? = null, content: @Composable ColumnScope.() -> Unit ) { - val cardShape = shape ?: RoundedCornerShape(16.dp) + val cardShape = shape ?: RoundedCornerShape(SIZE_MEDIUM.dp) val cardColors = colors ?: CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.backgroundDefault ) diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt index 8aea62ef..0c46f0f8 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt @@ -24,10 +24,14 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.selection.LocalTextSelectionColors +import androidx.compose.foundation.text.selection.TextSelectionColors import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -36,6 +40,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.platform.LocalSoftwareKeyboardController @@ -53,6 +58,7 @@ import eu.europa.ec.uilogic.component.preview.ThemeModePreviews import eu.europa.ec.uilogic.component.utils.EmptyTextToolbar import eu.europa.ec.uilogic.component.utils.HSpacer import eu.europa.ec.uilogic.component.utils.OneTimeLaunchedEffect +import eu.europa.ec.uilogic.component.utils.SIZE_SMALL import eu.europa.ec.uilogic.component.utils.SPACING_SMALL @Composable @@ -68,6 +74,16 @@ fun WrapPinTextField( clearCode: Boolean = false, focusOnCreate: Boolean = false ) { + + fun List.clearFocus() { + this.forEach { it.freeFocus() } + } + + fun List.requestFocus(index: Int) { + this.clearFocus() + this.elementAtOrNull(index)?.requestFocus() + } + // Text field range. val fieldsRange = 0 until length @@ -99,11 +115,15 @@ fun WrapPinTextField( it.value = "" onPinUpdate.invoke("") } - focusRequesters.first().requestFocus() + focusRequesters.requestFocus(0) } CompositionLocalProvider( - LocalTextToolbar provides EmptyTextToolbar + LocalTextToolbar provides EmptyTextToolbar, + LocalTextSelectionColors provides TextSelectionColors( + handleColor = Color.Transparent, + backgroundColor = Color.Transparent + ) ) { Column(modifier = modifier) { Row( @@ -124,52 +144,52 @@ fun WrapPinTextField( onKeyEvent = { if (it.key == Key.Backspace) { if (textFieldStateList[currentTextField].value.isNotEmpty()) { + textFieldStateList[currentTextField].value = "" - } else { - focusRequesters.elementAtOrNull(currentTextField - 1) - ?.requestFocus() - } - // Notify listener. - onPinUpdate.invoke( - textFieldStateList.joinToString( - separator = "", - transform = { textField -> - textField.value - } + + // Notify listener. + onPinUpdate.invoke( + textFieldStateList.joinToString( + separator = "", + transform = { textField -> + textField.value + } + ) ) - ) + } + focusRequesters.requestFocus(currentTextField - 1) true } else { false } }, + shape = RoundedCornerShape(SIZE_SMALL.dp), value = textFieldStateList[currentTextField].value, textStyle = LocalTextStyle.current.copy( fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + ), + colors = OutlinedTextFieldDefaults.colors().copy( + cursorColor = Color.Transparent, + errorCursorColor = Color.Transparent ), visualTransformation = visualTransformation, isError = hasError, onValueChange = { newText: String -> // Set new value, only if it is not blank. - if (newText.isNotBlank()) { + if (newText.isNotBlank() && newText != textFieldStateList[currentTextField].value) { textFieldStateList[currentTextField].value = newText.replaceFirst( textFieldStateList[currentTextField].value, "" ) - // Move to next cell if we are not on the last one. - if (currentTextField < length - 1) { - focusRequesters[currentTextField + 1].requestFocus() - } - // Check if all fields are valid. if (!textFieldStateList.any { textField -> textField.value.isEmpty() }) { + focusRequesters.clearFocus() keyboardController?.hide() - focusRequesters.forEach { - it.freeFocus() - } + } else if (currentTextField < fieldsRange.last) { + focusRequesters.requestFocus(currentTextField + 1) } // Notify listener. onPinUpdate.invoke( @@ -184,15 +204,14 @@ fun WrapPinTextField( }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.NumberPassword, - imeAction = when (currentTextField < length - 1) { + imeAction = when (currentTextField < fieldsRange.last) { true -> ImeAction.Next false -> ImeAction.Done } ), keyboardActions = KeyboardActions( onNext = { - focusRequesters.elementAtOrNull(currentTextField + 1) - ?.requestFocus() + focusRequesters.requestFocus(currentTextField + 1) }, onDone = { keyboardController?.hide() } @@ -200,7 +219,7 @@ fun WrapPinTextField( ) if (currentTextField != fieldsRange.last) { - HSpacer.ExtraSmall() + HSpacer.Small() } } } @@ -214,7 +233,7 @@ fun WrapPinTextField( OneTimeLaunchedEffect { if (focusOnCreate) { - focusRequesters.first().requestFocus() + focusRequesters.requestFocus(0) } } } diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapTextField.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapTextField.kt index 38a5be02..366ea2d7 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapTextField.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapTextField.kt @@ -27,7 +27,9 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldColors import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape @@ -36,6 +38,7 @@ import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp +import eu.europa.ec.uilogic.component.utils.SIZE_MEDIUM import eu.europa.ec.uilogic.extension.clickableNoRipple import eu.europa.ec.uilogic.extension.throttledClickable @@ -61,7 +64,8 @@ fun WrapTextField( keyboardActions: KeyboardActions = KeyboardActions.Default, onKeyEvent: ((KeyEvent) -> Boolean)? = null, textStyle: TextStyle = LocalTextStyle.current, - shape: Shape = RoundedCornerShape(16.dp), + shape: Shape = RoundedCornerShape(SIZE_MEDIUM.dp), + colors: TextFieldColors = OutlinedTextFieldDefaults.colors() ) { val textFieldModifier = modifier @@ -102,6 +106,7 @@ fun WrapTextField( keyboardActions = keyboardActions, shape = shape, textStyle = textStyle, + colors = colors ) } From c483ecc3f401d35709e25690a4b9322593850b08 Mon Sep 17 00:00:00 2001 From: Stilianos Tzouvaras Date: Thu, 12 Sep 2024 14:13:51 +0300 Subject: [PATCH 2/7] WrapPinTextField allows now hardware keyboard inputs, clean up --- ...droidApplicationFlavorsConventionPlugin.kt | 9 +-- .../kotlin/AndroidBaseLineProfilePlugin.kt | 9 +-- .../kotlin/AndroidLibraryConventionPlugin.kt | 8 +- .../project/convention/logic/AppFlavor.kt | 10 ++- .../ui/qr_scan/QrScanViewModel.kt | 16 ++-- .../document/code/DocumentOfferCodeScreen.kt | 1 + .../component/wrap/WrapPinTextField.kt | 77 ++++++++++--------- 7 files changed, 62 insertions(+), 68 deletions(-) diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt index b1b924d7..2d554965 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt @@ -19,19 +19,12 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure import project.convention.logic.configureFlavors -import project.convention.logic.getProperty class AndroidApplicationFlavorsConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - - val storedVersion = getProperty( - "VERSION_NAME", - "version.properties" - ).orEmpty() - extensions.configure { - configureFlavors(this, storedVersion) + configureFlavors(this) } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidBaseLineProfilePlugin.kt b/build-logic/convention/src/main/kotlin/AndroidBaseLineProfilePlugin.kt index 69e8bd60..5cfff3db 100644 --- a/build-logic/convention/src/main/kotlin/AndroidBaseLineProfilePlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidBaseLineProfilePlugin.kt @@ -23,19 +23,12 @@ import org.gradle.kotlin.dsl.dependencies import project.convention.logic.configureFlavors import project.convention.logic.configureGradleManagedDevices import project.convention.logic.configureKotlinAndroid -import project.convention.logic.getProperty import project.convention.logic.libs @Suppress("UnstableApiUsage") class AndroidBaseLineProfilePlugin : Plugin { override fun apply(target: Project) { with(target) { - - val storedVersion = getProperty( - "VERSION_NAME", - "version.properties" - ).orEmpty() - with(pluginManager) { apply("com.android.test") apply("org.jetbrains.kotlin.android") @@ -47,7 +40,7 @@ class AndroidBaseLineProfilePlugin : Plugin { with(defaultConfig) { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - configureFlavors(this, storedVersion) + configureFlavors(this) configureGradleManagedDevices(this) targetProjectPath = ":app" } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt index 6e3b8f90..a1ba855b 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -30,7 +30,6 @@ import project.convention.logic.configureGradleManagedDevices import project.convention.logic.configureKotlinAndroid import project.convention.logic.configurePrintApksTask import project.convention.logic.disableUnnecessaryAndroidTests -import project.convention.logic.getProperty import project.convention.logic.libs class AndroidLibraryConventionPlugin : Plugin { @@ -59,11 +58,6 @@ class AndroidLibraryConventionPlugin : Plugin { val openId4VciAuthorizationScheme = "eu.europa.ec.euidi" val openId4VciAuthorizationHost = "authorization" - val storedVersion = getProperty( - "VERSION_NAME", - "version.properties" - ).orEmpty() - with(pluginManager) { apply("com.android.library") apply("project.android.library.kover") @@ -116,7 +110,7 @@ class AndroidLibraryConventionPlugin : Plugin { manifestPlaceholders["openId4VciAuthorizationHost"] = openId4VciAuthorizationHost } - configureFlavors(this, storedVersion) + configureFlavors(this) configureGradleManagedDevices(this) } extensions.configure { diff --git a/build-logic/convention/src/main/kotlin/project/convention/logic/AppFlavor.kt b/build-logic/convention/src/main/kotlin/project/convention/logic/AppFlavor.kt index 8c354a43..620ee8d9 100644 --- a/build-logic/convention/src/main/kotlin/project/convention/logic/AppFlavor.kt +++ b/build-logic/convention/src/main/kotlin/project/convention/logic/AppFlavor.kt @@ -20,6 +20,7 @@ import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.ApplicationProductFlavor import com.android.build.api.dsl.CommonExtension import com.android.build.api.dsl.ProductFlavor +import org.gradle.api.Project @Suppress("EnumEntryName") enum class FlavorDimension { @@ -35,11 +36,16 @@ enum class AppFlavor( Demo(FlavorDimension.contentType) } -fun configureFlavors( +fun Project.configureFlavors( commonExtension: CommonExtension<*, *, *, *, *, *>, - version: String, flavorConfigurationBlock: ProductFlavor.(flavor: AppFlavor) -> Unit = {} ) { + + val version = getProperty( + "VERSION_NAME", + "version.properties" + ).orEmpty() + commonExtension.apply { flavorDimensions += FlavorDimension.contentType.name productFlavors { 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 31a74f5a..3df33c27 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 @@ -133,13 +133,15 @@ class QrScanViewModel( val urlIsValid = validateForm( form = Form( inputs = mapOf( - listOf(Rule.ValidateUrl( - errorMessage = "", - shouldValidateSchema = true, - shouldValidateHost = false, - shouldValidatePath = false, - shouldValidateQuery = true, - )) to scannedQr + listOf( + Rule.ValidateUrl( + errorMessage = "", + shouldValidateSchema = true, + shouldValidateHost = false, + shouldValidatePath = false, + shouldValidateQuery = true, + ) + ) to scannedQr ) ) ) diff --git a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/code/DocumentOfferCodeScreen.kt b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/code/DocumentOfferCodeScreen.kt index 6abbdffb..20ca258c 100644 --- a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/code/DocumentOfferCodeScreen.kt +++ b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/code/DocumentOfferCodeScreen.kt @@ -118,6 +118,7 @@ private fun CodeFieldLayout( visualTransformation = PasswordVisualTransformation(), pinWidth = 46.dp, focusOnCreate = true, + shouldHideKeyboardOnCompletion = true ) } diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt index 0c46f0f8..f92691a6 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt @@ -31,6 +31,7 @@ import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.foundation.text.selection.TextSelectionColors import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -43,6 +44,8 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalTextToolbar import androidx.compose.ui.text.font.FontWeight @@ -72,15 +75,11 @@ fun WrapPinTextField( visualTransformation: VisualTransformation = VisualTransformation.None, pinWidth: Dp? = null, clearCode: Boolean = false, - focusOnCreate: Boolean = false + focusOnCreate: Boolean = false, + shouldHideKeyboardOnCompletion: Boolean = false ) { - fun List.clearFocus() { - this.forEach { it.freeFocus() } - } - fun List.requestFocus(index: Int) { - this.clearFocus() this.elementAtOrNull(index)?.requestFocus() } @@ -90,6 +89,9 @@ fun WrapPinTextField( // Get keyboard controller. val keyboardController = LocalSoftwareKeyboardController.current + // Get Focus Manager + val focusManager = LocalFocusManager.current + // Init list of all digits. val textFieldStateList = remember { fieldsRange.map { @@ -131,7 +133,7 @@ fun WrapPinTextField( horizontalArrangement = Arrangement.SpaceBetween ) { for (currentTextField in fieldsRange) { - WrapTextField( + OutlinedTextField( modifier = Modifier .focusRequester(focusRequesters[currentTextField]) .then(pinWidth?.let { dp -> @@ -140,29 +142,31 @@ fun WrapPinTextField( .padding(vertical = SPACING_SMALL.dp) } ?: Modifier .weight(1f) - .wrapContentSize()), - onKeyEvent = { - if (it.key == Key.Backspace) { - if (textFieldStateList[currentTextField].value.isNotEmpty()) { + .wrapContentSize()) + .then( + Modifier.onKeyEvent { keyEvent -> + if (keyEvent.key == Key.Backspace) { + if (textFieldStateList[currentTextField].value.isNotEmpty()) { - textFieldStateList[currentTextField].value = "" + textFieldStateList[currentTextField].value = "" - // Notify listener. - onPinUpdate.invoke( - textFieldStateList.joinToString( - separator = "", - transform = { textField -> - textField.value - } - ) - ) + // Notify listener. + onPinUpdate.invoke( + textFieldStateList.joinToString( + separator = "", + transform = { textField -> + textField.value + } + ) + ) + } + focusRequesters.requestFocus(currentTextField - 1) + true + } else { + false + } } - focusRequesters.requestFocus(currentTextField - 1) - true - } else { - false - } - }, + ), shape = RoundedCornerShape(SIZE_SMALL.dp), value = textFieldStateList[currentTextField].value, textStyle = LocalTextStyle.current.copy( @@ -176,17 +180,18 @@ fun WrapPinTextField( visualTransformation = visualTransformation, isError = hasError, onValueChange = { newText: String -> - // Set new value, only if it is not blank. - if (newText.isNotBlank() && newText != textFieldStateList[currentTextField].value) { - textFieldStateList[currentTextField].value = - newText.replaceFirst( - textFieldStateList[currentTextField].value, - "" - ) + if (newText != textFieldStateList[currentTextField].value) { + textFieldStateList[currentTextField].value = newText.replaceFirst( + textFieldStateList[currentTextField].value, + "" + ) // Check if all fields are valid. - if (!textFieldStateList.any { textField -> textField.value.isEmpty() }) { - focusRequesters.clearFocus() + if ( + !textFieldStateList.any { textField -> textField.value.isEmpty() } + && shouldHideKeyboardOnCompletion + ) { + focusManager.clearFocus() keyboardController?.hide() } else if (currentTextField < fieldsRange.last) { focusRequesters.requestFocus(currentTextField + 1) From 47699a897a8331c7ef15fcdcc8972d1f14daeb57 Mon Sep 17 00:00:00 2001 From: "Tzouvaras, Stilianos" Date: Thu, 12 Sep 2024 19:22:19 +0300 Subject: [PATCH 3/7] Working on bug fixing --- .../component/wrap/WrapPinTextField.kt | 178 ++++++++++-------- 1 file changed, 101 insertions(+), 77 deletions(-) diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt index f92691a6..063d992d 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.foundation.text.selection.TextSelectionColors import androidx.compose.material3.LocalTextStyle @@ -38,6 +39,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -45,6 +48,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalTextToolbar @@ -64,6 +68,7 @@ import eu.europa.ec.uilogic.component.utils.OneTimeLaunchedEffect import eu.europa.ec.uilogic.component.utils.SIZE_SMALL import eu.europa.ec.uilogic.component.utils.SPACING_SMALL +@OptIn(ExperimentalComposeUiApi::class) @Composable fun WrapPinTextField( modifier: Modifier = Modifier.fillMaxWidth(), @@ -93,7 +98,7 @@ fun WrapPinTextField( val focusManager = LocalFocusManager.current // Init list of all digits. - val textFieldStateList = remember { + val textFieldStateList = rememberSaveable { fieldsRange.map { mutableStateOf("") } @@ -133,95 +138,114 @@ fun WrapPinTextField( horizontalArrangement = Arrangement.SpaceBetween ) { for (currentTextField in fieldsRange) { - OutlinedTextField( - modifier = Modifier - .focusRequester(focusRequesters[currentTextField]) - .then(pinWidth?.let { dp -> - Modifier - .width(dp) - .padding(vertical = SPACING_SMALL.dp) - } ?: Modifier - .weight(1f) - .wrapContentSize()) - .then( - Modifier.onKeyEvent { keyEvent -> - if (keyEvent.key == Key.Backspace) { - if (textFieldStateList[currentTextField].value.isNotEmpty()) { + DisableSelection { + OutlinedTextField( + modifier = Modifier + .focusRequester(focusRequesters[currentTextField]) + .then(pinWidth?.let { dp -> + Modifier + .width(dp) + .padding(vertical = SPACING_SMALL.dp) + } ?: Modifier + .weight(1f) + .wrapContentSize()) + .then( + Modifier.onKeyEvent { keyEvent -> + if (keyEvent.key == Key.Backspace) { + if (textFieldStateList[currentTextField].value.isNotEmpty()) { - textFieldStateList[currentTextField].value = "" + textFieldStateList[currentTextField].value = "" - // Notify listener. - onPinUpdate.invoke( - textFieldStateList.joinToString( - separator = "", - transform = { textField -> - textField.value - } + // Notify listener. + onPinUpdate.invoke( + textFieldStateList.joinToString( + separator = "", + transform = { textField -> + textField.value + } + ) ) - ) + } + focusRequesters.requestFocus(currentTextField - 1) + true + } else { + false } - focusRequesters.requestFocus(currentTextField - 1) - true - } else { + } + ) + .then( + Modifier.pointerInteropFilter { event -> + false } - } + ), + shape = RoundedCornerShape(SIZE_SMALL.dp), + value = textFieldStateList[currentTextField].value, + textStyle = LocalTextStyle.current.copy( + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, ), - shape = RoundedCornerShape(SIZE_SMALL.dp), - value = textFieldStateList[currentTextField].value, - textStyle = LocalTextStyle.current.copy( - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - ), - colors = OutlinedTextFieldDefaults.colors().copy( - cursorColor = Color.Transparent, - errorCursorColor = Color.Transparent - ), - visualTransformation = visualTransformation, - isError = hasError, - onValueChange = { newText: String -> - if (newText != textFieldStateList[currentTextField].value) { - textFieldStateList[currentTextField].value = newText.replaceFirst( - textFieldStateList[currentTextField].value, - "" - ) + colors = OutlinedTextFieldDefaults.colors().copy( + cursorColor = Color.Transparent, + errorCursorColor = Color.Transparent + ), + //visualTransformation = visualTransformation, + isError = hasError, + singleLine = true, + onValueChange = { newText: String -> - // Check if all fields are valid. if ( - !textFieldStateList.any { textField -> textField.value.isEmpty() } - && shouldHideKeyboardOnCompletion + textFieldStateList.all { textField -> textField.value.isEmpty() } + && currentTextField == fieldsRange.last + && newText.isNotEmpty() ) { - focusManager.clearFocus() - keyboardController?.hide() - } else if (currentTextField < fieldsRange.last) { - focusRequesters.requestFocus(currentTextField + 1) + return@OutlinedTextField } - // Notify listener. - onPinUpdate.invoke( - textFieldStateList.joinToString( - separator = "", - transform = { textField -> - textField.value - } + + if (newText != textFieldStateList[currentTextField].value) { + textFieldStateList[currentTextField].value = + newText.replaceFirst( + textFieldStateList[currentTextField].value, + "" + ) + + // Check if all fields are valid. + if ( + !textFieldStateList.any { textField -> textField.value.isEmpty() } + && shouldHideKeyboardOnCompletion + ) { + focusManager.clearFocus() + keyboardController?.hide() + } else if (currentTextField < fieldsRange.last) { + focusRequesters.requestFocus(currentTextField + 1) + } + // Notify listener. + onPinUpdate.invoke( + textFieldStateList.joinToString( + separator = "", + transform = { textField -> + textField.value + } + ) ) - ) - } - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.NumberPassword, - imeAction = when (currentTextField < fieldsRange.last) { - true -> ImeAction.Next - false -> ImeAction.Done - } - ), - keyboardActions = KeyboardActions( - onNext = { - focusRequesters.requestFocus(currentTextField + 1) - }, onDone = { - keyboardController?.hide() - } + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.NumberPassword, + imeAction = when (currentTextField < fieldsRange.last) { + true -> ImeAction.Next + false -> ImeAction.Done + } + ), + keyboardActions = KeyboardActions( + onNext = { + focusRequesters.requestFocus(currentTextField + 1) + }, onDone = { + keyboardController?.hide() + } + ) ) - ) + } if (currentTextField != fieldsRange.last) { HSpacer.Small() From 8a56c459d25f40db7a9fd6a9029f23188a2cb00c Mon Sep 17 00:00:00 2001 From: "Tzouvaras, Stilianos" Date: Thu, 12 Sep 2024 19:30:46 +0300 Subject: [PATCH 4/7] Final corrections --- .../component/wrap/WrapPinTextField.kt | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt index 063d992d..4bc5f8cc 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt @@ -16,6 +16,7 @@ package eu.europa.ec.uilogic.component.wrap +import android.view.MotionEvent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -52,10 +53,12 @@ import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalTextToolbar +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp @@ -97,10 +100,10 @@ fun WrapPinTextField( // Get Focus Manager val focusManager = LocalFocusManager.current - // Init list of all digits. + // Init list of all text field values. val textFieldStateList = rememberSaveable { fieldsRange.map { - mutableStateOf("") + mutableStateOf(TextFieldValue("")) } } @@ -110,16 +113,16 @@ fun WrapPinTextField( } displayCode?.let { otpCode -> - // Assign each charter from otpCode to the corresponding TextField + // Assign each character from otpCode to the corresponding TextFieldValue textFieldStateList.forEachIndexed { index, mutableState -> - mutableState.value = otpCode[index].toString() + mutableState.value = TextFieldValue(otpCode[index].toString()) } onPinUpdate.invoke(otpCode) } if (clearCode) { textFieldStateList.forEach { - it.value = "" + it.value = TextFieldValue("") onPinUpdate.invoke("") } focusRequesters.requestFocus(0) @@ -152,16 +155,16 @@ fun WrapPinTextField( .then( Modifier.onKeyEvent { keyEvent -> if (keyEvent.key == Key.Backspace) { - if (textFieldStateList[currentTextField].value.isNotEmpty()) { - - textFieldStateList[currentTextField].value = "" + if (textFieldStateList[currentTextField].value.text.isNotEmpty()) { + textFieldStateList[currentTextField].value = + TextFieldValue("") // Notify listener. onPinUpdate.invoke( textFieldStateList.joinToString( separator = "", transform = { textField -> - textField.value + textField.value.text } ) ) @@ -175,7 +178,13 @@ fun WrapPinTextField( ) .then( Modifier.pointerInteropFilter { event -> - + if (event.action == MotionEvent.ACTION_DOWN) { + // Move cursor to position 0 when the text field is touched + textFieldStateList[currentTextField].value = + textFieldStateList[currentTextField].value.copy( + selection = TextRange(0) + ) + } false } ), @@ -189,29 +198,31 @@ fun WrapPinTextField( cursorColor = Color.Transparent, errorCursorColor = Color.Transparent ), - //visualTransformation = visualTransformation, + visualTransformation = visualTransformation, isError = hasError, singleLine = true, - onValueChange = { newText: String -> + onValueChange = { newText: TextFieldValue -> if ( - textFieldStateList.all { textField -> textField.value.isEmpty() } + textFieldStateList.all { textField -> textField.value.text.isEmpty() } && currentTextField == fieldsRange.last - && newText.isNotEmpty() + && newText.text.isNotEmpty() ) { return@OutlinedTextField } - if (newText != textFieldStateList[currentTextField].value) { + if (newText.text != textFieldStateList[currentTextField].value.text) { textFieldStateList[currentTextField].value = - newText.replaceFirst( - textFieldStateList[currentTextField].value, - "" + TextFieldValue( + newText.text.replaceFirst( + textFieldStateList[currentTextField].value.text, + "" + ) ) // Check if all fields are valid. if ( - !textFieldStateList.any { textField -> textField.value.isEmpty() } + !textFieldStateList.any { textField -> textField.value.text.isEmpty() } && shouldHideKeyboardOnCompletion ) { focusManager.clearFocus() @@ -224,7 +235,7 @@ fun WrapPinTextField( textFieldStateList.joinToString( separator = "", transform = { textField -> - textField.value + textField.value.text } ) ) @@ -269,6 +280,7 @@ fun WrapPinTextField( } } + /** * Preview composable of [WrapPinTextField]. */ From 82fa1fc9beeee5eafc367c1d1ee994f1a00a2af8 Mon Sep 17 00:00:00 2001 From: "Tzouvaras, Stilianos" Date: Thu, 12 Sep 2024 20:24:53 +0300 Subject: [PATCH 5/7] Finalized all changes --- .../component/wrap/WrapPinTextField.kt | 59 ++++++------------- 1 file changed, 18 insertions(+), 41 deletions(-) diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt index 4bc5f8cc..a8c8cc7e 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt @@ -16,7 +16,6 @@ package eu.europa.ec.uilogic.component.wrap -import android.view.MotionEvent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -41,7 +40,6 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -49,16 +47,13 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent -import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalTextToolbar -import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp @@ -71,7 +66,6 @@ import eu.europa.ec.uilogic.component.utils.OneTimeLaunchedEffect import eu.europa.ec.uilogic.component.utils.SIZE_SMALL import eu.europa.ec.uilogic.component.utils.SPACING_SMALL -@OptIn(ExperimentalComposeUiApi::class) @Composable fun WrapPinTextField( modifier: Modifier = Modifier.fillMaxWidth(), @@ -100,10 +94,10 @@ fun WrapPinTextField( // Get Focus Manager val focusManager = LocalFocusManager.current - // Init list of all text field values. + // Init list of all digits. val textFieldStateList = rememberSaveable { fieldsRange.map { - mutableStateOf(TextFieldValue("")) + mutableStateOf("") } } @@ -113,16 +107,16 @@ fun WrapPinTextField( } displayCode?.let { otpCode -> - // Assign each character from otpCode to the corresponding TextFieldValue + // Assign each charter from otpCode to the corresponding TextField textFieldStateList.forEachIndexed { index, mutableState -> - mutableState.value = TextFieldValue(otpCode[index].toString()) + mutableState.value = otpCode[index].toString() } onPinUpdate.invoke(otpCode) } if (clearCode) { textFieldStateList.forEach { - it.value = TextFieldValue("") + it.value = "" onPinUpdate.invoke("") } focusRequesters.requestFocus(0) @@ -155,16 +149,14 @@ fun WrapPinTextField( .then( Modifier.onKeyEvent { keyEvent -> if (keyEvent.key == Key.Backspace) { - if (textFieldStateList[currentTextField].value.text.isNotEmpty()) { - textFieldStateList[currentTextField].value = - TextFieldValue("") - + if (textFieldStateList[currentTextField].value.isNotEmpty()) { + textFieldStateList[currentTextField].value = "" // Notify listener. onPinUpdate.invoke( textFieldStateList.joinToString( separator = "", transform = { textField -> - textField.value.text + textField.value } ) ) @@ -175,18 +167,6 @@ fun WrapPinTextField( false } } - ) - .then( - Modifier.pointerInteropFilter { event -> - if (event.action == MotionEvent.ACTION_DOWN) { - // Move cursor to position 0 when the text field is touched - textFieldStateList[currentTextField].value = - textFieldStateList[currentTextField].value.copy( - selection = TextRange(0) - ) - } - false - } ), shape = RoundedCornerShape(SIZE_SMALL.dp), value = textFieldStateList[currentTextField].value, @@ -201,33 +181,31 @@ fun WrapPinTextField( visualTransformation = visualTransformation, isError = hasError, singleLine = true, - onValueChange = { newText: TextFieldValue -> + onValueChange = { newText: String -> if ( - textFieldStateList.all { textField -> textField.value.text.isEmpty() } + textFieldStateList.all { textField -> textField.value.isEmpty() } && currentTextField == fieldsRange.last - && newText.text.isNotEmpty() + && newText.isNotEmpty() ) { return@OutlinedTextField } - if (newText.text != textFieldStateList[currentTextField].value.text) { + if (newText != textFieldStateList[currentTextField].value) { textFieldStateList[currentTextField].value = - TextFieldValue( - newText.text.replaceFirst( - textFieldStateList[currentTextField].value.text, - "" - ) + newText.replaceFirst( + textFieldStateList[currentTextField].value, + "" ) // Check if all fields are valid. if ( - !textFieldStateList.any { textField -> textField.value.text.isEmpty() } + !textFieldStateList.any { textField -> textField.value.isEmpty() } && shouldHideKeyboardOnCompletion ) { focusManager.clearFocus() keyboardController?.hide() - } else if (currentTextField < fieldsRange.last) { + } else if (currentTextField < fieldsRange.last && newText.isNotEmpty()) { focusRequesters.requestFocus(currentTextField + 1) } // Notify listener. @@ -235,7 +213,7 @@ fun WrapPinTextField( textFieldStateList.joinToString( separator = "", transform = { textField -> - textField.value.text + textField.value } ) ) @@ -280,7 +258,6 @@ fun WrapPinTextField( } } - /** * Preview composable of [WrapPinTextField]. */ From f74a11b2eef854f1491eebc7286524d05dab341c Mon Sep 17 00:00:00 2001 From: "Tzouvaras, Stilianos" Date: Thu, 12 Sep 2024 20:34:46 +0300 Subject: [PATCH 6/7] allow only digits for pin input --- .../europa/ec/uilogic/component/wrap/WrapPinTextField.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt index a8c8cc7e..6878754b 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapPinTextField.kt @@ -58,6 +58,7 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.core.text.isDigitsOnly import eu.europa.ec.uilogic.component.preview.PreviewTheme import eu.europa.ec.uilogic.component.preview.ThemeModePreviews import eu.europa.ec.uilogic.component.utils.EmptyTextToolbar @@ -184,9 +185,10 @@ fun WrapPinTextField( onValueChange = { newText: String -> if ( - textFieldStateList.all { textField -> textField.value.isEmpty() } - && currentTextField == fieldsRange.last - && newText.isNotEmpty() + !newText.isDigitsOnly() + || (textFieldStateList.all { textField -> textField.value.isEmpty() } + && currentTextField == fieldsRange.last + && newText.isNotEmpty()) ) { return@OutlinedTextField } From 9087819b42d08e5a7cef387ff1395b5dae3fc06c Mon Sep 17 00:00:00 2001 From: Stilianos Tzouvaras Date: Thu, 12 Sep 2024 22:19:21 +0300 Subject: [PATCH 7/7] Dependencies update, Kotlin 2 support, new compose and compose plugin. --- .gitignore | 5 +++- ...droidApplicationComposeConventionPlugin.kt | 6 +++-- .../AndroidLibraryComposeConventionPlugin.kt | 6 +++-- .../convention/logic/AndroidCompose.kt | 9 ++----- .../project/convention/logic/KotlinAndroid.kt | 11 +++++---- build.gradle.kts | 1 + .../commonfeature/ui/qr_scan/QrScanScreen.kt | 2 +- .../ui/dashboard/DashboardScreen.kt | 2 +- gradle/libs.versions.toml | 24 +++++++++---------- .../ui/document/add/AddDocumentScreen.kt | 2 +- .../document/details/DocumentDetailsScreen.kt | 2 +- .../ui/document/offer/DocumentOfferScreen.kt | 2 +- .../ui/qr/ProximityQRScreen.kt | 2 +- .../theme/templates/ThemeColorsTemplate.kt | 7 ++++++ .../ec/uilogic/component/wrap/WrapCheckBox.kt | 5 ++-- .../ec/uilogic/component/wrap/WrapIcon.kt | 4 ++-- 16 files changed, 50 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index 873118a4..15708bb9 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,7 @@ sign #fastlane .env.demo .env.dev -fastlane/report.xml \ No newline at end of file +fastlane/report.xml + +# Kotlin +.kotlin/* \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt index 804ed3f3..ac93d145 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt @@ -23,8 +23,10 @@ import project.convention.logic.configureAndroidCompose class AndroidApplicationComposeConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply("com.android.application") - + with(pluginManager) { + apply("com.android.application") + apply("org.jetbrains.kotlin.plugin.compose") + } val extension = extensions.getByType() configureAndroidCompose(extension) } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index e763c994..db601e98 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -23,8 +23,10 @@ import project.convention.logic.configureAndroidCompose class AndroidLibraryComposeConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply("com.android.library") - + with(pluginManager) { + apply("com.android.library") + apply("org.jetbrains.kotlin.plugin.compose") + } val extension = extensions.getByType() configureAndroidCompose(extension) } diff --git a/build-logic/convention/src/main/kotlin/project/convention/logic/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/project/convention/logic/AndroidCompose.kt index 0ea8a0e2..f4068e3a 100644 --- a/build-logic/convention/src/main/kotlin/project/convention/logic/AndroidCompose.kt +++ b/build-logic/convention/src/main/kotlin/project/convention/logic/AndroidCompose.kt @@ -33,11 +33,6 @@ internal fun Project.configureAndroidCompose( compose = true } - composeOptions { - kotlinCompilerExtensionVersion = - libs.findVersion("androidxComposeCompiler").get().toString() - } - dependencies { val bom = libs.findLibrary("androidx-compose-bom").get() @@ -74,8 +69,8 @@ internal fun Project.configureAndroidCompose( } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = freeCompilerArgs + buildComposeMetricsParameters() + compilerOptions { + freeCompilerArgs.addAll(buildComposeMetricsParameters()) } } } diff --git a/build-logic/convention/src/main/kotlin/project/convention/logic/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/project/convention/logic/KotlinAndroid.kt index a5cdcfbb..88bcfecc 100644 --- a/build-logic/convention/src/main/kotlin/project/convention/logic/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/project/convention/logic/KotlinAndroid.kt @@ -24,6 +24,7 @@ import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.provideDelegate import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile /** @@ -84,12 +85,12 @@ internal fun Project.configureKotlinJvm() { */ private fun Project.configureKotlin() { tasks.withType().configureEach { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - // Treat all Kotlin warnings as errors (disabled by default) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + val warningsAsErrors: String? by project - allWarningsAsErrors = warningsAsErrors.toBoolean() - freeCompilerArgs = freeCompilerArgs + listOf( + allWarningsAsErrors.set(warningsAsErrors.toBoolean()) + freeCompilerArgs.addAll( "-opt-in=kotlin.RequiresOptIn", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.FlowPreview", diff --git a/build.gradle.kts b/build.gradle.kts index 2dbc4cfd..ef202fe5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,5 +29,6 @@ plugins { alias(libs.plugins.sonar) apply false alias(libs.plugins.android.test) apply false alias(libs.plugins.baselineprofile) apply false + alias(libs.plugins.compose.compiler) apply false } true 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 6f81c314..cc378ea4 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 @@ -46,12 +46,12 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted diff --git a/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/ui/dashboard/DashboardScreen.kt b/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/ui/dashboard/DashboardScreen.kt index e617ade1..93820442 100644 --- a/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/ui/dashboard/DashboardScreen.kt +++ b/dashboard-feature/src/main/java/eu/europa/ec/dashboardfeature/ui/dashboard/DashboardScreen.kt @@ -51,7 +51,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -60,6 +59,7 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8e8f9356..1d4242c6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,21 +1,20 @@ [versions] accompanist = "0.34.0" -androidDesugarJdkLibs = "2.1.1" +androidDesugarJdkLibs = "2.1.2" androidGradlePlugin = "8.6.0" -androidxActivity = "1.9.1" +androidxActivity = "1.9.2" androidxAppCompat = "1.7.0" androidxBrowser = "1.8.0" -androidxComposeBom = "2024.08.00" -androidxComposeCompiler = "1.5.11" +androidxComposeBom = "2024.09.01" androidxComposeRuntimeTracing = "1.0.0-beta01" androidxCore = "1.13.1" androidxCoreSplashscreen = "1.0.1" androidxDataStore = "1.1.1" androidxEspresso = "3.6.1" -androidxLifecycle = "2.8.4" +androidxLifecycle = "2.8.5" androidxMacroBenchmark = "1.3.0" androidxMetrics = "1.0.0-beta01" -androidxNavigation = "2.7.7" +androidxNavigation = "2.8.0" androidxProfileinstaller = "1.3.1" androidxTestCore = "1.6.1" androidxTestExt = "1.2.1" @@ -33,11 +32,11 @@ coil = "2.6.0" constraintlayoutVersion = "1.0.1" gmsPlugin = "4.4.2" junit4 = "4.13.2" -kotlin = "1.9.23" +kotlin = "2.0.10" kotlinxCoroutines = "1.8.1" kotlinxDatetime = "0.4.1" -kotlinxSerializationJson = "1.6.1" -ksp = "1.9.23-1.0.20" +kotlinxSerializationJson = "1.6.3" +ksp = "2.0.10-1.0.24" lint = "31.6.0" okhttp = "4.12.0" protobuf = "3.24.0" @@ -48,7 +47,6 @@ secrets = "2.0.1" turbine = "1.1.0" koin = "3.5.0" koinAnnotations = "1.3.0" -org-jetbrains-kotlin-android = "1.9.23" material = "1.12.0" mockito = "5.12.0" mockitoKotlin = "5.3.1" @@ -62,8 +60,7 @@ eudiWalletCore = "0.11.1-SNAPSHOT" cborTree = "0.01.02" cameraCore = "1.3.4" owaspDependencyCheck = "10.0.3" -material3 = "1.2.1" -workTesting = "2.9.0" +material3 = "1.3.0" appCenter = "5.0.4" kover = "0.7.5" sonar = "5.0.0.4638" @@ -182,7 +179,8 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" } -org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "org-jetbrains-kotlin-android" } +org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } owasp-dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "owaspDependencyCheck" } kotlinx-kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } sonar = { id = "org.sonarqube", version.ref = "sonar" } diff --git a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/add/AddDocumentScreen.kt b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/add/AddDocumentScreen.kt index 234df946..4e289b06 100644 --- a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/add/AddDocumentScreen.kt +++ b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/add/AddDocumentScreen.kt @@ -35,10 +35,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.navigation.NavController import eu.europa.ec.commonfeature.model.DocumentOptionItemUi import eu.europa.ec.corelogic.controller.IssuanceMethod diff --git a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/details/DocumentDetailsScreen.kt b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/details/DocumentDetailsScreen.kt index 79022db8..a62fcd9a 100644 --- a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/details/DocumentDetailsScreen.kt +++ b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/details/DocumentDetailsScreen.kt @@ -37,12 +37,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.navigation.NavController import eu.europa.ec.businesslogic.util.safeLet import eu.europa.ec.commonfeature.config.IssuanceFlowUiConfig diff --git a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/offer/DocumentOfferScreen.kt b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/offer/DocumentOfferScreen.kt index 16940e84..f2d6629e 100644 --- a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/offer/DocumentOfferScreen.kt +++ b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/offer/DocumentOfferScreen.kt @@ -34,10 +34,10 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.navigation.NavController import eu.europa.ec.commonfeature.ui.request.DocumentCard import eu.europa.ec.commonfeature.ui.request.model.DocumentItemUi diff --git a/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/qr/ProximityQRScreen.kt b/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/qr/ProximityQRScreen.kt index a0cb7334..6af9c2c6 100644 --- a/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/qr/ProximityQRScreen.kt +++ b/proximity-feature/src/main/java/eu/europa/ec/proximityfeature/ui/qr/ProximityQRScreen.kt @@ -37,11 +37,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.navigation.NavController import eu.europa.ec.proximityfeature.ui.qr.component.rememberQrBitmapPainter import eu.europa.ec.resourceslogic.R diff --git a/resources-logic/src/main/java/eu/europa/ec/resourceslogic/theme/templates/ThemeColorsTemplate.kt b/resources-logic/src/main/java/eu/europa/ec/resourceslogic/theme/templates/ThemeColorsTemplate.kt index 0e0519f5..63ae599e 100644 --- a/resources-logic/src/main/java/eu/europa/ec/resourceslogic/theme/templates/ThemeColorsTemplate.kt +++ b/resources-logic/src/main/java/eu/europa/ec/resourceslogic/theme/templates/ThemeColorsTemplate.kt @@ -85,6 +85,13 @@ data class ThemeColorsTemplate( outline = Color(outline), outlineVariant = Color(outlineVariant), scrim = Color(scrim), + surfaceBright = Color.Unspecified, + surfaceDim = Color.Unspecified, + surfaceContainer = Color.Unspecified, + surfaceContainerHigh = Color.Unspecified, + surfaceContainerHighest = Color.Unspecified, + surfaceContainerLow = Color.Unspecified, + surfaceContainerLowest = Color.Unspecified ) } } \ No newline at end of file diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapCheckBox.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapCheckBox.kt index ed3561f4..7691d1da 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapCheckBox.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapCheckBox.kt @@ -19,7 +19,7 @@ package eu.europa.ec.uilogic.component.wrap import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement +import androidx.compose.material3.LocalMinimumInteractiveComponentSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -28,6 +28,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp import eu.europa.ec.uilogic.component.preview.PreviewTheme import eu.europa.ec.uilogic.component.preview.ThemeModePreviews @@ -45,7 +46,7 @@ fun WrapCheckbox( ) { // This is needed, otherwise M3 adds unwanted space around CheckBoxes. CompositionLocalProvider( - LocalMinimumInteractiveComponentEnforcement provides false + LocalMinimumInteractiveComponentSize provides Dp.Unspecified ) { Checkbox( checked = checkboxData.isChecked, diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapIcon.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapIcon.kt index 346de05b..3a350d12 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapIcon.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapIcon.kt @@ -22,10 +22,10 @@ import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -114,7 +114,7 @@ fun WrapIconButton( ) { val role = Role.Button val rippleSize = if (size == DEFAULT_ICON_SIZE.dp) 40.dp else size - val indication = rememberRipple( + val indication = ripple( bounded = false, radius = rippleSize / 2 )