diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ext/FieldExt.kt b/src/app/src/main/java/fr/iut/alldev/allin/ext/FieldExt.kt new file mode 100644 index 0000000..f18262f --- /dev/null +++ b/src/app/src/main/java/fr/iut/alldev/allin/ext/FieldExt.kt @@ -0,0 +1,54 @@ +package fr.iut.alldev.allin.ext + +import android.util.Patterns +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import fr.iut.alldev.allin.R + +const val ALLOWED_SYMBOLS = "~`!@#\$%^&*()_-+={[}]|\\:;\"'<,>.?/" + +sealed class FieldErrorState( + private val messageId: Int? = null, + private val messageArgs:Array = emptyArray() +){ + object NoError: FieldErrorState() + + data class TooShort(val fieldName: String, val minChar: Int) + : FieldErrorState(R.string.FieldError_TooShort, arrayOf(fieldName, minChar)) + + data class BadFormat(val fieldName: String, val format: String) + : FieldErrorState(R.string.FieldError_BadFormat, arrayOf(fieldName, format)) + + object NotIdentical: FieldErrorState(R.string.FieldError_NotIdentical) + + data class NoSpecialCharacter(val fieldName: String, val characters: String = ALLOWED_SYMBOLS) : + FieldErrorState(R.string.FieldError_NoSpecialCharacter, arrayOf(fieldName, characters)) + + @Composable + fun errorResource() = stringResourceOrNull(id = messageId, messageArgs) +} + +fun String.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches() + +fun String.containsCharacter(characters: CharSequence): Boolean { + var contains = false + characters.forEach { + if(this.contains(it)){ + contains = true + return@forEach + } + } + return contains +} + + +@Composable +fun stringResourceOrNull(@StringRes id: Int?, args: Array) = id?.let { + stringResource(id = id, *args) +} + +@Composable +fun stringResourceOrNull(@StringRes id: Int?) = id?.let { + stringResource(id = id) +} diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/core/AllInTextField.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/core/AllInTextField.kt index bfd577b..5628909 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/core/AllInTextField.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/core/AllInTextField.kt @@ -40,6 +40,7 @@ fun AllInTextField( placeholderFontSize: TextUnit = 18.sp, multiLine: Boolean = false, onValueChange: (String)->Unit, + errorText: String? = null, bringIntoViewRequester: BringIntoViewRequester, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardType: KeyboardType = KeyboardType.Text, @@ -62,6 +63,7 @@ fun AllInTextField( OutlinedTextField( value = textFieldValue, + isError = errorText!=null, modifier = modifier .onFocusChanged { hasFocus = it.hasFocus @@ -71,6 +73,17 @@ fun AllInTextField( } } }, + supportingText = errorText?.let { + { + Text( + text = it, + style = AllInTheme.typography.r, + color = Color.Red, + fontSize = 10.sp, + overflow = TextOverflow.Ellipsis + ) + } + }, visualTransformation = visualTransformation, singleLine = !multiLine, onValueChange = { @@ -109,8 +122,10 @@ fun AllInTextField( unfocusedBorderColor = borderColor, focusedTextColor = textColor, unfocusedTextColor = textColor, + errorTextColor = textColor, focusedContainerColor = containerColor, - unfocusedContainerColor = containerColor + unfocusedContainerColor = containerColor, + errorContainerColor = containerColor, ) ) } @@ -123,6 +138,7 @@ fun AllInPasswordField( modifier: Modifier = Modifier, keyboardType: KeyboardType = KeyboardType.Password, keyboardActions: KeyboardActions = KeyboardActions.Default, + errorText: String? = null, onValueChange: (String)->Unit, bringIntoViewRequester: BringIntoViewRequester, isHiddenByDefault: Boolean = true @@ -132,6 +148,7 @@ fun AllInPasswordField( } AllInTextField( modifier = modifier, + errorText = errorText, placeholder = placeholder, keyboardActions = keyboardActions, visualTransformation = if (hidden) PasswordVisualTransformation() else VisualTransformation.None, @@ -182,6 +199,21 @@ private fun AllInTextFieldValuePreview() { } } +@OptIn(ExperimentalFoundationApi::class) +@Preview +@Composable +private fun AllInTextFieldErrorPreview() { + AllInTheme { + AllInTextField( + placeholder = "Email", + value = "JohnDoe@mail.com", + errorText = "This is an error.", + onValueChange = { }, + bringIntoViewRequester = BringIntoViewRequester() + ) + } +} + @OptIn(ExperimentalFoundationApi::class) @Preview @Composable diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/register/RegisterScreen.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/register/RegisterScreen.kt index 5fcdacb..536ca5d 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/register/RegisterScreen.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/register/RegisterScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.text.ClickableText import androidx.compose.material3.Text import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString @@ -34,22 +33,34 @@ fun RegisterScreen( ) { val loading by remember{ registerViewModel.loading } + val usernameError by remember{ registerViewModel.usernameError } + val emailError by remember{ registerViewModel.emailError } + val passwordError by remember{ registerViewModel.passwordError } + val passwordValidationError by remember{ registerViewModel.passwordValidationError } + val (username, setUsername) = remember{ registerViewModel.username } val (email, setEmail) = remember{ registerViewModel.email } val (password, setPassword) = remember{ registerViewModel.password } val (passwordValidation, setPasswordValidation) = remember{ registerViewModel.passwordValidation } val bringIntoViewRequester = remember { BringIntoViewRequester() } + val scrollState = rememberScrollState() + + val usernameFieldName = stringResource(id = R.string.username) + val emailFieldName = stringResource(id = R.string.email) + val passwordFieldName = stringResource(id = R.string.password) - Box( + Column( Modifier .fillMaxSize() .background(AllInTheme.themeColors.main_surface) .padding(horizontal = 44.dp) - .verticalScroll(rememberScrollState()) ) { Column( - Modifier.align(Alignment.Center) + Modifier + .weight(1f) + .verticalScroll(scrollState), + verticalArrangement = Arrangement.Center ) { Text( modifier = Modifier.fillMaxWidth(), @@ -84,24 +95,27 @@ fun RegisterScreen( ) { AllInTextField( modifier = Modifier.fillMaxWidth(), - placeholder = stringResource(id = R.string.username), + placeholder = usernameFieldName, value = username, onValueChange = setUsername, maxChar = 20, + errorText = usernameError.errorResource(), bringIntoViewRequester = bringIntoViewRequester ) AllInTextField( modifier = Modifier.fillMaxWidth(), - placeholder = stringResource(id = R.string.email), + placeholder = emailFieldName, value = email, onValueChange = setEmail, + errorText = emailError.errorResource(), keyboardType = KeyboardType.Email, bringIntoViewRequester = bringIntoViewRequester ) AllInPasswordField( modifier = Modifier.fillMaxWidth(), - placeholder = stringResource(id = R.string.password), + placeholder = passwordFieldName, value = password, + errorText = passwordError.errorResource(), onValueChange = setPassword, bringIntoViewRequester = bringIntoViewRequester ) @@ -109,21 +123,25 @@ fun RegisterScreen( modifier = Modifier.fillMaxWidth(), placeholder = stringResource(id = R.string.confirm_password), value = passwordValidation, + errorText = passwordValidationError.errorResource(), onValueChange = setPasswordValidation, bringIntoViewRequester = bringIntoViewRequester ) } - Spacer(modifier = Modifier.height(67.dp)) } Column( Modifier - .align(Alignment.BottomCenter) .padding(bottom = 32.dp) ) { AllInGradientButton( text = stringResource(id = R.string.Register), onClick = { - registerViewModel.onRegister(navigateToDashboard) + registerViewModel.onRegister( + usernameFieldName, + emailFieldName, + passwordFieldName, + navigateToDashboard + ) } ) Spacer(modifier = Modifier.height(30.dp)) diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/register/RegisterViewModel.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/register/RegisterViewModel.kt index 53cd8aa..13c5061 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/register/RegisterViewModel.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/register/RegisterViewModel.kt @@ -5,37 +5,100 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import fr.iut.alldev.allin.data.repository.UserRepository +import fr.iut.alldev.allin.ext.ALLOWED_SYMBOLS +import fr.iut.alldev.allin.ext.FieldErrorState +import fr.iut.alldev.allin.ext.containsCharacter +import fr.iut.alldev.allin.ext.isEmail import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject +private const val MIN_PASSWORD_SIZE = 8 +private const val MIN_USERNAME_SIZE = 3 + @HiltViewModel class RegisterViewModel @Inject constructor( - val userRepository: UserRepository + private val userRepository: UserRepository ) : ViewModel() { var loading = mutableStateOf(false) + var hasError = mutableStateOf(false) val username = mutableStateOf("") val email = mutableStateOf("") val password = mutableStateOf("") val passwordValidation = mutableStateOf("") + val usernameError = mutableStateOf(FieldErrorState.NoError) + val emailError = mutableStateOf(FieldErrorState.NoError) + val passwordError = mutableStateOf(FieldErrorState.NoError) + val passwordValidationError = mutableStateOf(FieldErrorState.NoError) + + private fun initErrorField(){ + usernameError.value = FieldErrorState.NoError + emailError.value = FieldErrorState.NoError + passwordError.value = FieldErrorState.NoError + passwordValidationError.value = FieldErrorState.NoError + hasError.value = false + } + private fun verifyField( + usernameFieldName:String, + emailFieldName:String, + passwordFieldName:String + ){ + if(username.value.length < MIN_USERNAME_SIZE){ + usernameError.value = FieldErrorState.TooShort(usernameFieldName,MIN_USERNAME_SIZE) + hasError.value = true + } + + if(password.value.length < MIN_PASSWORD_SIZE){ + passwordError.value = FieldErrorState.TooShort(passwordFieldName, MIN_PASSWORD_SIZE) + hasError.value = true + }else if(!password.value.containsCharacter(ALLOWED_SYMBOLS)){ + passwordError.value = FieldErrorState.NoSpecialCharacter(passwordFieldName) + hasError.value = true + } + + if(!email.value.isEmail()){ + emailError.value = FieldErrorState.BadFormat(emailFieldName, "john@doe.com") + hasError.value = true + } + + if(passwordValidation.value != password.value){ + passwordValidationError.value = FieldErrorState.NotIdentical + hasError.value = true + } + } + fun onRegister( + usernameFieldName:String, + emailFieldName: String, + passwordFieldName:String, navigateToDashboard: ()->Unit ){ viewModelScope.launch { loading.value = true withContext(Dispatchers.IO) { - userRepository.register( - username.value, - email.value, - password.value + + initErrorField() + verifyField( + usernameFieldName.lowercase(), + emailFieldName.lowercase(), + passwordFieldName.lowercase() ) + if(!hasError.value) { + userRepository.register( + username.value, + email.value, + password.value + ) + } + } + if(!hasError.value){ + navigateToDashboard() } - navigateToDashboard() loading.value = false } } diff --git a/src/app/src/main/res/values-fr/strings.xml b/src/app/src/main/res/values-fr/strings.xml index 3cca4a7..dad0440 100644 --- a/src/app/src/main/res/values-fr/strings.xml +++ b/src/app/src/main/res/values-fr/strings.xml @@ -1,5 +1,6 @@ + Pseudo Email Mot de passe @@ -12,6 +13,10 @@ Valider Annuler OK + Le %s doit contenir au moins %d caractères. + Le %s a un mauvais format : %s. + Les champs ne sont pas identiques. + Le %s doit contenir au moins un caractère spécial : %s. Bets diff --git a/src/app/src/main/res/values/strings.xml b/src/app/src/main/res/values/strings.xml index d339f48..d29c4ec 100644 --- a/src/app/src/main/res/values/strings.xml +++ b/src/app/src/main/res/values/strings.xml @@ -15,6 +15,10 @@ Validate Cancel OK + The %s must contain at least %d characters. + The %s has bad format : %s. + The fields are not identical. + The %s must contain at least one special character : %s. Bets