Rework inputs to registration screen component

This commit is contained in:
Z. Charles Dziura 2025-03-17 20:58:09 -04:00
parent 2dbf6d61d0
commit 878ff48d95
10 changed files with 367 additions and 201 deletions

View file

@ -13,6 +13,8 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.lifecycleScope
@ -26,7 +28,9 @@ import dagger.hilt.android.AndroidEntryPoint
import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.confirm.ConfirmationData
import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.confirm.ConfirmationScreen
import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login.LoginScreen
import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login.LoginScreenViewModel
import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register.RegistrationScreen
import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register.RegistrationScreenViewModel
import ing.bikeshedengineer.debtpirate.app.screen.home.presentation.overview.OverviewScreen
import ing.bikeshedengineer.debtpirate.domain.usecase.GetStoredTokensUseCase
import ing.bikeshedengineer.debtpirate.navigation.Destination
@ -96,8 +100,42 @@ class MainActivity : ComponentActivity() {
navigation<Destination.AuthGraph>(
startDestination = Destination.AuthLogin
) {
composable<Destination.AuthLogin> { LoginScreen() }
composable<Destination.AuthRegistration> { RegistrationScreen() }
composable<Destination.AuthLogin> {
val viewModel = hiltViewModel<LoginScreenViewModel>()
LoginScreen(
emailAddress = viewModel.emailAddress.collectAsState(""),
isEmailAddressValid = viewModel.isEmailAddressValid.collectAsState(true),
password = viewModel.password.collectAsState(""),
isPasswordValid = viewModel.isPasswordValid.collectAsState(true),
toastMessages = viewModel.toastMessages.collectAsState(""),
onAction = viewModel::onAction,
handleCredentialManagerSignIn = viewModel::handleCredentialManagerSignIn,
onRegisterButtonClick = viewModel::onRegisterButtonClick
)
}
composable<Destination.AuthRegistration> {
val viewModel = hiltViewModel<RegistrationScreenViewModel>()
RegistrationScreen(
emailAddress = viewModel.emailAddress.collectAsState(""),
emailAddressError = viewModel.emailAddressError.collectAsState(""),
isEmailAddressValid = viewModel.isEmailAddressValid.collectAsState(true),
name = viewModel.name.collectAsState(""),
nameError = viewModel.nameError.collectAsState(""),
isNameValid = viewModel.isNameValid.collectAsState(true),
password = viewModel.password.collectAsState(""),
passwordError = viewModel.passwordError.collectAsState(""),
isPasswordValid = viewModel.isPasswordValid.collectAsState(true),
confirmPassword = viewModel.confirmPassword.collectAsState(""),
confirmPasswordError = viewModel.confirmPasswordError.collectAsState(""),
isConfirmPasswordValid = viewModel.isConfirmPasswordValid.collectAsState(true),
onAction = viewModel::onAction,
onRegistrationMessage = viewModel.onRegistrationComplete.collectAsState(null),
navigateToConfirmationScreen = viewModel::navigateToConfirmationScreen,
onNavigateUp = viewModel::navigateUp
)
}
composable<Destination.AuthRegistrationConfirmation> {
val (emailAddress) = it.toRoute<Destination.AuthRegistrationConfirmation>()
ConfirmationScreen(

View file

@ -3,7 +3,7 @@ package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login
import android.annotation.SuppressLint
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -31,10 +31,8 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
@ -44,6 +42,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
import androidx.credentials.GetPasswordOption
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.NoCredentialException
@ -52,9 +51,16 @@ import androidx.hilt.navigation.compose.hiltViewModel
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun LoginScreen(
viewModel: LoginScreenViewModel = hiltViewModel<LoginScreenViewModel>()
emailAddress: State<String>,
isEmailAddressValid: State<Boolean>,
password: State<String>,
isPasswordValid: State<Boolean>,
toastMessages: State<String>,
onAction: (LoginScreenStateAction) -> Unit,
handleCredentialManagerSignIn: (GetCredentialResponse) -> Unit,
onRegisterButtonClick: () -> Unit,
) {
val context = LocalContext.current as ComponentActivity
val context = LocalActivity.current!!
val credentialManager = CredentialManager.create(context)
LaunchedEffect(Unit) {
try {
@ -64,7 +70,7 @@ fun LoginScreen(
)
)
viewModel.handleCredentialManagerSignIn(result)
handleCredentialManagerSignIn(result)
} catch (err: GetCredentialException) {
when (err) {
is NoCredentialException -> {
@ -79,7 +85,6 @@ fun LoginScreen(
}
}
val toastMessages = viewModel.toastMessages.collectAsState("")
LaunchedEffect(toastMessages.value) {
val message = toastMessages.value
@ -103,37 +108,28 @@ fun LoginScreen(
.weight(3f)
.padding(16.dp)
) {
val emailAddress = viewModel.emailAddress.collectAsState()
val isValidEmailAddress = viewModel.isValidEmailAddress.collectAsState(true)
val password = viewModel.password.collectAsState()
val isValidPassword = viewModel.isValidPassword.collectAsState(true)
LoginComponent(
emailAddress,
onUpdateEmailAddress = {
viewModel.onAction(
onAction(
LoginScreenStateAction.UpdateEmailAddress(
it
)
)
},
isValidEmailAddress,
isEmailAddressValid,
password,
onUpdatePassword = {
viewModel.onAction(
onAction(
LoginScreenStateAction.UpdatePassword(
it
)
)
},
isValidPassword,
isPasswordValid,
submitLoginRequest = {
viewModel.onAction(
LoginScreenStateAction.ValidateCredentials(
emailAddress.value,
password.value
)
onAction(
LoginScreenStateAction.ValidateCredentials
)
}
)
@ -147,7 +143,7 @@ fun LoginScreen(
)
)
RegisterButton(viewModel = viewModel)
RegisterButton(onRegisterButtonClick)
}
}
}
@ -258,9 +254,9 @@ private fun Separator(modifier: Modifier = Modifier) {
}
@Composable
private fun RegisterButton(viewModel: LoginScreenViewModel) {
private fun RegisterButton(onRegisterButtonClick: () -> Unit) {
OutlinedButton(
onClick = { viewModel.onRegisterButtonClick() },
onClick = { onRegisterButtonClick() },
modifier = Modifier
.fillMaxWidth()
) {

View file

@ -0,0 +1,10 @@
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login
data class LoginScreenState(
val emailAddress: String,
val password: String,
val isEmailAddressPristine: Boolean,
val isPasswordPristine: Boolean,
val isEmailAddressValid: Boolean,
val isPasswordValid: Boolean,
)

View file

@ -3,6 +3,6 @@ package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login
sealed interface LoginScreenStateAction {
data class UpdateEmailAddress(val emailAddress: String) : LoginScreenStateAction
data class UpdatePassword(val password: String) : LoginScreenStateAction
data class ValidateCredentials(val emailAddress: String, val password: String) : LoginScreenStateAction
data class SubmitLoginRequest(val emailAddress: String, val password: String) : LoginScreenStateAction
data object ValidateCredentials : LoginScreenStateAction
data object SubmitLoginRequest : LoginScreenStateAction
}

View file

@ -1,6 +1,7 @@
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login
import android.util.Log
import android.util.Patterns
import androidx.credentials.GetCredentialResponse
import androidx.credentials.PasswordCredential
import androidx.lifecycle.ViewModel
@ -17,9 +18,11 @@ import ing.bikeshedengineer.debtpirate.navigation.Navigator
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -30,19 +33,48 @@ class LoginScreenViewModel @Inject constructor(
private val validateLoginCredentials: ValidateLoginCredentialsUseCase,
private val updateStoreData: UpdateStoreDataUseCase
) : ViewModel() {
private val _isEmailAddressPristine = MutableStateFlow(true)
private val _emailAddress = MutableStateFlow("")
val emailAddress = _emailAddress.asStateFlow()
val isValidEmailAddress =
_isEmailAddressPristine.combine(_emailAddress.map { it.isNotBlank() }) { isPristine, isValid ->
isPristine || isValid
}
private val _state = MutableStateFlow(
LoginScreenState(
emailAddress = "",
password = "",
isEmailAddressPristine = true,
isPasswordPristine = true,
isEmailAddressValid = true,
isPasswordValid = true,
)
)
private val _isPasswordPristine = MutableStateFlow(true)
private val _password = MutableStateFlow("")
val password = _password.asStateFlow()
val isValidPassword = _isPasswordPristine.combine(_password.map { it.isNotBlank() }) { isPristine, isValid ->
isPristine || isValid
val emailAddress = _state.map { it.emailAddress }
val password = _state.map { it.password }
val isEmailAddressPristine = _state.map { it.isEmailAddressPristine }
val isPasswordPristine = _state.map { it.isPasswordPristine }
val isEmailAddressValid = _state.map { it.isEmailAddressValid }
val isPasswordValid = _state.map { it.isPasswordValid }
init {
_state.distinctUntilChangedBy { it.emailAddress }
.map { it.emailAddress.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(it.emailAddress).matches() }
.onEach { isEmailAddressValid ->
_state.update {
it.copy(
isEmailAddressPristine = false,
isEmailAddressValid = it.isEmailAddressPristine || isEmailAddressValid
)
}
}
.launchIn(viewModelScope)
_state.distinctUntilChangedBy { it.password }
.map { it.password.isNotBlank() }
.onEach { isPasswordValid ->
_state.update {
it.copy(
isPasswordPristine = false,
isPasswordValid = it.isPasswordPristine || isPasswordValid
)
}
}
.launchIn(viewModelScope)
}
private val _toastMessages = MutableSharedFlow<String>()
@ -51,37 +83,32 @@ class LoginScreenViewModel @Inject constructor(
fun onAction(action: LoginScreenStateAction) {
when (action) {
is LoginScreenStateAction.UpdateEmailAddress -> {
_emailAddress.value = action.emailAddress
_isEmailAddressPristine.value = false
_state.update { it.copy(emailAddress = action.emailAddress) }
}
is LoginScreenStateAction.UpdatePassword -> {
_password.value = action.password
_isPasswordPristine.value = false
_state.update { it.copy(password = action.password) }
}
is LoginScreenStateAction.ValidateCredentials -> {
val (emailAddress, password) = action
onValidateLoginCredentials(emailAddress, password)
onValidateLoginCredentials(_state.value.emailAddress, _state.value.password)
}
is LoginScreenStateAction.SubmitLoginRequest -> {
viewModelScope.launch {
val (emailAddress, password) = action
onSubmitLoginRequest(emailAddress, password)
onSubmitLoginRequest(_state.value.emailAddress, _state.value.password)
}
}
}
}
private fun onValidateLoginCredentials(emailAddress: String, password: String) {
_isEmailAddressPristine.value = false
_isPasswordPristine.value = false
_state.update { it.copy(isEmailAddressPristine = true, isPasswordPristine = true) }
val validationResult = validateLoginCredentials(emailAddress, password)
when (validationResult) {
is LoginCredentialsValidationResult.ValidCredentials -> {
onAction(LoginScreenStateAction.SubmitLoginRequest(emailAddress, password))
onAction(LoginScreenStateAction.SubmitLoginRequest)
}
else -> {
@ -131,7 +158,7 @@ class LoginScreenViewModel @Inject constructor(
val emailAddress = credentials.id
val password = credentials.password
onAction(LoginScreenStateAction.SubmitLoginRequest(emailAddress, password))
onAction(LoginScreenStateAction.SubmitLoginRequest)
}
else -> {

View file

@ -29,7 +29,6 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
@ -49,17 +48,32 @@ import androidx.compose.ui.unit.sp
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CredentialManager
import androidx.credentials.exceptions.CreateCredentialException
import androidx.hilt.navigation.compose.hiltViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable()
fun RegistrationScreen(
viewModel: RegistrationScreenViewModel = hiltViewModel<RegistrationScreenViewModel>()
emailAddress: State<String>,
emailAddressError: State<String>,
isEmailAddressValid: State<Boolean>,
name: State<String>,
nameError: State<String>,
isNameValid: State<Boolean>,
password: State<String>,
passwordError: State<String>,
isPasswordValid: State<Boolean>,
confirmPassword: State<String>,
confirmPasswordError: State<String>,
isConfirmPasswordValid: State<Boolean>,
onAction: (RegistrationScreenAction) -> Unit,
onRegistrationMessage: State<Pair<String, String>?>,
navigateToConfirmationScreen: (String) -> Unit,
onNavigateUp: () -> Unit,
) {
val localContext = LocalContext.current
val onRegistrationMessage = viewModel.onRegistrationComplete.collectAsState(null)
LaunchedEffect(onRegistrationMessage.value) {
val message = onRegistrationMessage.value
if (message != null) {
val (username, password) = message
val credentialManager = CredentialManager.create(localContext)
@ -72,13 +86,13 @@ fun RegistrationScreen(
// TODO: Display a toast...
}
viewModel.navigateToConfirmationScreen(username)
navigateToConfirmationScreen(username)
}
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
topBar = { RegistrationTopAppBar(onNavigateUp = viewModel::navigateUp) },
topBar = { RegistrationTopAppBar(onNavigateUp) },
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection)
@ -89,7 +103,19 @@ fun RegistrationScreen(
.padding(16.dp)
) {
RegistrationComponent(
viewModel,
emailAddress = emailAddress,
emailAddressError = emailAddressError,
isEmailAddressValid = isEmailAddressValid,
name = name,
nameError = nameError,
isNameValid = isNameValid,
password = password,
passwordError = passwordError,
isPasswordValid = isPasswordValid,
confirmPassword = confirmPassword,
confirmPasswordError = confirmPasswordError,
isConfirmPasswordValid = isConfirmPasswordValid,
onAction = onAction
)
}
}
@ -126,25 +152,21 @@ private fun RegistrationTopAppBar(onNavigateUp: () -> Unit, modifier: Modifier =
@Composable
private fun RegistrationComponent(
viewModel: RegistrationScreenViewModel,
emailAddress: State<String>,
emailAddressError: State<String>,
isEmailAddressValid: State<Boolean>,
name: State<String>,
nameError: State<String>,
isNameValid: State<Boolean>,
password: State<String>,
passwordError: State<String>,
isPasswordValid: State<Boolean>,
confirmPassword: State<String>,
confirmPasswordError: State<String>,
isConfirmPasswordValid: State<Boolean>,
onAction: (RegistrationScreenAction) -> Unit,
modifier: Modifier = Modifier
) {
val emailAddress = viewModel.emailAddress.collectAsState()
val emailAddressError = viewModel.emailAddressError.collectAsState()
val isInvalidEmailAddress = viewModel.isInvalidEmailAddress.collectAsState(false)
val name = viewModel.name.collectAsState()
val nameError = viewModel.nameError.collectAsState()
val isInvalidName = viewModel.isInvalidName.collectAsState(false)
val password = viewModel.password.collectAsState()
val passwordError = viewModel.passwordError.collectAsState()
val isInvalidPassword = viewModel.isInvalidPassword.collectAsState(false)
val confirmPassword = viewModel.confirmPassword.collectAsState()
val confirmPasswordError = viewModel.confirmPasswordError.collectAsState()
val isInvalidConfirmPassword = viewModel.isInvalidConfirmPassword.collectAsState(false)
Column(modifier = modifier.verticalScroll(rememberScrollState())) {
OutlinedTextField(
value = emailAddress.value,
@ -153,13 +175,13 @@ private fun RegistrationComponent(
supportingText = { Text(text = emailAddressError.value) },
leadingIcon = { Icon(Icons.Outlined.Mail, "Email") },
singleLine = true,
isError = isInvalidEmailAddress.value,
isError = !isEmailAddressValid.value,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
),
onValueChange = { viewModel.onAction(RegistrationScreenAction.UpdateEmailAddress(it)) },
onValueChange = { onAction(RegistrationScreenAction.UpdateEmailAddress(it)) },
modifier = Modifier.fillMaxWidth()
)
@ -170,13 +192,13 @@ private fun RegistrationComponent(
supportingText = { Text(text = nameError.value) },
leadingIcon = { Icon(Icons.Outlined.Person, "Name") },
singleLine = true,
isError = isInvalidName.value,
isError = !isNameValid.value,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Words,
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next
),
onValueChange = { viewModel.onAction(RegistrationScreenAction.UpdateName(it)) },
onValueChange = { onAction(RegistrationScreenAction.UpdateName(it)) },
modifier = Modifier
.fillMaxWidth()
.padding(PaddingValues(top = 4.dp))
@ -194,14 +216,14 @@ private fun RegistrationComponent(
)
},
singleLine = true,
isError = isInvalidPassword.value,
isError = !isPasswordValid.value,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next
),
onValueChange = { viewModel.onAction(RegistrationScreenAction.UpdatePassword(it)) },
onValueChange = { onAction(RegistrationScreenAction.UpdatePassword(it)) },
modifier = Modifier
.fillMaxWidth()
.padding(PaddingValues(top = 4.dp))
@ -219,22 +241,17 @@ private fun RegistrationComponent(
)
},
singleLine = true,
isError = isInvalidConfirmPassword.value,
isError = !isConfirmPasswordValid.value,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Send
),
onValueChange = { viewModel.onAction(RegistrationScreenAction.UpdateConfirmPassword(it)) },
onValueChange = { onAction(RegistrationScreenAction.UpdateConfirmPassword(it)) },
keyboardActions = KeyboardActions(
onSend = {
viewModel.registerNewAccount(
emailAddress.value,
name.value,
password.value,
confirmPassword.value
)
onAction(RegistrationScreenAction.RegisterNewUser)
}
),
modifier = Modifier
@ -266,31 +283,18 @@ private fun RegistrationComponent(
)
RegisterButton(
emailAddress,
name,
password,
confirmPassword,
registerNewAccount = viewModel::registerNewAccount
onAction = onAction
)
}
}
@Composable
private fun RegisterButton(
emailAddress: State<String>,
name: State<String>,
password: State<String>,
confirmPassword: State<String>,
registerNewAccount: (String, String, String, String) -> Unit
onAction: (RegistrationScreenAction) -> Unit
) {
Button(
onClick = {
registerNewAccount(
emailAddress.value,
name.value,
password.value,
confirmPassword.value
)
onAction(RegistrationScreenAction.RegisterNewUser)
},
modifier = Modifier
.fillMaxWidth()

View file

@ -6,10 +6,5 @@ sealed interface RegistrationScreenAction {
data class UpdatePassword(val password: String) : RegistrationScreenAction
data class UpdateConfirmPassword(val confirmPassword: String) : RegistrationScreenAction
data object ResetFields : RegistrationScreenAction
data class RegisterNewUser(
val emailAddress: String,
val name: String,
val password: String,
val confirmPassword: String
) : RegistrationScreenAction
data object RegisterNewUser : RegistrationScreenAction
}

View file

@ -0,0 +1,20 @@
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register
data class RegistrationScreenState(
val emailAddress: String,
val name: String,
val password: String,
val confirmPassword: String,
val isEmailAddressPristine: Boolean,
val isNamePristine: Boolean,
val isPasswordPristine: Boolean,
val isConfirmPasswordPristine: Boolean,
val emailAddressError: String,
val nameError: String,
val passwordError: String,
val confirmPasswordError: String,
val isEmailAddressValid: Boolean,
val isNameValid: Boolean,
val isPasswordValid: Boolean,
val isConfirmPasswordValid: Boolean
)

View file

@ -1,6 +1,6 @@
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register
import androidx.credentials.CreatePasswordRequest
import android.util.Patterns
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
@ -13,8 +13,11 @@ import ing.bikeshedengineer.debtpirate.navigation.Navigator
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -25,6 +28,144 @@ class RegistrationScreenViewModel @Inject constructor(
private val submitAccountRegistrationRequest: SubmitAccountRegistrationRequestUseCase,
private val updateStoreData: UpdateStoreDataUseCase,
) : ViewModel() {
private val _state = MutableStateFlow(
RegistrationScreenState(
emailAddress = "",
name = "",
password = "",
confirmPassword = "",
isEmailAddressPristine = true,
isNamePristine = true,
isPasswordPristine = true,
isConfirmPasswordPristine = true,
emailAddressError = "",
nameError = "",
passwordError = "",
confirmPasswordError = "",
isEmailAddressValid = false,
isNameValid = false,
isPasswordValid = false,
isConfirmPasswordValid = false
)
)
val emailAddress = _state.map { it.emailAddress }
val name = _state.map { it.name }
val password = _state.map { it.password }
val confirmPassword = _state.map { it.confirmPassword }
val isEmailAddressPristine = _state.map { it.isEmailAddressPristine }
val isNamePristine = _state.map { it.isNamePristine }
val isPasswordPristine = _state.map { it.isPasswordPristine }
val isConfirmPasswordPristine = _state.map { it.isConfirmPasswordPristine }
val emailAddressError = _state.map { it.emailAddressError }
val nameError = _state.map { it.nameError }
val passwordError = _state.map { it.passwordError }
val confirmPasswordError = _state.map { it.confirmPasswordError }
val isEmailAddressValid = _state.map { it.isEmailAddressValid }
val isNameValid = _state.map { it.isNameValid }
val isPasswordValid = _state.map { it.isPasswordValid }
val isConfirmPasswordValid = _state.map { it.isConfirmPasswordValid }
private val _onRegistrationComplete = MutableSharedFlow<Pair<String, String>?>()
val onRegistrationComplete = _onRegistrationComplete.asSharedFlow()
init {
_state.distinctUntilChangedBy { it.emailAddress }
.map { it.emailAddress.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(it.emailAddress).matches() }
.onEach { isEmailAddressValid ->
_state.update {
it.copy(
isEmailAddressPristine = false,
isEmailAddressValid = it.isEmailAddressPristine || isEmailAddressValid
)
}
}
.launchIn(viewModelScope)
_state.distinctUntilChangedBy { it.name }
.map { it.name.isNotBlank() }
.onEach { isNameValid ->
_state.update {
it.copy(
isNamePristine = false,
isNameValid = it.isNamePristine || isNameValid
)
}
}
.launchIn(viewModelScope)
_state.distinctUntilChangedBy { it.password }
.map { it.password.isNotBlank() }
.onEach { isPasswordValid ->
_state.update {
it.copy(
isPasswordPristine = false,
isPasswordValid = it.isPasswordPristine || isPasswordValid
)
}
}
.launchIn(viewModelScope)
_state.distinctUntilChangedBy { it.confirmPassword }
.map { it.confirmPassword.isNotBlank() }
.onEach { isConfirmPasswordValid ->
_state.update {
it.copy(
isConfirmPasswordPristine = false,
isConfirmPasswordValid = it.isConfirmPasswordPristine || isConfirmPasswordValid
)
}
}
.launchIn(viewModelScope)
}
fun onAction(action: RegistrationScreenAction) {
when (action) {
is RegistrationScreenAction.UpdateEmailAddress -> {
_state.update { it.copy(emailAddress = action.emailAddress) }
}
is RegistrationScreenAction.UpdateName -> {
_state.update { it.copy(name = action.name) }
}
is RegistrationScreenAction.UpdatePassword -> {
_state.update { it.copy(password = action.password) }
}
is RegistrationScreenAction.UpdateConfirmPassword -> {
_state.update { it.copy(confirmPassword = action.confirmPassword) }
}
is RegistrationScreenAction.ResetFields -> {
_state.update {
it.copy(
emailAddress = "",
isEmailAddressPristine = true,
name = "",
isNamePristine = true,
password = "",
isPasswordPristine = true,
confirmPassword = "",
isConfirmPasswordPristine = true
)
}
}
is RegistrationScreenAction.RegisterNewUser -> {
registerNewAccount(
_state.value.emailAddress,
_state.value.name,
_state.value.password,
_state.value.confirmPassword
)
}
}
}
fun navigateUp() {
viewModelScope.launch {
navigator.navigateUp()
@ -37,70 +178,6 @@ class RegistrationScreenViewModel @Inject constructor(
}
}
private val _emailAddress = MutableStateFlow("")
val emailAddress = _emailAddress.asStateFlow()
private val _emailAddressError = MutableStateFlow("")
val emailAddressError = _emailAddressError.asStateFlow()
val isInvalidEmailAddress = _emailAddressError.asStateFlow().map { it.isNotEmpty() }
private val _name = MutableStateFlow("")
val name = _name.asStateFlow()
private val _nameError = MutableStateFlow("")
val nameError = _nameError.asStateFlow()
val isInvalidName = _nameError.asStateFlow().map { it.isNotEmpty() }
private val _password = MutableStateFlow("")
val password = _password.asStateFlow()
private val _passwordError = MutableStateFlow("")
val passwordError = _passwordError.asStateFlow()
val isInvalidPassword = _passwordError.asStateFlow().map { it.isNotEmpty() }
private val _confirmPassword = MutableStateFlow("")
val confirmPassword = _confirmPassword.asStateFlow()
private val _confirmPasswordError = MutableStateFlow("")
val confirmPasswordError = _confirmPasswordError.asStateFlow()
val isInvalidConfirmPassword = _confirmPasswordError.asStateFlow().map { it.isNotEmpty() }
private val _onRegistrationComplete = MutableSharedFlow<Pair<String, String>>()
val onRegistrationComplete = _onRegistrationComplete.asSharedFlow()
fun onAction(action: RegistrationScreenAction) {
when (action) {
is RegistrationScreenAction.UpdateEmailAddress -> {
_emailAddress.value = action.emailAddress
}
is RegistrationScreenAction.UpdateName -> {
_name.value = action.name
}
is RegistrationScreenAction.UpdatePassword -> {
_password.value = action.password
}
is RegistrationScreenAction.UpdateConfirmPassword -> {
_confirmPassword.value = action.confirmPassword
}
is RegistrationScreenAction.ResetFields -> {
resetFields()
}
is RegistrationScreenAction.RegisterNewUser -> {
registerNewAccount(
action.emailAddress,
action.name,
action.password,
action.confirmPassword
)
}
}
}
fun registerNewAccount(
emailAddress: String,
name: String,
@ -140,37 +217,43 @@ class RegistrationScreenViewModel @Inject constructor(
val validationResults = validateNewAccount(emailAddress, name, password, confirmPassword)
validationResults.forEach {
if (it == NewAccountRegistrationValidationResult.EmptyEmailAddressField) {
_emailAddressError.value = "Enter an email address"
_state.update {
it.copy(emailAddressError = "Enter an email address")
}
} else if (it == NewAccountRegistrationValidationResult.InvalidEmailAddress) {
_emailAddressError.value = "Enter a valid email address"
_state.update {
it.copy(emailAddressError = "Enter a valid email address")
}
} else if (it == NewAccountRegistrationValidationResult.ValidEmailAddress) {
_emailAddressError.value = ""
_state.update {
it.copy(emailAddressError = "")
}
}
if (it == NewAccountRegistrationValidationResult.EmptyNameField) {
_nameError.value = "Enter your name"
_state.update { it.copy(nameError = "Enter your name") }
} else if (it == NewAccountRegistrationValidationResult.ValidName) {
_nameError.value = ""
_state.update { it.copy(nameError = "") }
}
if (it == NewAccountRegistrationValidationResult.EmptyPasswordField) {
_passwordError.value = "Enter a password"
_state.update { it.copy(passwordError = "Enter a password") }
} else if (it == NewAccountRegistrationValidationResult.PasswordTooShort) {
_passwordError.value = "Password must be more than 8 characters"
_state.update { it.copy(passwordError = "Password must be more than 8 characters long") }
} else if (it == NewAccountRegistrationValidationResult.ValidPassword) {
_passwordError.value = ""
_state.update { it.copy(passwordError = "") }
}
if (it == NewAccountRegistrationValidationResult.EmptyConfirmPasswordField) {
_confirmPasswordError.value = "Enter a password"
_state.update { it.copy(confirmPasswordError = "Enter a password") }
} else if (it == NewAccountRegistrationValidationResult.PasswordsDontMatch) {
_confirmPasswordError.value = "Passwords don't match"
_state.update { it.copy(confirmPasswordError = "Passwords don't match") }
} else if (it == NewAccountRegistrationValidationResult.ValidConfirmPassword) {
_confirmPasswordError.value = ""
_state.update { it.copy(confirmPasswordError = "") }
}
}
return validationResults.filter {
return validationResults.none {
when (it) {
NewAccountRegistrationValidationResult.ValidEmailAddress,
NewAccountRegistrationValidationResult.ValidName,
@ -180,13 +263,6 @@ class RegistrationScreenViewModel @Inject constructor(
else -> true
}
}.isEmpty()
}
private fun resetFields() {
_emailAddress.value = ""
_name.value = ""
_password.value = ""
_confirmPassword.value = ""
}
}
}