From 437e81e225b8ec40012559462e1595ee666175ea Mon Sep 17 00:00:00 2001 From: Basler182 Date: Fri, 14 Jun 2024 12:37:38 +0200 Subject: [PATCH 01/28] extended ValidatedOutlinedTextField Signed-off-by: Basler182 --- .../ValidatedOutlinedTextField.kt | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/validated/outlinedtextfield/ValidatedOutlinedTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/validated/outlinedtextfield/ValidatedOutlinedTextField.kt index e14a6f883..b77e9ff39 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/validated/outlinedtextfield/ValidatedOutlinedTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/validated/outlinedtextfield/ValidatedOutlinedTextField.kt @@ -2,6 +2,7 @@ package edu.stanford.spezi.core.design.component.validated.outlinedtextfield import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -9,11 +10,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import edu.stanford.spezi.core.design.theme.Colors -import edu.stanford.spezi.core.design.theme.TextStyles.labelSmall @Composable fun ValidatedOutlinedTextField( @@ -22,6 +22,12 @@ fun ValidatedOutlinedTextField( onValueChange: (String) -> Unit, labelText: String = "", errorText: String? = null, + singleLine: Boolean = true, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next), + visualTransformation: VisualTransformation = VisualTransformation.None, + readOnly: Boolean = false, + trailingIcon: @Composable (() -> Unit)? = null, + keyboardActions: KeyboardActions = KeyboardActions.Default, ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { OutlinedTextField( @@ -31,17 +37,21 @@ fun ValidatedOutlinedTextField( onValueChange(it) }, label = { Text(labelText) }, - singleLine = true, - keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next), + singleLine = singleLine, + keyboardOptions = keyboardOptions, isError = errorText != null, + readOnly = readOnly, + trailingIcon = trailingIcon, + keyboardActions = keyboardActions, + visualTransformation = visualTransformation, + supportingText = { + if (errorText != null) { + Text( + text = errorText, + ) + } + }, ) - if (errorText != null) { - Text( - text = errorText, - style = labelSmall, - color = Colors.error - ) - } } } From ca1ab36c36918effc427cd91b75a457670665436 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Fri, 14 Jun 2024 12:38:49 +0200 Subject: [PATCH 02/28] updated RegisterScreen to ValidatedOutlinedTextField Signed-off-by: Basler182 --- .../module/account/register/RegisterScreen.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt index 03d5b0100..c8d8a7223 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt @@ -40,7 +40,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.hilt.navigation.compose.hiltViewModel import edu.stanford.spezi.core.design.component.VerticalSpacer -import edu.stanford.spezi.core.design.component.validated.textfield.ValidatedTextField +import edu.stanford.spezi.core.design.component.validated.outlinedtextfield.ValidatedOutlinedTextField import edu.stanford.spezi.core.design.theme.Colors.primary import edu.stanford.spezi.core.design.theme.Sizes import edu.stanford.spezi.core.design.theme.Spacings @@ -106,7 +106,7 @@ fun RegisterScreen( ) VerticalSpacer(height = Spacings.large) Text("CREDENTIALS", style = labelLarge) - ValidatedTextField( + ValidatedOutlinedTextField( modifier = Modifier.fillMaxWidth(), value = uiState.email.value, onValueChange = { @@ -117,7 +117,7 @@ fun RegisterScreen( ) VerticalSpacer(height = Spacings.small) if (!uiState.isGoogleSignUp) { - ValidatedTextField( + ValidatedOutlinedTextField( modifier = Modifier.fillMaxWidth(), value = uiState.password.value, onValueChange = { @@ -130,7 +130,7 @@ fun RegisterScreen( } VerticalSpacer() Text("NAME", style = labelLarge) - ValidatedTextField( + ValidatedOutlinedTextField( modifier = Modifier.fillMaxWidth(), value = uiState.firstName.value, onValueChange = { onAction(Action.TextFieldUpdate(it, TextFieldType.FIRST_NAME)) }, @@ -138,7 +138,7 @@ fun RegisterScreen( errorText = uiState.firstName.error, ) VerticalSpacer(height = Spacings.small) - ValidatedTextField( + ValidatedOutlinedTextField( modifier = Modifier.fillMaxWidth(), value = uiState.lastName.value, onValueChange = { @@ -165,7 +165,7 @@ fun RegisterScreen( } } - ValidatedTextField( + ValidatedOutlinedTextField( modifier = Modifier.fillMaxWidth(), value = uiState.selectedGender.value, onValueChange = { @@ -185,7 +185,7 @@ fun RegisterScreen( VerticalSpacer(height = Spacings.small) var isDatePickerDialogOpen by remember { mutableStateOf(false) } - ValidatedTextField( + ValidatedOutlinedTextField( modifier = Modifier.fillMaxWidth(), value = uiState.dateOfBirth?.format(DateTimeFormatter.ofPattern("dd MMMM yyyy")) ?: "", onValueChange = { /* Do nothing as we handle the date through the DatePicker */ }, From a7fa86ed8c0a3ae48728da013c50961f27ac5804 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Fri, 14 Jun 2024 12:58:13 +0200 Subject: [PATCH 03/28] added visibility icon on/off Signed-off-by: Basler182 --- core/design/src/main/res/drawable/ic_visibility.xml | 10 ++++++++++ .../design/src/main/res/drawable/ic_visibility_off.xml | 10 ++++++++++ 2 files changed, 20 insertions(+) create mode 100644 core/design/src/main/res/drawable/ic_visibility.xml create mode 100644 core/design/src/main/res/drawable/ic_visibility_off.xml diff --git a/core/design/src/main/res/drawable/ic_visibility.xml b/core/design/src/main/res/drawable/ic_visibility.xml new file mode 100644 index 000000000..01604e57f --- /dev/null +++ b/core/design/src/main/res/drawable/ic_visibility.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/design/src/main/res/drawable/ic_visibility_off.xml b/core/design/src/main/res/drawable/ic_visibility_off.xml new file mode 100644 index 000000000..39550b7dd --- /dev/null +++ b/core/design/src/main/res/drawable/ic_visibility_off.xml @@ -0,0 +1,10 @@ + + + From 445f308ef897bb44a77275cb8442295202b55658 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Fri, 14 Jun 2024 12:59:39 +0200 Subject: [PATCH 04/28] added LoginFormValidator Signed-off-by: Basler182 --- .../account/login/LoginFormValidator.kt | 26 +++++++++++++++++ .../module/account/register/FormValidator.kt | 22 +++++++++++++++ .../account/register/RegisterFormValidator.kt | 28 +++++++++---------- 3 files changed, 61 insertions(+), 15 deletions(-) create mode 100644 modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginFormValidator.kt create mode 100644 modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/FormValidator.kt diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginFormValidator.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginFormValidator.kt new file mode 100644 index 000000000..11fe37ee1 --- /dev/null +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginFormValidator.kt @@ -0,0 +1,26 @@ +package edu.stanford.spezi.module.account.login + +import edu.stanford.spezi.module.account.register.FormValidator +import javax.inject.Inject + +internal class LoginFormValidator @Inject internal constructor() : FormValidator() { + + fun emailResult(email: String): Result = if (isValidEmail(email)) { + Result.Valid + } else { + Result.Invalid("Invalid email") + } + + fun passwordResult(password: String): Result = if (isValidPassword(password)) { + Result.Valid + } else { + Result.Invalid("Password must be at least $MIN_PASSWORD_LENGTH characters") + } + + fun isFormValid(uiState: UiState): Boolean { + return emailResult(uiState.email.value) is Result.Valid && + passwordResult(uiState.password.value) is Result.Valid + } + + fun isEmailValid(email: String): Boolean = emailResult(email) is Result.Valid +} diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/FormValidator.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/FormValidator.kt new file mode 100644 index 000000000..94a1055c4 --- /dev/null +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/FormValidator.kt @@ -0,0 +1,22 @@ +package edu.stanford.spezi.module.account.register + +internal open class FormValidator { + fun isValidEmail(email: String): Boolean { + return android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() + } + + fun isValidPassword(password: String): Boolean { + return password.length >= MIN_PASSWORD_LENGTH + } + + internal companion object { + const val MIN_PASSWORD_LENGTH = 6 // Minimum for firebase + } + + sealed interface Result { + data object Valid : Result + data class Invalid(val message: String) : Result + + fun errorMessageOrNull() = if (this is Invalid) message else null + } +} diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterFormValidator.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterFormValidator.kt index 3d89d68ea..92b48cba9 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterFormValidator.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterFormValidator.kt @@ -3,15 +3,15 @@ package edu.stanford.spezi.module.account.register import java.time.LocalDate import javax.inject.Inject -class RegisterFormValidator @Inject internal constructor() { +internal class RegisterFormValidator @Inject internal constructor() : FormValidator() { - fun emailResult(email: String): Result = if (android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) { + fun emailResult(email: String): Result = if (isValidEmail(email)) { Result.Valid } else { Result.Invalid("Invalid email") } - fun passwordResult(password: String): Result = if (password.length >= MIN_PASSWORD_LENGTH) { + fun passwordResult(password: String): Result = if (isValidPassword(password)) { Result.Valid } else { Result.Invalid("Password must be at least $MIN_PASSWORD_LENGTH characters") @@ -24,7 +24,16 @@ class RegisterFormValidator @Inject internal constructor() { if (lastName.isNotEmpty()) Result.Valid else Result.Invalid("Last name cannot be empty") fun isGenderValid(gender: String): Result = - if (listOf("Male", "Female", "Other").contains(gender)) Result.Valid else Result.Invalid("Please select valid gender") + if (listOf( + "Male", + "Female", + "Other" + ).contains(gender) + ) { + Result.Valid + } else { + Result.Invalid("Please select valid gender") + } fun birthdayResult(dateOfBirth: LocalDate?): Result = if (dateOfBirth != null && dateOfBirth.isBefore(LocalDate.now())) { @@ -48,15 +57,4 @@ class RegisterFormValidator @Inject internal constructor() { birthdayResult(uiState.dateOfBirth) is Result.Valid && passwordConditionSatisfied() } - - sealed interface Result { - data object Valid : Result - data class Invalid(val message: String) : Result - - fun errorMessageOrNull() = if (this is Invalid) message else null - } - - private companion object { - const val MIN_PASSWORD_LENGTH = 6 // Minimum for firebase - } } From 0e0a64ca1f5b8d9951090d9f0138e4d6ca1968ed Mon Sep 17 00:00:00 2001 From: Basler182 Date: Fri, 14 Jun 2024 13:14:06 +0200 Subject: [PATCH 05/28] update password keyboard Behaviour in LoginScreen Signed-off-by: Basler182 --- .../spezi/module/account/login/LoginScreen.kt | 61 +++++++------ .../module/account/login/LoginViewModel.kt | 86 ++++++++++++++----- .../spezi/module/account/login/UiState.kt | 17 ++-- 3 files changed, 108 insertions(+), 56 deletions(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginScreen.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginScreen.kt index 11f9c1cba..c9d8e9249 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginScreen.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginScreen.kt @@ -13,7 +13,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button -import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -21,6 +22,8 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation @@ -28,12 +31,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.hilt.navigation.compose.hiltViewModel +import edu.stanford.spezi.core.design.component.validated.outlinedtextfield.ValidatedOutlinedTextField import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.TextStyles.bodyLarge import edu.stanford.spezi.core.design.theme.TextStyles.titleLarge import edu.stanford.spezi.module.account.login.components.SignInWithGoogleButton import edu.stanford.spezi.module.account.login.components.TextDivider +import edu.stanford.spezi.module.account.register.FieldState @Composable fun LoginScreen( @@ -72,32 +77,48 @@ You may login to your existing account or create a new one if you don't have one style = bodyLarge, ) Spacer(modifier = Modifier.height(Spacings.large)) - OutlinedTextField( + ValidatedOutlinedTextField( modifier = Modifier.fillMaxWidth(), - value = uiState.email, + value = uiState.email.value, + errorText = uiState.email.error, onValueChange = { email -> onAction(Action.TextFieldUpdate(email, TextFieldType.EMAIL)) }, - label = { Text("E-Mail Address") }, + labelText = "E-Mail Address", singleLine = true, keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next) ) Spacer(modifier = Modifier.height(Spacings.small)) - OutlinedTextField( + ValidatedOutlinedTextField( modifier = Modifier.fillMaxWidth(), - value = uiState.password, + value = uiState.password.value, + errorText = uiState.password.error, onValueChange = { onAction(Action.TextFieldUpdate(it, TextFieldType.PASSWORD)) }, - label = { Text("Password") }, - singleLine = true, + labelText = "Password", visualTransformation = if (uiState.passwordVisibility) { VisualTransformation.None } else { PasswordVisualTransformation() }, keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { onAction(Action.TogglePasswordVisibility) }) + keyboardActions = KeyboardActions(onDone = { + onAction(Action.PasswordSignInOrSignUp) + }), + trailingIcon = { + IconButton(onClick = { onAction(Action.TogglePasswordVisibility) }) { + val icon: Painter = if (uiState.passwordVisibility) { + painterResource(id = edu.stanford.spezi.core.design.R.drawable.ic_visibility) + } else { + painterResource(id = edu.stanford.spezi.core.design.R.drawable.ic_visibility_off) + } + Icon( + painter = icon, + contentDescription = if (uiState.passwordVisibility) "Hide password" else "Show password" + ) + } + } ) TextButton( onClick = { @@ -109,14 +130,10 @@ You may login to your existing account or create a new one if you don't have one Spacer(modifier = Modifier.height(Spacings.medium)) Button( onClick = { - if (uiState.isAlreadyRegistered) { - onAction(Action.PasswordCredentialSignIn) - } else { - onAction(Action.NavigateToRegister) - } + onAction(Action.PasswordSignInOrSignUp) }, modifier = Modifier.fillMaxWidth(), - enabled = uiState.email.isNotEmpty() && uiState.password.isNotEmpty() + enabled = uiState.email.value.isNotEmpty() && uiState.password.value.isNotEmpty() ) { Text( text = if (uiState.isAlreadyRegistered) "Login" else "Register" @@ -143,11 +160,7 @@ You may login to your existing account or create a new one if you don't have one Spacer(modifier = Modifier.height(Spacings.medium)) SignInWithGoogleButton( onButtonClick = { - if (uiState.isAlreadyRegistered) { - onAction(Action.GoogleSignIn) - } else { - onAction(Action.GoogleSignUp) - } + onAction(Action.GoogleSignInOrSignUp) }, isAlreadyRegistered = uiState.isAlreadyRegistered, ) @@ -167,12 +180,12 @@ private fun LoginScreenPreview( private class LoginScreenPreviewProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf( UiState( - email = "", - password = "", + email = FieldState(""), + password = FieldState(""), passwordVisibility = false, ), UiState( - email = "test@test.de", - password = "password", + email = FieldState("test@test.de"), + password = FieldState("password"), passwordVisibility = true, isAlreadyRegistered = true ) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginViewModel.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginViewModel.kt index 1f7d04743..05e187a38 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginViewModel.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginViewModel.kt @@ -8,6 +8,8 @@ import edu.stanford.spezi.core.utils.MessageNotifier import edu.stanford.spezi.module.account.AccountEvents import edu.stanford.spezi.module.account.AccountNavigationEvent import edu.stanford.spezi.module.account.cred.manager.CredentialLoginManagerAuth +import edu.stanford.spezi.module.account.register.FieldState +import edu.stanford.spezi.module.account.register.FormValidator import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -15,11 +17,12 @@ import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class LoginViewModel @Inject constructor( +internal class LoginViewModel @Inject constructor( private val navigator: Navigator, private val credentialLoginManagerAuth: CredentialLoginManagerAuth, private val accountEvents: AccountEvents, private val messageNotifier: MessageNotifier, + private val validator: LoginFormValidator, ) : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState = _uiState.asStateFlow() @@ -40,8 +43,12 @@ class LoginViewModel @Inject constructor( it } - is Action.GoogleSignIn -> { - googleSignIn() + is Action.GoogleSignInOrSignUp -> { + if (uiState.value.isAlreadyRegistered) { + googleSignIn() + } else { + googleSignUp() + } it } @@ -49,24 +56,44 @@ class LoginViewModel @Inject constructor( handleIsAlreadyRegistered(action, it) } - is Action.PasswordCredentialSignIn -> { - passwordSignIn() - it - } - is Action.ForgotPassword -> { forgotPassword() it } - Action.GoogleSignUp -> { - googleSignUp() + Action.PasswordSignInOrSignUp -> { + handleLoginOrRegister() it } } } } + private fun handleLoginOrRegister() { + val uiState = _uiState.value + if (uiState.isAlreadyRegistered && validator.isFormValid(uiState)) { + passwordSignIn() + } + + if (!uiState.isAlreadyRegistered && validator.isFormValid(uiState)) { + navigateToRegister() + } + + _uiState.update { + it.copy( + hasAttemptedSubmit = true, + email = FieldState( + uiState.email.value, + error = validator.emailResult(uiState.email.value).errorMessageOrNull() + ), + password = FieldState( + uiState.password.value, + error = validator.passwordResult(uiState.password.value).errorMessageOrNull() + ) + ) + } + } + private fun handleIsAlreadyRegistered( action: Action.SetIsAlreadyRegistered, it: UiState, @@ -85,28 +112,41 @@ class LoginViewModel @Inject constructor( navigator.navigateTo( AccountNavigationEvent.RegisterScreen( isGoogleSignUp = false, - email = uiState.value.email, - password = uiState.value.password, + email = uiState.value.email.value, + password = uiState.value.password.value, ) ) } private fun updateTextField( action: Action.TextFieldUpdate, - it: UiState, + uiState: UiState, ): UiState { - val newValue = action.newValue + val newValue = FieldState(action.newValue) + val result = when (action.type) { + TextFieldType.PASSWORD -> validator.passwordResult(action.newValue) + TextFieldType.EMAIL -> validator.emailResult(action.newValue) + } + val error = + if (uiState.hasAttemptedSubmit && result is FormValidator.Result.Invalid) result.errorMessageOrNull() else null return when (action.type) { - TextFieldType.PASSWORD -> it.copy(password = newValue) - TextFieldType.EMAIL -> it.copy(email = newValue) + TextFieldType.PASSWORD -> uiState.copy( + password = newValue.copy(error = error), + isFormValid = validator.isFormValid(uiState) + ) + + TextFieldType.EMAIL -> uiState.copy( + email = newValue.copy(error = error), + isFormValid = validator.isFormValid(uiState) + ) } } private fun forgotPassword() { - if (uiState.value.email.isEmpty()) { - messageNotifier.notify("Please enter your email") + if (validator.isEmailValid(uiState.value.email.value)) { + messageNotifier.notify("Please enter a valid email") } else { - sendForgotPasswordEmail(uiState.value.email) + sendForgotPasswordEmail(uiState.value.email.value) } } @@ -114,8 +154,8 @@ class LoginViewModel @Inject constructor( navigator.navigateTo( AccountNavigationEvent.RegisterScreen( isGoogleSignUp = true, - email = uiState.value.email, - password = uiState.value.password + email = uiState.value.email.value, + password = uiState.value.password.value ) ) } @@ -133,8 +173,8 @@ class LoginViewModel @Inject constructor( private fun passwordSignIn() { viewModelScope.launch { val result = credentialLoginManagerAuth.handlePasswordSignIn( - _uiState.value.email, - _uiState.value.password, + _uiState.value.email.value, + _uiState.value.password.value, ) if (result) { accountEvents.emit(event = AccountEvents.Event.SignInSuccess) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/UiState.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/UiState.kt index f8cc16512..0a18d1323 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/UiState.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/UiState.kt @@ -1,12 +1,16 @@ package edu.stanford.spezi.module.account.login +import edu.stanford.spezi.module.account.register.FieldState + data class UiState( - val password: String = "", - val email: String = "", + val password: FieldState = FieldState(), + val email: FieldState = FieldState(), val passwordVisibility: Boolean = false, val showProgress: Boolean = false, val showFilterByAuthorizedAccounts: Boolean = true, + val isFormValid: Boolean = false, val isAlreadyRegistered: Boolean = false, + val hasAttemptedSubmit: Boolean = false, ) enum class TextFieldType { @@ -17,13 +21,8 @@ sealed interface Action { data class TextFieldUpdate(val newValue: String, val type: TextFieldType) : Action data object TogglePasswordVisibility : Action data object NavigateToRegister : Action - data object GoogleSignIn : Action - - data object GoogleSignUp : Action - + data object GoogleSignInOrSignUp : Action data object ForgotPassword : Action - - data object PasswordCredentialSignIn : Action - + data object PasswordSignInOrSignUp : Action data class SetIsAlreadyRegistered(val isAlreadyRegistered: Boolean) : Action } From 9b14a160cdb7e81b8370f7d6629dc1e51f9a16c4 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Fri, 14 Jun 2024 18:28:42 +0200 Subject: [PATCH 06/28] tested CredentialRegisterManagerAuth Signed-off-by: Basler182 --- .../CredentialRegisterManagerAuthTest.kt | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 modules/account/src/test/java/edu/stanford/spezi/module/account/cred/manager/CredentialRegisterManagerAuthTest.kt diff --git a/modules/account/src/test/java/edu/stanford/spezi/module/account/cred/manager/CredentialRegisterManagerAuthTest.kt b/modules/account/src/test/java/edu/stanford/spezi/module/account/cred/manager/CredentialRegisterManagerAuthTest.kt new file mode 100644 index 000000000..b2ea4ad99 --- /dev/null +++ b/modules/account/src/test/java/edu/stanford/spezi/module/account/cred/manager/CredentialRegisterManagerAuthTest.kt @@ -0,0 +1,99 @@ +package edu.stanford.spezi.module.account.cred.manager + +import android.content.Context +import androidx.credentials.CredentialManager +import com.google.common.truth.Truth.assertThat +import edu.stanford.spezi.core.testing.runTestUnconfined +import io.mockk.coEvery +import io.mockk.mockk +import org.junit.Before +import org.junit.Test +import java.time.LocalDate + +class CredentialRegisterManagerAuthTest { + + private lateinit var credentialRegisterManagerAuth: CredentialRegisterManagerAuth + private val firebaseAuthManager: FirebaseAuthManager = mockk() + private val credentialManager: CredentialManager = mockk() + private val context: Context = mockk() + + @Before + fun setUp() { + credentialRegisterManagerAuth = CredentialRegisterManagerAuth( + firebaseAuthManager, + credentialManager, + context + ) + } + + @Test + fun `given valid data when googleSignUp is called then return true`() = runTestUnconfined { + // Given + val idToken = "idToken" + val firstName = "Leland" + val lastName = "Stanford" + val email = "Leland.Stanford@example.com" + val selectedGender = "Male" + val dateOfBirth = LocalDate.now() + + coEvery { firebaseAuthManager.linkUserToGoogleAccount(idToken) } returns true + coEvery { + firebaseAuthManager.saveUserData( + firstName, + lastName, + email, + selectedGender, + dateOfBirth + ) + } returns true + + // When + val result = credentialRegisterManagerAuth.googleSignUp( + idToken, + firstName, + lastName, + email, + selectedGender, + dateOfBirth + ) + + // Then + assertThat(result.getOrNull()).isTrue() + } + + @Test + fun `given valid data when passwordAndEmailSignUp is called then return true`() = + runTestUnconfined { + // Given + val email = "Leland.Stanford@example.com" + val password = "password123" + val firstName = "Leland" + val lastName = "Stanford" + val selectedGender = "Male" + val dateOfBirth = LocalDate.now() + + coEvery { + firebaseAuthManager.signUpWithEmailAndPassword( + email, + password, + firstName, + lastName, + selectedGender, + dateOfBirth + ) + } returns Result.success(true) + + // When + val result = credentialRegisterManagerAuth.passwordAndEmailSignUp( + email, + password, + firstName, + lastName, + selectedGender, + dateOfBirth + ) + + // Then + assertThat(result.getOrNull()).isTrue() + } +} From d11b89af26a54df97ffc4f9f90c5531852735d21 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Fri, 14 Jun 2024 18:29:54 +0200 Subject: [PATCH 07/28] update RegisterFormValidatorTest Signed-off-by: Basler182 --- .../register/RegisterFormValidatorTest.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/account/src/test/java/edu/stanford/spezi/module/account/register/RegisterFormValidatorTest.kt b/modules/account/src/test/java/edu/stanford/spezi/module/account/register/RegisterFormValidatorTest.kt index c1c4ee8b7..7ca466370 100644 --- a/modules/account/src/test/java/edu/stanford/spezi/module/account/register/RegisterFormValidatorTest.kt +++ b/modules/account/src/test/java/edu/stanford/spezi/module/account/register/RegisterFormValidatorTest.kt @@ -17,7 +17,7 @@ class RegisterFormValidatorTest { val result = registerFormValidator.passwordResult(validPassword) // Then - assertThat(result).isEqualTo(RegisterFormValidator.Result.Valid) + assertThat(result).isEqualTo(FormValidator.Result.Valid) } @Test @@ -29,7 +29,7 @@ class RegisterFormValidatorTest { val result = registerFormValidator.passwordResult(invalidPassword) // Then - assertThat(result).isInstanceOf(RegisterFormValidator.Result.Invalid::class.java) + assertThat(result).isInstanceOf(FormValidator.Result.Invalid::class.java) } @Test @@ -41,7 +41,7 @@ class RegisterFormValidatorTest { val result = registerFormValidator.firstnameResult(validFirstName) // Then - assertThat(result).isEqualTo(RegisterFormValidator.Result.Valid) + assertThat(result).isEqualTo(FormValidator.Result.Valid) } @Test @@ -53,7 +53,7 @@ class RegisterFormValidatorTest { val result = registerFormValidator.firstnameResult(invalidFirstName) // Then - assertThat(result).isInstanceOf(RegisterFormValidator.Result.Invalid::class.java) + assertThat(result).isInstanceOf(FormValidator.Result.Invalid::class.java) } @Test @@ -65,7 +65,7 @@ class RegisterFormValidatorTest { val result = registerFormValidator.lastnameResult(validLastName) // Then - assertThat(result).isEqualTo(RegisterFormValidator.Result.Valid) + assertThat(result).isEqualTo(FormValidator.Result.Valid) } @Test @@ -77,7 +77,7 @@ class RegisterFormValidatorTest { val result = registerFormValidator.lastnameResult(invalidLastName) // Then - assertThat(result).isInstanceOf(RegisterFormValidator.Result.Invalid::class.java) + assertThat(result).isInstanceOf(FormValidator.Result.Invalid::class.java) } @Test @@ -89,7 +89,7 @@ class RegisterFormValidatorTest { val result = registerFormValidator.birthdayResult(validDateOfBirth) // Then - assertThat(result).isEqualTo(RegisterFormValidator.Result.Valid) + assertThat(result).isEqualTo(FormValidator.Result.Valid) } @Test @@ -101,6 +101,6 @@ class RegisterFormValidatorTest { val result = registerFormValidator.birthdayResult(invalidDateOfBirth) // Then - assertThat(result).isInstanceOf(RegisterFormValidator.Result.Invalid::class.java) + assertThat(result).isInstanceOf(FormValidator.Result.Invalid::class.java) } } From 93c97d9a3fdd4360226b4233d9979eeadd19ad71 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Fri, 14 Jun 2024 18:31:35 +0200 Subject: [PATCH 08/28] tested LoginViewModel Signed-off-by: Basler182 --- .../module/account/login/LoginViewModel.kt | 4 +- .../account/login/LoginViewModelTest.kt | 134 ++++++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 modules/account/src/test/java/edu/stanford/spezi/module/account/login/LoginViewModelTest.kt diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginViewModel.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginViewModel.kt index 05e187a38..47367924d 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginViewModel.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginViewModel.kt @@ -144,9 +144,9 @@ internal class LoginViewModel @Inject constructor( private fun forgotPassword() { if (validator.isEmailValid(uiState.value.email.value)) { - messageNotifier.notify("Please enter a valid email") - } else { sendForgotPasswordEmail(uiState.value.email.value) + } else { + messageNotifier.notify("Please enter a valid email") } } diff --git a/modules/account/src/test/java/edu/stanford/spezi/module/account/login/LoginViewModelTest.kt b/modules/account/src/test/java/edu/stanford/spezi/module/account/login/LoginViewModelTest.kt new file mode 100644 index 000000000..af5c11983 --- /dev/null +++ b/modules/account/src/test/java/edu/stanford/spezi/module/account/login/LoginViewModelTest.kt @@ -0,0 +1,134 @@ +package edu.stanford.spezi.module.account.login + +import com.google.common.truth.Truth.assertThat +import edu.stanford.spezi.core.navigation.Navigator +import edu.stanford.spezi.core.testing.runTestUnconfined +import edu.stanford.spezi.core.utils.MessageNotifier +import edu.stanford.spezi.module.account.AccountEvents +import edu.stanford.spezi.module.account.AccountNavigationEvent +import edu.stanford.spezi.module.account.cred.manager.CredentialLoginManagerAuth +import io.mockk.Runs +import io.mockk.coVerify +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.setMain +import org.junit.Before +import org.junit.Test + +class LoginViewModelTest { + + private lateinit var loginViewModel: LoginViewModel + private val credentialLoginManagerAuth: CredentialLoginManagerAuth = mockk(relaxed = true) + private val messageNotifier: MessageNotifier = mockk(relaxed = true) + private val accountEvents: AccountEvents = mockk(relaxed = true) + private val validator: LoginFormValidator = LoginFormValidator() + private val navigator: Navigator = mockk(relaxed = true) + + @Before + fun setUp() { + loginViewModel = LoginViewModel( + credentialLoginManagerAuth = credentialLoginManagerAuth, + messageNotifier = messageNotifier, + accountEvents = accountEvents, + navigator = navigator, + validator = validator + ) + every { navigator.navigateTo(any()) } just Runs + Dispatchers.setMain(UnconfinedTestDispatcher()) + } + + @Test + fun `given TextFieldUpdate action when onAction is called then update LoginUiState`() = + runTestUnconfined { + // Given + val email = "test@test.com" + val action = Action.TextFieldUpdate(email, TextFieldType.EMAIL) + + // When + loginViewModel.onAction(action) + + // Then + val uiState = loginViewModel.uiState.value + assertThat(uiState.email.value).isEqualTo(email) + } + + @Test + fun `given TogglePasswordVisibility action when onAction is called then update LoginUiState`() = + runTestUnconfined { + // Given + val action = Action.TogglePasswordVisibility + + // When + loginViewModel.onAction(action) + + // Then + val uiState = loginViewModel.uiState.value + assertThat(uiState.passwordVisibility).isTrue() + } + + @Test + fun `given NavigateToRegister action when onAction is called then navigate to RegisterScreen`() = + runTestUnconfined { + // Given + val action = Action.NavigateToRegister + val expectedNavigationEvent = AccountNavigationEvent.RegisterScreen( + isGoogleSignUp = false, + email = loginViewModel.uiState.value.email.value, + password = loginViewModel.uiState.value.password.value, + ) + + // When + loginViewModel.onAction(action) + + // Then + verify { navigator.navigateTo(expectedNavigationEvent) } + } + + @Test + fun `given SetIsAlreadyRegistered action when onAction is called then update isAlreadyRegistered in LoginUiState`() = + runTestUnconfined { + // Given + val action = Action.SetIsAlreadyRegistered(true) + + // When + loginViewModel.onAction(action) + + // Then + val uiState = loginViewModel.uiState.value + assertThat(uiState.isAlreadyRegistered).isTrue() + } + + @Test + fun `given ForgotPassword action with valid email when onAction is called then send forgot password email`() = + runTestUnconfined { + // Given + val action = Action.ForgotPassword + val validEmail = "test@test.com" + + // When + loginViewModel.onAction(Action.TextFieldUpdate(validEmail, TextFieldType.EMAIL)) + loginViewModel.onAction(action) + + // Then + coVerify { credentialLoginManagerAuth.sendForgotPasswordEmail(validEmail) } + } + + @Test + fun `given ForgotPassword action with invalid email when onAction is called then do not send forgot password email`() = + runTestUnconfined { + // Given + val action = Action.ForgotPassword + val invalidEmail = "invalidEmail" + + // When + loginViewModel.onAction(Action.TextFieldUpdate(invalidEmail, TextFieldType.EMAIL)) + loginViewModel.onAction(action) + + // Then + coVerify(exactly = 0) { credentialLoginManagerAuth.sendForgotPasswordEmail(invalidEmail) } + } +} From 18e6dc816995e7bc2e188124d35b81fec081221f Mon Sep 17 00:00:00 2001 From: Basler182 Date: Fri, 14 Jun 2024 18:33:49 +0200 Subject: [PATCH 09/28] switched to PattersCompat Email Matcher for easier testing Signed-off-by: Basler182 --- .../stanford/spezi/module/account/register/FormValidator.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/FormValidator.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/FormValidator.kt index 94a1055c4..7a3c5696e 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/FormValidator.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/FormValidator.kt @@ -1,8 +1,10 @@ package edu.stanford.spezi.module.account.register +import androidx.core.util.PatternsCompat + internal open class FormValidator { fun isValidEmail(email: String): Boolean { - return android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() + return PatternsCompat.EMAIL_ADDRESS.matcher(email).matches() } fun isValidPassword(password: String): Boolean { From decf4e38c74b48cc61d5f1295b50ea817418bd9d Mon Sep 17 00:00:00 2001 From: Basler182 Date: Fri, 14 Jun 2024 18:35:20 +0200 Subject: [PATCH 10/28] tested LoginFormValidator Signed-off-by: Basler182 --- .../account/login/LoginFormValidatorTest.kt | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 modules/account/src/test/java/edu/stanford/spezi/module/account/login/LoginFormValidatorTest.kt diff --git a/modules/account/src/test/java/edu/stanford/spezi/module/account/login/LoginFormValidatorTest.kt b/modules/account/src/test/java/edu/stanford/spezi/module/account/login/LoginFormValidatorTest.kt new file mode 100644 index 000000000..b6c9862c8 --- /dev/null +++ b/modules/account/src/test/java/edu/stanford/spezi/module/account/login/LoginFormValidatorTest.kt @@ -0,0 +1,93 @@ +package edu.stanford.spezi.module.account.login + +import com.google.common.truth.Truth.assertThat +import edu.stanford.spezi.module.account.register.FieldState +import edu.stanford.spezi.module.account.register.FormValidator +import org.junit.Test + +class LoginFormValidatorTest { + + private val loginFormValidator = LoginFormValidator() + + @Test + fun `given valid email when emailResult is called then return Valid`() { + // Given + val validEmail = "test@test.com" + + // When + val result = loginFormValidator.emailResult(validEmail) + + // Then + assertThat(result).isEqualTo(FormValidator.Result.Valid) + } + + @Test + fun `given invalid email when emailResult is called then return Invalid`() { + // Given + val invalidEmail = "invalidEmail" + + // When + val result = loginFormValidator.emailResult(invalidEmail) + + // Then + assertThat(result).isInstanceOf(FormValidator.Result.Invalid::class.java) + } + + @Test + fun `given valid password when passwordResult is called then return Valid`() { + // Given + val validPassword = "password123" + + // When + val result = loginFormValidator.passwordResult(validPassword) + + // Then + assertThat(result).isEqualTo(FormValidator.Result.Valid) + } + + @Test + fun `given invalid password when passwordResult is called then return Invalid`() { + // Given + val invalidPassword = "pass" + + // When + val result = loginFormValidator.passwordResult(invalidPassword) + + // Then + assertThat(result).isInstanceOf(FormValidator.Result.Invalid::class.java) + } + + @Test + fun `given valid form when isFormValid is called then return true`() { + // Given + val validEmail = "test@test.com" + val validPassword = "password123" + val uiState = UiState( + email = FieldState(value = validEmail), + password = FieldState(value = validPassword) + ) + + // When + val result = loginFormValidator.isFormValid(uiState) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `given invalid form when isFormValid is called then return false`() { + // Given + val invalidEmail = "invalidEmail" + val invalidPassword = "pass" + val uiState = UiState( + email = FieldState(value = invalidEmail), + password = FieldState(value = invalidPassword) + ) + + // When + val result = loginFormValidator.isFormValid(uiState) + + // Then + assertThat(result).isFalse() + } +} From c170066f45fd76e0431c6ca0184993f80cab8f28 Mon Sep 17 00:00:00 2001 From: Kilian Schneider <48420258+Basler182@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:25:18 +0200 Subject: [PATCH 11/28] Fix/issue 32 leading aligned icons register (#44) # *Fix/issue 32 leading aligned icons register* Could be either merged into #43 or main once #43 is merged ## :recycle: Current situation & Problem #32 ## :gear: Release Notes - IconLeadingContent Composable - Adapted RegisterScreen accordingly ## :pencil: Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md). --------- Signed-off-by: Basler182 --- app/src/main/AndroidManifest.xml | 1 + .../edu/stanford/bdh/engagehf/MainActivity.kt | 9 +- .../spezi/module/account/login/LoginScreen.kt | 112 +++++--- .../account/register/IconLeadingContent.kt | 66 +++++ .../module/account/register/RegisterScreen.kt | 258 ++++++++++++------ .../account/register/RegisterUiState.kt | 5 +- .../account/register/RegisterViewModel.kt | 23 +- 7 files changed, 340 insertions(+), 134 deletions(-) create mode 100644 modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/IconLeadingContent.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 35bd7f30d..04f942ae1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,7 @@ diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/MainActivity.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/MainActivity.kt index 6d11c9e34..da89bcbf2 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/MainActivity.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/MainActivity.kt @@ -130,12 +130,17 @@ class MainActivity : ComponentActivity() { Routes.InvitationCodeScreen ) - is OnboardingNavigationEvent.OnboardingScreen -> navHostController.navigate(Routes.OnboardingScreen) + is OnboardingNavigationEvent.OnboardingScreen -> navHostController.navigate( + Routes.OnboardingScreen + ) + is OnboardingNavigationEvent.SequentialOnboardingScreen -> navHostController.navigate( Routes.SequentialOnboardingScreen ) - is OnboardingNavigationEvent.ConsentScreen -> navHostController.navigate(Routes.ConsentScreen) + is OnboardingNavigationEvent.ConsentScreen -> navHostController.navigate( + Routes.ConsentScreen + ) } } } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginScreen.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginScreen.kt index c9d8e9249..a6e36e803 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginScreen.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginScreen.kt @@ -2,6 +2,7 @@ package edu.stanford.spezi.module.account.login +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -9,9 +10,15 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Email +import androidx.compose.material.icons.outlined.Lock import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -23,6 +30,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -39,6 +48,7 @@ import edu.stanford.spezi.core.design.theme.TextStyles.titleLarge import edu.stanford.spezi.module.account.login.components.SignInWithGoogleButton import edu.stanford.spezi.module.account.login.components.TextDivider import edu.stanford.spezi.module.account.register.FieldState +import edu.stanford.spezi.module.account.register.IconLeadingContent @Composable fun LoginScreen( @@ -58,10 +68,22 @@ internal fun LoginScreen( uiState: UiState, onAction: (Action) -> Unit, ) { + val keyboardController = LocalSoftwareKeyboardController.current + Column( modifier = Modifier .fillMaxSize() - .padding(Spacings.medium), + .imePadding() + .verticalScroll(rememberScrollState()) + .padding(Spacings.medium) + .pointerInput(Unit) { + detectTapGestures( + onTap = { + println("Hide Keyboard") + keyboardController?.hide() + } + ) + }, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { @@ -77,49 +99,57 @@ You may login to your existing account or create a new one if you don't have one style = bodyLarge, ) Spacer(modifier = Modifier.height(Spacings.large)) - ValidatedOutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = uiState.email.value, - errorText = uiState.email.error, - onValueChange = { email -> - onAction(Action.TextFieldUpdate(email, TextFieldType.EMAIL)) - }, - labelText = "E-Mail Address", - singleLine = true, - keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next) - ) + IconLeadingContent( + icon = Icons.Outlined.Email, + content = { + ValidatedOutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = uiState.email.value, + errorText = uiState.email.error, + onValueChange = { email -> + onAction(Action.TextFieldUpdate(email, TextFieldType.EMAIL)) + }, + labelText = "E-Mail Address", + singleLine = true, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next) + ) + }) Spacer(modifier = Modifier.height(Spacings.small)) - ValidatedOutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = uiState.password.value, - errorText = uiState.password.error, - onValueChange = { - onAction(Action.TextFieldUpdate(it, TextFieldType.PASSWORD)) - }, - labelText = "Password", - visualTransformation = if (uiState.passwordVisibility) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { - onAction(Action.PasswordSignInOrSignUp) - }), - trailingIcon = { - IconButton(onClick = { onAction(Action.TogglePasswordVisibility) }) { - val icon: Painter = if (uiState.passwordVisibility) { - painterResource(id = edu.stanford.spezi.core.design.R.drawable.ic_visibility) + IconLeadingContent( + icon = Icons.Outlined.Lock, + content = { + ValidatedOutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = uiState.password.value, + errorText = uiState.password.error, + onValueChange = { + onAction(Action.TextFieldUpdate(it, TextFieldType.PASSWORD)) + }, + labelText = "Password", + visualTransformation = if (uiState.passwordVisibility) { + VisualTransformation.None } else { - painterResource(id = edu.stanford.spezi.core.design.R.drawable.ic_visibility_off) + PasswordVisualTransformation() + }, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { + onAction(Action.PasswordSignInOrSignUp) + }), + trailingIcon = { + IconButton(onClick = { onAction(Action.TogglePasswordVisibility) }) { + val icon: Painter = if (uiState.passwordVisibility) { + painterResource(id = edu.stanford.spezi.core.design.R.drawable.ic_visibility) + } else { + painterResource(id = edu.stanford.spezi.core.design.R.drawable.ic_visibility_off) + } + Icon( + painter = icon, + contentDescription = if (uiState.passwordVisibility) "Hide password" else "Show password" + ) + } } - Icon( - painter = icon, - contentDescription = if (uiState.passwordVisibility) "Hide password" else "Show password" - ) - } - } - ) + ) + }) TextButton( onClick = { onAction(Action.ForgotPassword) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/IconLeadingContent.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/IconLeadingContent.kt new file mode 100644 index 000000000..5f36c1289 --- /dev/null +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/IconLeadingContent.kt @@ -0,0 +1,66 @@ +package edu.stanford.spezi.module.account.register + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountBox +import androidx.compose.material.icons.filled.Person +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import edu.stanford.spezi.core.design.theme.Sizes +import edu.stanford.spezi.core.design.theme.Spacings + +@Composable +internal fun IconLeadingContent( + modifier: Modifier = Modifier, + icon: ImageVector? = null, + content: @Composable () -> Unit = {}, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.fillMaxWidth() + ) { + if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(Sizes.Icon.small) + ) + Spacer(modifier = Modifier.width(Spacings.small)) + } else { + // Reserve the space for the icon + Spacer(modifier = Modifier.width(Sizes.Icon.small + Spacings.small)) + } + content() + } +} + +@Preview +@Composable +private fun IconLeadingContentPreview( + @PreviewParameter(IconLeadingContentPreviewProvider::class) params: Pair, +) { + IconLeadingContent( + icon = params.first, + content = { Text(params.second) } + ) +} + +private class IconLeadingContentPreviewProvider : + PreviewParameterProvider> { + override val values: Sequence> = sequenceOf( + Pair(Icons.Default.AccountBox, "Account Information"), + Pair(Icons.Default.Person, "Personal Information"), + Pair(null, "No Icon") + ) +} diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt index c8d8a7223..7bdcaaa56 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt @@ -1,23 +1,26 @@ package edu.stanford.spezi.module.account.register +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imeNestedScroll import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.outlined.Email +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.outlined.Person import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -28,13 +31,18 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -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.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -45,7 +53,6 @@ import edu.stanford.spezi.core.design.theme.Colors.primary import edu.stanford.spezi.core.design.theme.Sizes import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme -import edu.stanford.spezi.core.design.theme.TextStyles.labelLarge import edu.stanford.spezi.core.design.theme.TextStyles.titleLarge import edu.stanford.spezi.core.design.theme.TextStyles.titleSmall import java.time.LocalDate @@ -71,19 +78,27 @@ fun RegisterScreen( ) } -@OptIn(ExperimentalLayoutApi::class) @Composable fun RegisterScreen( uiState: RegisterUiState, onAction: (Action) -> Unit, ) { + val keyboardController = LocalSoftwareKeyboardController.current + val genderFocus = remember { FocusRequester() } + val dateOfBirthFocus = remember { FocusRequester() } Column( modifier = Modifier .fillMaxSize() .padding(Spacings.medium) .imePadding() - .imeNestedScroll() - .verticalScroll(rememberScrollState()), + .verticalScroll(rememberScrollState()) + .pointerInput(Unit) { + detectTapGestures( + onTap = { + keyboardController?.hide() + } + ) + }, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { @@ -105,55 +120,97 @@ fun RegisterScreen( style = titleSmall, ) VerticalSpacer(height = Spacings.large) - Text("CREDENTIALS", style = labelLarge) - ValidatedOutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = uiState.email.value, - onValueChange = { - onAction(Action.TextFieldUpdate(it, TextFieldType.EMAIL)) - }, - labelText = "E-Mail Address", - errorText = uiState.email.error, - ) + IconLeadingContent( + icon = Icons.Outlined.Email, + content = { + ValidatedOutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = uiState.email.value, + onValueChange = { + onAction(Action.TextFieldUpdate(it, TextFieldType.EMAIL)) + }, + labelText = "E-Mail Address", + errorText = uiState.email.error, + ) + }) VerticalSpacer(height = Spacings.small) if (!uiState.isGoogleSignUp) { - ValidatedOutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = uiState.password.value, - onValueChange = { - onAction(Action.TextFieldUpdate(it, TextFieldType.PASSWORD)) - }, - labelText = "Password", - errorText = uiState.password.error, - visualTransformation = PasswordVisualTransformation(), - ) + IconLeadingContent( + icon = Icons.Outlined.Lock, + content = { + ValidatedOutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = uiState.password.value, + onValueChange = { + onAction(Action.TextFieldUpdate(it, TextFieldType.PASSWORD)) + }, + labelText = "Password", + errorText = uiState.password.error, + trailingIcon = { + IconButton(onClick = { onAction(Action.TogglePasswordVisibility) }) { + val icon: Painter = if (uiState.isPasswordVisible) { + painterResource(id = edu.stanford.spezi.core.design.R.drawable.ic_visibility) + } else { + painterResource(id = edu.stanford.spezi.core.design.R.drawable.ic_visibility_off) + } + Icon( + painter = icon, + contentDescription = if (uiState.isPasswordVisible) "Hide password" else "Show password" + ) + } + }, + visualTransformation = if (uiState.isPasswordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + ) + }) } - VerticalSpacer() - Text("NAME", style = labelLarge) - ValidatedOutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = uiState.firstName.value, - onValueChange = { onAction(Action.TextFieldUpdate(it, TextFieldType.FIRST_NAME)) }, - labelText = "First Name", - errorText = uiState.firstName.error, - ) + IconLeadingContent( + icon = Icons.Outlined.Person, + content = { + ValidatedOutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = uiState.firstName.value, + onValueChange = { + onAction( + Action.TextFieldUpdate( + it, + TextFieldType.FIRST_NAME + ) + ) + }, + labelText = "First Name", + errorText = uiState.firstName.error, + ) + }) VerticalSpacer(height = Spacings.small) - ValidatedOutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = uiState.lastName.value, - onValueChange = { - onAction(Action.TextFieldUpdate(it, TextFieldType.LAST_NAME)) - }, - labelText = "Last Name", - errorText = uiState.lastName.error, - ) - VerticalSpacer() - Text("PERSONAL DETAILS", style = labelLarge) + IconLeadingContent( + content = { + ValidatedOutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = uiState.lastName.value, + onValueChange = { + onAction(Action.TextFieldUpdate(it, TextFieldType.LAST_NAME)) + }, + labelText = "Last Name", + errorText = uiState.lastName.error, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { + genderFocus.requestFocus() + if (uiState.selectedGender.value.isEmpty()) { + onAction(Action.DropdownMenuExpandedUpdate(true)) + } + }) + ) + }) DropdownMenu( modifier = Modifier.fillMaxWidth(), expanded = uiState.isDropdownMenuExpanded, onDismissRequest = { onAction(Action.DropdownMenuExpandedUpdate(false)) + dateOfBirthFocus.requestFocus() } ) { uiState.genderOptions.forEach { gender -> @@ -161,52 +218,79 @@ fun RegisterScreen( onClick = { onAction(Action.TextFieldUpdate(gender, TextFieldType.GENDER)) onAction(Action.DropdownMenuExpandedUpdate(false)) + dateOfBirthFocus.requestFocus() }) } } - - ValidatedOutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = uiState.selectedGender.value, - onValueChange = { - onAction(Action.TextFieldUpdate(it, TextFieldType.GENDER)) - }, - labelText = "Gender", - readOnly = true, - trailingIcon = { - IconButton(onClick = { - onAction(Action.DropdownMenuExpandedUpdate(true)) - }) { - Icon(Icons.Filled.ArrowDropDown, contentDescription = "Select Gender") - } - }, - errorText = uiState.selectedGender.error, - ) + IconLeadingContent( + content = { + ValidatedOutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .focusRequester(genderFocus), + value = uiState.selectedGender.value, + onValueChange = { + onAction(Action.TextFieldUpdate(it, TextFieldType.GENDER)) + }, + labelText = "Gender", + readOnly = true, + trailingIcon = { + IconButton(onClick = { + onAction(Action.DropdownMenuExpandedUpdate(true)) + }) { + Icon( + Icons.Filled.ArrowDropDown, + contentDescription = "Select Gender", + ) + } + }, + errorText = uiState.selectedGender.error, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { + dateOfBirthFocus.requestFocus() + if (uiState.selectedGender.value.isNotEmpty()) { + onAction(Action.SetIsDatePickerOpen(true)) + } + }), + ) + }) VerticalSpacer(height = Spacings.small) - var isDatePickerDialogOpen by remember { mutableStateOf(false) } - ValidatedOutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = uiState.dateOfBirth?.format(DateTimeFormatter.ofPattern("dd MMMM yyyy")) ?: "", - onValueChange = { /* Do nothing as we handle the date through the DatePicker */ }, - labelText = "Date of Birth", - singleLine = true, - keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next), - trailingIcon = { - IconButton(onClick = { isDatePickerDialogOpen = true }) { - Icon(Icons.Filled.Edit, contentDescription = "Select Date") - } - }, - readOnly = true - ) + IconLeadingContent( + content = { + ValidatedOutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .focusRequester(dateOfBirthFocus), + value = uiState.dateOfBirth?.format(DateTimeFormatter.ofPattern("dd MMMM yyyy")) + ?: "", + onValueChange = { /* Do nothing as we handle the date through the DatePicker */ }, + labelText = "Date of Birth", + singleLine = true, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), + trailingIcon = { + IconButton(onClick = { + onAction(Action.SetIsDatePickerOpen(true)) + }) { + Icon(Icons.Filled.Edit, contentDescription = "Select Date") + } + }, + readOnly = true, + keyboardActions = KeyboardActions(onDone = { + onAction(Action.OnRegisterPressed) + }) + ) + }) - if (isDatePickerDialogOpen) { + if (uiState.isDatePickerDialogOpen) { DatePickerDialog( onDateSelected = { date -> onAction(Action.DateFieldUpdate(date)) - isDatePickerDialogOpen = false + onAction(Action.SetIsDatePickerOpen(false)) }, - onDismiss = { isDatePickerDialogOpen = false } + onDismiss = { + onAction(Action.SetIsDatePickerOpen(false)) + } ) } @@ -216,7 +300,11 @@ fun RegisterScreen( onAction(Action.OnRegisterPressed) }, modifier = Modifier.fillMaxWidth(), - enabled = uiState.isFormValid + enabled = uiState.email.value.isNotEmpty() && + uiState.firstName.value.isNotEmpty() && + uiState.lastName.value.isNotEmpty() && + uiState.selectedGender.value.isNotEmpty() && + uiState.dateOfBirth != null ) { Text("Signup") } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterUiState.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterUiState.kt index b14b77c89..88062e947 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterUiState.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterUiState.kt @@ -11,9 +11,11 @@ data class RegisterUiState( val dateOfBirth: LocalDate? = null, val dateOfBirthError: String? = null, val isDropdownMenuExpanded: Boolean = false, + val isDatePickerDialogOpen: Boolean = false, val isFormValid: Boolean = false, val genderOptions: List = listOf("Male", "Female", "Other"), val isGoogleSignUp: Boolean = false, + val isPasswordVisible: Boolean = false, ) data class FieldState( @@ -34,6 +36,7 @@ sealed interface Action { data class DateFieldUpdate(val newValue: LocalDate) : Action data class DropdownMenuExpandedUpdate(val isExpanded: Boolean) : Action data object OnRegisterPressed : Action - + data object TogglePasswordVisibility : Action data class SetIsGoogleSignUp(val isGoogleSignUp: Boolean) : Action + data class SetIsDatePickerOpen(val isOpen: Boolean) : Action } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterViewModel.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterViewModel.kt index 6c2c9b529..85b46c442 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterViewModel.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterViewModel.kt @@ -51,8 +51,9 @@ class RegisterViewModel @Inject internal constructor( it.copy(isDropdownMenuExpanded = action.isExpanded) } - Action.OnRegisterPressed -> { - onRegisteredPressed() + is Action.OnRegisterPressed -> { + _uiState.value = onRegisteredPressed() + it } is Action.SetIsGoogleSignUp -> { @@ -61,6 +62,14 @@ class RegisterViewModel @Inject internal constructor( } it.copy(isGoogleSignUp = action.isGoogleSignUp) } + + is Action.TogglePasswordVisibility -> { + it.copy(isPasswordVisible = !it.isPasswordVisible) + } + + is Action.SetIsDatePickerOpen -> { + it.copy(isDatePickerDialogOpen = action.isOpen) + } } } } @@ -125,7 +134,9 @@ class RegisterViewModel @Inject internal constructor( uiState } else { uiState.copy( - email = uiState.email.copy(error = validator.emailResult(uiState.email.value).errorMessageOrNull()), + email = uiState.email.copy( + error = validator.emailResult(uiState.email.value).errorMessageOrNull() + ), password = uiState.password.copy( error = validator.passwordResult(uiState.password.value).errorMessageOrNull() ), @@ -136,9 +147,11 @@ class RegisterViewModel @Inject internal constructor( error = validator.lastnameResult(uiState.lastName.value).errorMessageOrNull() ), selectedGender = uiState.selectedGender.copy( - error = validator.isGenderValid(uiState.selectedGender.value).errorMessageOrNull() + error = validator.isGenderValid(uiState.selectedGender.value) + .errorMessageOrNull() ), - dateOfBirthError = validator.birthdayResult(uiState.dateOfBirth).errorMessageOrNull(), + dateOfBirthError = validator.birthdayResult(uiState.dateOfBirth) + .errorMessageOrNull(), isFormValid = validator.isFormValid(uiState) ) } From bab77f2d69a5c4e31079736dbab68f5b50f019ac Mon Sep 17 00:00:00 2001 From: Basler182 Date: Fri, 21 Jun 2024 09:52:01 +0200 Subject: [PATCH 12/28] vertical aligned leading icon to top Signed-off-by: Basler182 --- .../ValidatedOutlinedTextField.kt | 6 ++++-- .../account/register/IconLeadingContent.kt | 20 ++++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/validated/outlinedtextfield/ValidatedOutlinedTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/validated/outlinedtextfield/ValidatedOutlinedTextField.kt index b77e9ff39..23a0519c9 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/validated/outlinedtextfield/ValidatedOutlinedTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/validated/outlinedtextfield/ValidatedOutlinedTextField.kt @@ -44,12 +44,14 @@ fun ValidatedOutlinedTextField( trailingIcon = trailingIcon, keyboardActions = keyboardActions, visualTransformation = visualTransformation, - supportingText = { - if (errorText != null) { + supportingText = if (errorText != null) { + { Text( text = errorText, ) } + } else { + null }, ) } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/IconLeadingContent.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/IconLeadingContent.kt index 5f36c1289..f48f7f222 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/IconLeadingContent.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/IconLeadingContent.kt @@ -3,13 +3,13 @@ package edu.stanford.spezi.module.account.register import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountBox import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Icon -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import edu.stanford.spezi.core.design.component.validated.outlinedtextfield.ValidatedOutlinedTextField import edu.stanford.spezi.core.design.theme.Sizes import edu.stanford.spezi.core.design.theme.Spacings @@ -27,16 +28,18 @@ internal fun IconLeadingContent( content: @Composable () -> Unit = {}, ) { Row( - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.Top, modifier = modifier.fillMaxWidth() ) { if (icon != null) { Icon( imageVector = icon, contentDescription = null, - modifier = Modifier.size(Sizes.Icon.small) + modifier = Modifier + .size(Sizes.Icon.small) + .offset(y = Spacings.small) ) - Spacer(modifier = Modifier.width(Spacings.small)) + Spacer(modifier = Modifier.width(Spacings.medium)) } else { // Reserve the space for the icon Spacer(modifier = Modifier.width(Sizes.Icon.small + Spacings.small)) @@ -52,7 +55,14 @@ private fun IconLeadingContentPreview( ) { IconLeadingContent( icon = params.first, - content = { Text(params.second) } + content = { + ValidatedOutlinedTextField( + labelText = params.second, + value = "", + onValueChange = {}, + errorText = null + ) + } ) } From 475d254f2050bfac7ecc6b25edfd6042762616f4 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Fri, 21 Jun 2024 09:58:20 +0200 Subject: [PATCH 13/28] invoked painter resource on iconId Signed-off-by: Basler182 --- .../spezi/module/account/register/RegisterScreen.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt index 7bdcaaa56..93f0def27 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt @@ -36,7 +36,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource @@ -148,13 +147,13 @@ fun RegisterScreen( errorText = uiState.password.error, trailingIcon = { IconButton(onClick = { onAction(Action.TogglePasswordVisibility) }) { - val icon: Painter = if (uiState.isPasswordVisible) { - painterResource(id = edu.stanford.spezi.core.design.R.drawable.ic_visibility) + val iconId: Int = if (uiState.isPasswordVisible) { + edu.stanford.spezi.core.design.R.drawable.ic_visibility } else { - painterResource(id = edu.stanford.spezi.core.design.R.drawable.ic_visibility_off) + edu.stanford.spezi.core.design.R.drawable.ic_visibility_off } Icon( - painter = icon, + painter = painterResource(id = iconId), contentDescription = if (uiState.isPasswordVisible) "Hide password" else "Show password" ) } From 9217d44d68b466e67dc976f2ae2245d221489174 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Fri, 21 Jun 2024 10:07:24 +0200 Subject: [PATCH 14/28] added local date format and also moved formatting into view model Signed-off-by: Basler182 --- .../spezi/module/account/register/RegisterScreen.kt | 4 +--- .../module/account/register/RegisterUiState.kt | 1 + .../module/account/register/RegisterViewModel.kt | 13 ++++++++++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt index 93f0def27..d8866a592 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt @@ -55,7 +55,6 @@ import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.TextStyles.titleLarge import edu.stanford.spezi.core.design.theme.TextStyles.titleSmall import java.time.LocalDate -import java.time.format.DateTimeFormatter @Composable fun RegisterScreen( @@ -261,8 +260,7 @@ fun RegisterScreen( modifier = Modifier .fillMaxWidth() .focusRequester(dateOfBirthFocus), - value = uiState.dateOfBirth?.format(DateTimeFormatter.ofPattern("dd MMMM yyyy")) - ?: "", + value = uiState.formattedDateOfBirth, onValueChange = { /* Do nothing as we handle the date through the DatePicker */ }, labelText = "Date of Birth", singleLine = true, diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterUiState.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterUiState.kt index 88062e947..7518fc111 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterUiState.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterUiState.kt @@ -9,6 +9,7 @@ data class RegisterUiState( val lastName: FieldState = FieldState(), val selectedGender: FieldState = FieldState(), val dateOfBirth: LocalDate? = null, + val formattedDateOfBirth: String = "", val dateOfBirthError: String? = null, val isDropdownMenuExpanded: Boolean = false, val isDatePickerDialogOpen: Boolean = false, diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterViewModel.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterViewModel.kt index 85b46c442..0e5f9cb9f 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterViewModel.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterViewModel.kt @@ -11,6 +11,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.time.format.DateTimeFormatter +import java.util.Locale import javax.inject.Inject @HiltViewModel @@ -43,7 +45,16 @@ class RegisterViewModel @Inject internal constructor( } is Action.DateFieldUpdate -> { - val updatedUiState = it.copy(dateOfBirth = action.newValue) + val deviceLocale: Locale = Locale.getDefault() + val updatedUiState = it.copy( + dateOfBirth = action.newValue, + formattedDateOfBirth = action.newValue.format( + DateTimeFormatter.ofPattern( + "dd MMMM yyyy", + deviceLocale + ) + ) + ) updatedUiState.copy(isFormValid = validator.isFormValid(updatedUiState)) } From f02427be595db8e7ee892f018efc51b9b149c360 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sat, 22 Jun 2024 21:06:05 +0200 Subject: [PATCH 15/28] used typealias composable block Signed-off-by: Basler182 --- .../outlinedtextfield/ValidatedOutlinedTextField.kt | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/validated/outlinedtextfield/ValidatedOutlinedTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/validated/outlinedtextfield/ValidatedOutlinedTextField.kt index 23a0519c9..da01eb59c 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/validated/outlinedtextfield/ValidatedOutlinedTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/validated/outlinedtextfield/ValidatedOutlinedTextField.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import edu.stanford.spezi.core.utils.ComposableBlock @Composable fun ValidatedOutlinedTextField( @@ -26,7 +27,7 @@ fun ValidatedOutlinedTextField( keyboardOptions: KeyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next), visualTransformation: VisualTransformation = VisualTransformation.None, readOnly: Boolean = false, - trailingIcon: @Composable (() -> Unit)? = null, + trailingIcon: ComposableBlock? = null, keyboardActions: KeyboardActions = KeyboardActions.Default, ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { @@ -44,14 +45,8 @@ fun ValidatedOutlinedTextField( trailingIcon = trailingIcon, keyboardActions = keyboardActions, visualTransformation = visualTransformation, - supportingText = if (errorText != null) { - { - Text( - text = errorText, - ) - } - } else { - null + supportingText = errorText?.let { + { Text(text = it) } }, ) } From c4f0444d808c7a1e2d9f4c1e73e1ebaf4f77d9c8 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sat, 22 Jun 2024 21:08:32 +0200 Subject: [PATCH 16/28] improved FormValidator Signed-off-by: Basler182 --- .../account/login/LoginFormValidator.kt | 20 ++--------- .../module/account/register/FormValidator.kt | 22 +++++++++--- .../account/register/RegisterFormValidator.kt | 34 ++++++++----------- 3 files changed, 35 insertions(+), 41 deletions(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginFormValidator.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginFormValidator.kt index 11fe37ee1..23222cfcc 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginFormValidator.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginFormValidator.kt @@ -3,24 +3,10 @@ package edu.stanford.spezi.module.account.login import edu.stanford.spezi.module.account.register.FormValidator import javax.inject.Inject -internal class LoginFormValidator @Inject internal constructor() : FormValidator() { - - fun emailResult(email: String): Result = if (isValidEmail(email)) { - Result.Valid - } else { - Result.Invalid("Invalid email") - } - - fun passwordResult(password: String): Result = if (isValidPassword(password)) { - Result.Valid - } else { - Result.Invalid("Password must be at least $MIN_PASSWORD_LENGTH characters") - } +internal class LoginFormValidator @Inject constructor() : FormValidator() { fun isFormValid(uiState: UiState): Boolean { - return emailResult(uiState.email.value) is Result.Valid && - passwordResult(uiState.password.value) is Result.Valid + return isValidEmail(uiState.email.value).isValid && + isValidPassword(uiState.password.value).isValid } - - fun isEmailValid(email: String): Boolean = emailResult(email) is Result.Valid } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/FormValidator.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/FormValidator.kt index 7a3c5696e..aa5ff5070 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/FormValidator.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/FormValidator.kt @@ -2,13 +2,22 @@ package edu.stanford.spezi.module.account.register import androidx.core.util.PatternsCompat -internal open class FormValidator { - fun isValidEmail(email: String): Boolean { - return PatternsCompat.EMAIL_ADDRESS.matcher(email).matches() +internal abstract class FormValidator { + + fun isValidEmail(email: String): Result { + return if (PatternsCompat.EMAIL_ADDRESS.matcher(email).matches()) { + Result.Valid + } else { + Result.Invalid("Invalid email") + } } - fun isValidPassword(password: String): Boolean { - return password.length >= MIN_PASSWORD_LENGTH + fun isValidPassword(password: String): Result { + return if (password.length >= MIN_PASSWORD_LENGTH) { + Result.Valid + } else { + Result.Invalid("Password must be at least $MIN_PASSWORD_LENGTH characters") + } } internal companion object { @@ -20,5 +29,8 @@ internal open class FormValidator { data class Invalid(val message: String) : Result fun errorMessageOrNull() = if (this is Invalid) message else null + + val isValid: Boolean + get() = this is Valid } } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterFormValidator.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterFormValidator.kt index 92b48cba9..21085f49c 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterFormValidator.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterFormValidator.kt @@ -3,19 +3,7 @@ package edu.stanford.spezi.module.account.register import java.time.LocalDate import javax.inject.Inject -internal class RegisterFormValidator @Inject internal constructor() : FormValidator() { - - fun emailResult(email: String): Result = if (isValidEmail(email)) { - Result.Valid - } else { - Result.Invalid("Invalid email") - } - - fun passwordResult(password: String): Result = if (isValidPassword(password)) { - Result.Valid - } else { - Result.Invalid("Password must be at least $MIN_PASSWORD_LENGTH characters") - } +internal class RegisterFormValidator @Inject constructor() : FormValidator() { fun firstnameResult(firstName: String): Result = if (firstName.isNotEmpty()) Result.Valid else Result.Invalid("First name cannot be empty") @@ -45,16 +33,24 @@ internal class RegisterFormValidator @Inject internal constructor() : FormValida fun isFormValid(uiState: RegisterUiState): Boolean { val passwordConditionSatisfied = { if (uiState.isGoogleSignUp) { - passwordResult(uiState.password.value) is Result.Valid + isValidPassword(uiState.password.value).isValid } else { true } } - return emailResult(uiState.email.value) is Result.Valid && - firstnameResult(uiState.firstName.value) is Result.Valid && - lastnameResult(uiState.lastName.value) is Result.Valid && - isGenderValid(uiState.selectedGender.value) is Result.Valid && - birthdayResult(uiState.dateOfBirth) is Result.Valid && + return isValidEmail(uiState.email.value).isValid && + firstnameResult(uiState.firstName.value).isValid && + lastnameResult(uiState.lastName.value).isValid && + isGenderValid(uiState.selectedGender.value).isValid && + birthdayResult(uiState.dateOfBirth).isValid && passwordConditionSatisfied() } + + fun isRegisterButtonEnabled(uiState: RegisterUiState): Boolean { + return uiState.email.value.isNotEmpty() && + uiState.firstName.value.isNotEmpty() && + uiState.lastName.value.isNotEmpty() && + uiState.selectedGender.value.isNotEmpty() && + uiState.dateOfBirth != null + } } From 23835232d69a6fa456086a2d365ac9aa8eb1d24a Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sat, 22 Jun 2024 21:09:07 +0200 Subject: [PATCH 17/28] introduced namespace and extracted button enabled logic Signed-off-by: Basler182 --- .../spezi/module/account/login/LoginScreen.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginScreen.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginScreen.kt index 7f01082d0..b521bffab 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginScreen.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginScreen.kt @@ -29,7 +29,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource @@ -50,6 +49,7 @@ import edu.stanford.spezi.module.account.login.components.SignInWithGoogleButton import edu.stanford.spezi.module.account.login.components.TextDivider import edu.stanford.spezi.module.account.register.FieldState import edu.stanford.spezi.module.account.register.IconLeadingContent +import edu.stanford.spezi.core.design.R as DesignR @Composable fun LoginScreen( @@ -139,13 +139,13 @@ You may login to your existing account or create a new one if you don't have one }), trailingIcon = { IconButton(onClick = { onAction(Action.TogglePasswordVisibility) }) { - val icon: Painter = if (uiState.passwordVisibility) { - painterResource(id = edu.stanford.spezi.core.design.R.drawable.ic_visibility) + val iconId = if (uiState.passwordVisibility) { + DesignR.drawable.ic_visibility } else { - painterResource(id = edu.stanford.spezi.core.design.R.drawable.ic_visibility_off) + DesignR.drawable.ic_visibility_off } Icon( - painter = icon, + painter = painterResource(id = iconId), contentDescription = if (uiState.passwordVisibility) "Hide password" else "Show password" ) } @@ -165,7 +165,7 @@ You may login to your existing account or create a new one if you don't have one onAction(Action.PasswordSignInOrSignUp) }, modifier = Modifier.fillMaxWidth(), - enabled = uiState.email.value.isNotEmpty() && uiState.password.value.isNotEmpty() + enabled = uiState.isPasswordSignInEnabled ) { Text( text = if (uiState.isAlreadyRegistered) "Login" else "Register" From 0d8a5943d10cee5fba5a8290c1d047a4ca04ae09 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sat, 22 Jun 2024 21:10:04 +0200 Subject: [PATCH 18/28] use run catching in password login Signed-off-by: Basler182 --- .../cred/manager/CredentialLoginManagerAuth.kt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/cred/manager/CredentialLoginManagerAuth.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/cred/manager/CredentialLoginManagerAuth.kt index dd2d06f64..7855eed52 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/cred/manager/CredentialLoginManagerAuth.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/cred/manager/CredentialLoginManagerAuth.kt @@ -1,7 +1,6 @@ package edu.stanford.spezi.module.account.cred.manager import android.content.Context -import androidx.credentials.CreatePasswordRequest import androidx.credentials.CredentialManager import androidx.credentials.CustomCredential import androidx.credentials.GetCredentialRequest @@ -33,13 +32,10 @@ class CredentialLoginManagerAuth @Inject constructor( suspend fun handlePasswordSignIn( username: String, password: String, - ): Boolean { - val createPasswordRequest = CreatePasswordRequest(id = username, password = password) - val createCredential = credentialManager.createCredential(context, createPasswordRequest) - if (createCredential.type == PasswordCredential.TYPE_PASSWORD_CREDENTIAL) { - return firebaseAuthManager.signInWithEmailAndPassword(username, password) + ): Result { + return runCatching { + firebaseAuthManager.signInWithEmailAndPassword(username, password) } - return false } private suspend fun getCredential(filterByAuthorizedAccounts: Boolean): GoogleIdTokenCredential? { @@ -67,7 +63,8 @@ class CredentialLoginManagerAuth @Inject constructor( when (val credential = response.credential) { is CustomCredential -> { if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { - val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data) + val googleIdTokenCredential = + GoogleIdTokenCredential.createFrom(credential.data) return googleIdTokenCredential } if (credential.type == PasswordCredential.TYPE_PASSWORD_CREDENTIAL) { From bc02edf00420157bc456638b4c5410f83a799c9a Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sat, 22 Jun 2024 21:10:59 +0200 Subject: [PATCH 19/28] extracted date time formatter Signed-off-by: Basler182 --- .../account/register/RegisterViewModel.kt | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterViewModel.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterViewModel.kt index 0e5f9cb9f..1ec0f4207 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterViewModel.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterViewModel.kt @@ -29,6 +29,13 @@ class RegisterViewModel @Inject internal constructor( private var googleCredential: String? = null + private val birthdayDateFormatter by lazy { + DateTimeFormatter.ofPattern( + "dd MMMM yyyy", + Locale.getDefault() + ) + } + fun onAction(action: Action) { _uiState.update { when (action) { @@ -41,21 +48,22 @@ class RegisterViewModel @Inject internal constructor( TextFieldType.LAST_NAME -> it.copy(lastName = newValue) TextFieldType.GENDER -> it.copy(selectedGender = newValue) } - updatedUiState.copy(isFormValid = validator.isFormValid(updatedUiState)) + updatedUiState.copy( + isFormValid = validator.isFormValid(updatedUiState), + isRegisterButtonEnabled = validator.isRegisterButtonEnabled(updatedUiState) + ) } is Action.DateFieldUpdate -> { - val deviceLocale: Locale = Locale.getDefault() val updatedUiState = it.copy( dateOfBirth = action.newValue, - formattedDateOfBirth = action.newValue.format( - DateTimeFormatter.ofPattern( - "dd MMMM yyyy", - deviceLocale - ) - ) + formattedDateOfBirth = birthdayDateFormatter.format(action.newValue), + isDatePickerDialogOpen = false, + ) + updatedUiState.copy( + isFormValid = validator.isFormValid(updatedUiState), + isRegisterButtonEnabled = validator.isRegisterButtonEnabled(updatedUiState) ) - updatedUiState.copy(isFormValid = validator.isFormValid(updatedUiState)) } is Action.DropdownMenuExpandedUpdate -> { @@ -63,8 +71,7 @@ class RegisterViewModel @Inject internal constructor( } is Action.OnRegisterPressed -> { - _uiState.value = onRegisteredPressed() - it + onRegisteredPressed() } is Action.SetIsGoogleSignUp -> { @@ -146,10 +153,10 @@ class RegisterViewModel @Inject internal constructor( } else { uiState.copy( email = uiState.email.copy( - error = validator.emailResult(uiState.email.value).errorMessageOrNull() + error = validator.isValidEmail(uiState.email.value).errorMessageOrNull() ), password = uiState.password.copy( - error = validator.passwordResult(uiState.password.value).errorMessageOrNull() + error = validator.isValidPassword(uiState.password.value).errorMessageOrNull() ), firstName = uiState.firstName.copy( error = validator.firstnameResult(uiState.firstName.value).errorMessageOrNull() From 2505669202400df683dcad24fca9832e99338c5f Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sat, 22 Jun 2024 21:11:21 +0200 Subject: [PATCH 20/28] extracted register button enabled logic from screen Signed-off-by: Basler182 --- .../stanford/spezi/module/account/register/RegisterUiState.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterUiState.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterUiState.kt index 7518fc111..c2c23d6f6 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterUiState.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterUiState.kt @@ -17,6 +17,7 @@ data class RegisterUiState( val genderOptions: List = listOf("Male", "Female", "Other"), val isGoogleSignUp: Boolean = false, val isPasswordVisible: Boolean = false, + val isRegisterButtonEnabled: Boolean = false, ) data class FieldState( From 4bc3c4802a2e909080097e6a4c3ec77196e461a1 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sat, 22 Jun 2024 21:11:35 +0200 Subject: [PATCH 21/28] extracted register button enabled logic from screen Signed-off-by: Basler182 --- .../spezi/module/account/register/RegisterScreen.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt index 575a9d404..31ad3b02f 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterScreen.kt @@ -285,7 +285,6 @@ fun RegisterScreen( DatePickerDialog( onDateSelected = { date -> onAction(Action.DateFieldUpdate(date)) - onAction(Action.SetIsDatePickerOpen(false)) }, onDismiss = { onAction(Action.SetIsDatePickerOpen(false)) @@ -299,11 +298,7 @@ fun RegisterScreen( onAction(Action.OnRegisterPressed) }, modifier = Modifier.fillMaxWidth(), - enabled = uiState.email.value.isNotEmpty() && - uiState.firstName.value.isNotEmpty() && - uiState.lastName.value.isNotEmpty() && - uiState.selectedGender.value.isNotEmpty() && - uiState.dateOfBirth != null + enabled = uiState.isRegisterButtonEnabled ) { Text("Signup") } From 1c39df8b8efd0caef796ae8a64a788737576decb Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sat, 22 Jun 2024 21:11:49 +0200 Subject: [PATCH 22/28] added missing spacing Signed-off-by: Basler182 --- .../spezi/module/account/register/IconLeadingContent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/IconLeadingContent.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/IconLeadingContent.kt index f48f7f222..6caa8cd21 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/IconLeadingContent.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/IconLeadingContent.kt @@ -42,7 +42,7 @@ internal fun IconLeadingContent( Spacer(modifier = Modifier.width(Spacings.medium)) } else { // Reserve the space for the icon - Spacer(modifier = Modifier.width(Sizes.Icon.small + Spacings.small)) + Spacer(modifier = Modifier.width(Sizes.Icon.small + Spacings.small + Spacings.small)) } content() } From b1e0f2c00e24309d2a1006c998873f77e86fb750 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sat, 22 Jun 2024 21:12:42 +0200 Subject: [PATCH 23/28] Removed enclosing update Signed-off-by: Basler182 --- .../module/account/login/LoginViewModel.kt | 81 ++++++++++--------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginViewModel.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginViewModel.kt index 47367924d..5e5a1f5a4 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginViewModel.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginViewModel.kt @@ -27,44 +27,46 @@ internal class LoginViewModel @Inject constructor( private val _uiState = MutableStateFlow(UiState()) val uiState = _uiState.asStateFlow() + private var hasAttemptedSubmit: Boolean = false + fun onAction(action: Action) { - _uiState.update { - when (action) { - is Action.TextFieldUpdate -> { + when (action) { + is Action.TextFieldUpdate -> { + _uiState.update { updateTextField(action, it) } + } - is Action.TogglePasswordVisibility -> { + is Action.TogglePasswordVisibility -> { + _uiState.update { it.copy(passwordVisibility = !it.passwordVisibility) } + } - is Action.NavigateToRegister -> { - navigateToRegister() - it - } + is Action.NavigateToRegister -> { + navigateToRegister() + } - is Action.GoogleSignInOrSignUp -> { - if (uiState.value.isAlreadyRegistered) { - googleSignIn() - } else { - googleSignUp() - } - it + is Action.GoogleSignInOrSignUp -> { + if (uiState.value.isAlreadyRegistered) { + googleSignIn() + } else { + googleSignUp() } + } - is Action.SetIsAlreadyRegistered -> { + is Action.SetIsAlreadyRegistered -> { + _uiState.update { handleIsAlreadyRegistered(action, it) } + } - is Action.ForgotPassword -> { - forgotPassword() - it - } + is Action.ForgotPassword -> { + forgotPassword() + } - Action.PasswordSignInOrSignUp -> { - handleLoginOrRegister() - it - } + Action.PasswordSignInOrSignUp -> { + handleLoginOrRegister() } } } @@ -79,17 +81,16 @@ internal class LoginViewModel @Inject constructor( navigateToRegister() } + hasAttemptedSubmit = true _uiState.update { it.copy( - hasAttemptedSubmit = true, - email = FieldState( - uiState.email.value, - error = validator.emailResult(uiState.email.value).errorMessageOrNull() + email = it.email.copy( + error = validator.isValidEmail(email = uiState.email.value).errorMessageOrNull() + ), + password = it.password.copy( + error = validator.isValidPassword(password = uiState.password.value) + .errorMessageOrNull() ), - password = FieldState( - uiState.password.value, - error = validator.passwordResult(uiState.password.value).errorMessageOrNull() - ) ) } } @@ -124,26 +125,28 @@ internal class LoginViewModel @Inject constructor( ): UiState { val newValue = FieldState(action.newValue) val result = when (action.type) { - TextFieldType.PASSWORD -> validator.passwordResult(action.newValue) - TextFieldType.EMAIL -> validator.emailResult(action.newValue) + TextFieldType.PASSWORD -> validator.isValidPassword(action.newValue) + TextFieldType.EMAIL -> validator.isValidEmail(action.newValue) } val error = - if (uiState.hasAttemptedSubmit && result is FormValidator.Result.Invalid) result.errorMessageOrNull() else null + if (hasAttemptedSubmit && result is FormValidator.Result.Invalid) result.errorMessageOrNull() else null return when (action.type) { TextFieldType.PASSWORD -> uiState.copy( password = newValue.copy(error = error), - isFormValid = validator.isFormValid(uiState) + isFormValid = validator.isFormValid(uiState), + isPasswordSignInEnabled = uiState.email.value.isNotEmpty() && newValue.value.isNotEmpty() ) TextFieldType.EMAIL -> uiState.copy( email = newValue.copy(error = error), - isFormValid = validator.isFormValid(uiState) + isFormValid = validator.isFormValid(uiState), + isPasswordSignInEnabled = newValue.value.isNotEmpty() && uiState.password.value.isNotEmpty() ) } } private fun forgotPassword() { - if (validator.isEmailValid(uiState.value.email.value)) { + if (validator.isValidEmail(uiState.value.email.value) is FormValidator.Result.Valid) { sendForgotPasswordEmail(uiState.value.email.value) } else { messageNotifier.notify("Please enter a valid email") @@ -176,7 +179,7 @@ internal class LoginViewModel @Inject constructor( _uiState.value.email.value, _uiState.value.password.value, ) - if (result) { + if (result.isSuccess) { accountEvents.emit(event = AccountEvents.Event.SignInSuccess) } else { accountEvents.emit(event = AccountEvents.Event.SignInFailure) From 90c0a0daaec15bbf5df2eae231e8944754f0aff5 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sat, 22 Jun 2024 21:13:51 +0200 Subject: [PATCH 24/28] updated tests Signed-off-by: Basler182 --- .../account/login/LoginFormValidatorTest.kt | 25 +++++++++---------- .../account/login/LoginViewModelTest.kt | 9 ++++--- .../register/RegisterFormValidatorTest.kt | 24 +++++++++--------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/modules/account/src/test/java/edu/stanford/spezi/module/account/login/LoginFormValidatorTest.kt b/modules/account/src/test/java/edu/stanford/spezi/module/account/login/LoginFormValidatorTest.kt index b6c9862c8..8636c35d9 100644 --- a/modules/account/src/test/java/edu/stanford/spezi/module/account/login/LoginFormValidatorTest.kt +++ b/modules/account/src/test/java/edu/stanford/spezi/module/account/login/LoginFormValidatorTest.kt @@ -2,7 +2,6 @@ package edu.stanford.spezi.module.account.login import com.google.common.truth.Truth.assertThat import edu.stanford.spezi.module.account.register.FieldState -import edu.stanford.spezi.module.account.register.FormValidator import org.junit.Test class LoginFormValidatorTest { @@ -10,51 +9,51 @@ class LoginFormValidatorTest { private val loginFormValidator = LoginFormValidator() @Test - fun `given valid email when emailResult is called then return Valid`() { + fun `given valid email when isValidEmail is called then return Valid`() { // Given val validEmail = "test@test.com" // When - val result = loginFormValidator.emailResult(validEmail) + val result = loginFormValidator.isValidEmail(validEmail) // Then - assertThat(result).isEqualTo(FormValidator.Result.Valid) + assertThat(result.isValid).isTrue() } @Test - fun `given invalid email when emailResult is called then return Invalid`() { + fun `given invalid email when isValidEmail is called then return Invalid`() { // Given val invalidEmail = "invalidEmail" // When - val result = loginFormValidator.emailResult(invalidEmail) + val result = loginFormValidator.isValidEmail(invalidEmail) // Then - assertThat(result).isInstanceOf(FormValidator.Result.Invalid::class.java) + assertThat(result.isValid).isFalse() } @Test - fun `given valid password when passwordResult is called then return Valid`() { + fun `given valid password when isValidPassword is called then return Valid`() { // Given val validPassword = "password123" // When - val result = loginFormValidator.passwordResult(validPassword) + val result = loginFormValidator.isValidPassword(validPassword) // Then - assertThat(result).isEqualTo(FormValidator.Result.Valid) + assertThat(result.isValid).isTrue() } @Test - fun `given invalid password when passwordResult is called then return Invalid`() { + fun `given invalid password when isValidPassword is called then return Invalid`() { // Given val invalidPassword = "pass" // When - val result = loginFormValidator.passwordResult(invalidPassword) + val result = loginFormValidator.isValidPassword(invalidPassword) // Then - assertThat(result).isInstanceOf(FormValidator.Result.Invalid::class.java) + assertThat(result.isValid).isFalse() } @Test diff --git a/modules/account/src/test/java/edu/stanford/spezi/module/account/login/LoginViewModelTest.kt b/modules/account/src/test/java/edu/stanford/spezi/module/account/login/LoginViewModelTest.kt index af5c11983..4adb8dcea 100644 --- a/modules/account/src/test/java/edu/stanford/spezi/module/account/login/LoginViewModelTest.kt +++ b/modules/account/src/test/java/edu/stanford/spezi/module/account/login/LoginViewModelTest.kt @@ -2,6 +2,7 @@ package edu.stanford.spezi.module.account.login import com.google.common.truth.Truth.assertThat import edu.stanford.spezi.core.navigation.Navigator +import edu.stanford.spezi.core.testing.CoroutineTestRule import edu.stanford.spezi.core.testing.runTestUnconfined import edu.stanford.spezi.core.utils.MessageNotifier import edu.stanford.spezi.module.account.AccountEvents @@ -13,10 +14,8 @@ import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.verify -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.setMain import org.junit.Before +import org.junit.Rule import org.junit.Test class LoginViewModelTest { @@ -28,6 +27,9 @@ class LoginViewModelTest { private val validator: LoginFormValidator = LoginFormValidator() private val navigator: Navigator = mockk(relaxed = true) + @get:Rule + val coroutineTestRule = CoroutineTestRule() + @Before fun setUp() { loginViewModel = LoginViewModel( @@ -38,7 +40,6 @@ class LoginViewModelTest { validator = validator ) every { navigator.navigateTo(any()) } just Runs - Dispatchers.setMain(UnconfinedTestDispatcher()) } @Test diff --git a/modules/account/src/test/java/edu/stanford/spezi/module/account/register/RegisterFormValidatorTest.kt b/modules/account/src/test/java/edu/stanford/spezi/module/account/register/RegisterFormValidatorTest.kt index 7ca466370..bea11db9d 100644 --- a/modules/account/src/test/java/edu/stanford/spezi/module/account/register/RegisterFormValidatorTest.kt +++ b/modules/account/src/test/java/edu/stanford/spezi/module/account/register/RegisterFormValidatorTest.kt @@ -9,27 +9,27 @@ class RegisterFormValidatorTest { private val registerFormValidator = RegisterFormValidator() @Test - fun `given valid password when passwordResult is called then return Valid`() { + fun `given valid password when isValidPassword is called then return Valid`() { // Given val validPassword = "password123" // When - val result = registerFormValidator.passwordResult(validPassword) + val result = registerFormValidator.isValidPassword(validPassword) // Then - assertThat(result).isEqualTo(FormValidator.Result.Valid) + assertThat(result.isValid).isTrue() } @Test - fun `given invalid password when passwordResult is called then return Invalid`() { + fun `given invalid password when isValidPassword is called then return Invalid`() { // Given val invalidPassword = "pass" // When - val result = registerFormValidator.passwordResult(invalidPassword) + val result = registerFormValidator.isValidPassword(invalidPassword) // Then - assertThat(result).isInstanceOf(FormValidator.Result.Invalid::class.java) + assertThat(result.isValid).isFalse() } @Test @@ -41,7 +41,7 @@ class RegisterFormValidatorTest { val result = registerFormValidator.firstnameResult(validFirstName) // Then - assertThat(result).isEqualTo(FormValidator.Result.Valid) + assertThat(result.isValid).isTrue() } @Test @@ -53,7 +53,7 @@ class RegisterFormValidatorTest { val result = registerFormValidator.firstnameResult(invalidFirstName) // Then - assertThat(result).isInstanceOf(FormValidator.Result.Invalid::class.java) + assertThat(result.isValid).isFalse() } @Test @@ -65,7 +65,7 @@ class RegisterFormValidatorTest { val result = registerFormValidator.lastnameResult(validLastName) // Then - assertThat(result).isEqualTo(FormValidator.Result.Valid) + assertThat(result.isValid).isTrue() } @Test @@ -77,7 +77,7 @@ class RegisterFormValidatorTest { val result = registerFormValidator.lastnameResult(invalidLastName) // Then - assertThat(result).isInstanceOf(FormValidator.Result.Invalid::class.java) + assertThat(result.isValid).isFalse() } @Test @@ -89,7 +89,7 @@ class RegisterFormValidatorTest { val result = registerFormValidator.birthdayResult(validDateOfBirth) // Then - assertThat(result).isEqualTo(FormValidator.Result.Valid) + assertThat(result.isValid).isTrue() } @Test @@ -101,6 +101,6 @@ class RegisterFormValidatorTest { val result = registerFormValidator.birthdayResult(invalidDateOfBirth) // Then - assertThat(result).isInstanceOf(FormValidator.Result.Invalid::class.java) + assertThat(result.isValid).isFalse() } } From 4531c13a997d2e3c38ed908ffd955df999a89cc0 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sat, 22 Jun 2024 21:14:18 +0200 Subject: [PATCH 25/28] added password sign in enabled button Signed-off-by: Basler182 --- .../kotlin/edu/stanford/spezi/module/account/login/UiState.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/UiState.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/UiState.kt index 0a18d1323..cc873e87c 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/UiState.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/UiState.kt @@ -10,7 +10,7 @@ data class UiState( val showFilterByAuthorizedAccounts: Boolean = true, val isFormValid: Boolean = false, val isAlreadyRegistered: Boolean = false, - val hasAttemptedSubmit: Boolean = false, + val isPasswordSignInEnabled: Boolean = false, ) enum class TextFieldType { From f33201e48db0580bb6af2d70d244b1bc5012cec8 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sat, 22 Jun 2024 21:58:09 +0200 Subject: [PATCH 26/28] updated usage of .isValid Signed-off-by: Basler182 --- .../stanford/spezi/module/account/login/LoginViewModel.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginViewModel.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginViewModel.kt index 5e5a1f5a4..b524b0f25 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginViewModel.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/login/LoginViewModel.kt @@ -9,7 +9,6 @@ import edu.stanford.spezi.module.account.AccountEvents import edu.stanford.spezi.module.account.AccountNavigationEvent import edu.stanford.spezi.module.account.cred.manager.CredentialLoginManagerAuth import edu.stanford.spezi.module.account.register.FieldState -import edu.stanford.spezi.module.account.register.FormValidator import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -129,7 +128,7 @@ internal class LoginViewModel @Inject constructor( TextFieldType.EMAIL -> validator.isValidEmail(action.newValue) } val error = - if (hasAttemptedSubmit && result is FormValidator.Result.Invalid) result.errorMessageOrNull() else null + if (hasAttemptedSubmit && result.isValid.not()) result.errorMessageOrNull() else null return when (action.type) { TextFieldType.PASSWORD -> uiState.copy( password = newValue.copy(error = error), @@ -146,7 +145,7 @@ internal class LoginViewModel @Inject constructor( } private fun forgotPassword() { - if (validator.isValidEmail(uiState.value.email.value) is FormValidator.Result.Valid) { + if (validator.isValidEmail(uiState.value.email.value).isValid) { sendForgotPasswordEmail(uiState.value.email.value) } else { messageNotifier.notify("Please enter a valid email") From 175955c27552adda30886c9334b64684bc7c6810 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sat, 22 Jun 2024 21:59:04 +0200 Subject: [PATCH 27/28] changed property method order Signed-off-by: Basler182 --- .../stanford/spezi/module/account/register/FormValidator.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/FormValidator.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/FormValidator.kt index aa5ff5070..172062abf 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/FormValidator.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/FormValidator.kt @@ -28,9 +28,9 @@ internal abstract class FormValidator { data object Valid : Result data class Invalid(val message: String) : Result - fun errorMessageOrNull() = if (this is Invalid) message else null - val isValid: Boolean get() = this is Valid + + fun errorMessageOrNull() = if (this is Invalid) message else null } } From 0af524781b92fe69574cdabdef88e314bad194b3 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sat, 22 Jun 2024 22:01:37 +0200 Subject: [PATCH 28/28] refactored register button enabled form validation into view model Signed-off-by: Basler182 --- .../module/account/register/RegisterFormValidator.kt | 8 -------- .../module/account/register/RegisterViewModel.kt | 12 ++++++++++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterFormValidator.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterFormValidator.kt index 21085f49c..336af2574 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterFormValidator.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterFormValidator.kt @@ -45,12 +45,4 @@ internal class RegisterFormValidator @Inject constructor() : FormValidator() { birthdayResult(uiState.dateOfBirth).isValid && passwordConditionSatisfied() } - - fun isRegisterButtonEnabled(uiState: RegisterUiState): Boolean { - return uiState.email.value.isNotEmpty() && - uiState.firstName.value.isNotEmpty() && - uiState.lastName.value.isNotEmpty() && - uiState.selectedGender.value.isNotEmpty() && - uiState.dateOfBirth != null - } } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterViewModel.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterViewModel.kt index 1ec0f4207..d6b9120d7 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterViewModel.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/register/RegisterViewModel.kt @@ -50,7 +50,7 @@ class RegisterViewModel @Inject internal constructor( } updatedUiState.copy( isFormValid = validator.isFormValid(updatedUiState), - isRegisterButtonEnabled = validator.isRegisterButtonEnabled(updatedUiState) + isRegisterButtonEnabled = isRegisterButtonEnabled(updatedUiState) ) } @@ -62,7 +62,7 @@ class RegisterViewModel @Inject internal constructor( ) updatedUiState.copy( isFormValid = validator.isFormValid(updatedUiState), - isRegisterButtonEnabled = validator.isRegisterButtonEnabled(updatedUiState) + isRegisterButtonEnabled = isRegisterButtonEnabled(updatedUiState) ) } @@ -174,4 +174,12 @@ class RegisterViewModel @Inject internal constructor( ) } } + + private fun isRegisterButtonEnabled(uiState: RegisterUiState): Boolean { + return uiState.email.value.isNotEmpty() && + uiState.firstName.value.isNotEmpty() && + uiState.lastName.value.isNotEmpty() && + uiState.selectedGender.value.isNotEmpty() && + uiState.dateOfBirth != null + } }