diff --git a/app/.kotlin/sessions/kotlin-compiler-14485427041908763179.salive b/app/.kotlin/sessions/kotlin-compiler-14485427041908763179.salive new file mode 100644 index 0000000..e69de29 diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/host/MainActivity.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/host/MainActivity.kt index d0ee4c3..721dd85 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/host/MainActivity.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/host/MainActivity.kt @@ -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( startDestination = Destination.AuthLogin ) { - composable { LoginScreen() } - composable { RegistrationScreen() } + composable { + val viewModel = hiltViewModel() + + 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 { + val viewModel = hiltViewModel() + + 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 { val (emailAddress) = it.toRoute() ConfirmationScreen( diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreen.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreen.kt index 974d048..07f1537 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreen.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreen.kt @@ -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() + emailAddress: State, + isEmailAddressValid: State, + password: State, + isPasswordValid: State, + toastMessages: State, + 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() ) { diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenState.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenState.kt new file mode 100644 index 0000000..134c25a --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenState.kt @@ -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, +) \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenStateAction.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenStateAction.kt index c8d00a4..c160618 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenStateAction.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenStateAction.kt @@ -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 } \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenViewModel.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenViewModel.kt index 9d3bb12..7266107 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenViewModel.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenViewModel.kt @@ -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() @@ -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 -> { diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreen.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreen.kt index 7a6e1c3..e0c9429 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreen.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreen.kt @@ -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() + emailAddress: State, + emailAddressError: State, + isEmailAddressValid: State, + name: State, + nameError: State, + isNameValid: State, + password: State, + passwordError: State, + isPasswordValid: State, + confirmPassword: State, + confirmPasswordError: State, + isConfirmPasswordValid: State, + onAction: (RegistrationScreenAction) -> Unit, + onRegistrationMessage: State?>, + 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, + emailAddressError: State, + isEmailAddressValid: State, + name: State, + nameError: State, + isNameValid: State, + password: State, + passwordError: State, + isPasswordValid: State, + confirmPassword: State, + confirmPasswordError: State, + isConfirmPasswordValid: State, + 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, - name: State, - password: State, - confirmPassword: State, - 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() diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenAction.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenAction.kt index cff7d64..eea59b2 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenAction.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenAction.kt @@ -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 } \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenState.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenState.kt new file mode 100644 index 0000000..ae55812 --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenState.kt @@ -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 +) \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenViewModel.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenViewModel.kt index eb28056..a589692 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenViewModel.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenViewModel.kt @@ -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?>() + 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>() - 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 = "" + } } } \ No newline at end of file