Rework inputs to registration screen component
This commit is contained in:
parent
2dbf6d61d0
commit
878ff48d95
10 changed files with 367 additions and 201 deletions
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
) {
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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 -> {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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 = ""
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue