Adding field verification during registration
continuous-integration/drone/push Build is passing Details

pull/3/head
Arthur VALIN 1 year ago
parent b7337e679a
commit b835f1a4f4

@ -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<out Any> = 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<out Any>) = id?.let {
stringResource(id = id, *args)
}
@Composable
fun stringResourceOrNull(@StringRes id: Int?) = id?.let {
stringResource(id = id)
}

@ -40,6 +40,7 @@ fun AllInTextField(
placeholderFontSize: TextUnit = 18.sp, placeholderFontSize: TextUnit = 18.sp,
multiLine: Boolean = false, multiLine: Boolean = false,
onValueChange: (String)->Unit, onValueChange: (String)->Unit,
errorText: String? = null,
bringIntoViewRequester: BringIntoViewRequester, bringIntoViewRequester: BringIntoViewRequester,
visualTransformation: VisualTransformation = VisualTransformation.None, visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardType: KeyboardType = KeyboardType.Text, keyboardType: KeyboardType = KeyboardType.Text,
@ -62,6 +63,7 @@ fun AllInTextField(
OutlinedTextField( OutlinedTextField(
value = textFieldValue, value = textFieldValue,
isError = errorText!=null,
modifier = modifier modifier = modifier
.onFocusChanged { .onFocusChanged {
hasFocus = it.hasFocus 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, visualTransformation = visualTransformation,
singleLine = !multiLine, singleLine = !multiLine,
onValueChange = { onValueChange = {
@ -109,8 +122,10 @@ fun AllInTextField(
unfocusedBorderColor = borderColor, unfocusedBorderColor = borderColor,
focusedTextColor = textColor, focusedTextColor = textColor,
unfocusedTextColor = textColor, unfocusedTextColor = textColor,
errorTextColor = textColor,
focusedContainerColor = containerColor, focusedContainerColor = containerColor,
unfocusedContainerColor = containerColor unfocusedContainerColor = containerColor,
errorContainerColor = containerColor,
) )
) )
} }
@ -123,6 +138,7 @@ fun AllInPasswordField(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
keyboardType: KeyboardType = KeyboardType.Password, keyboardType: KeyboardType = KeyboardType.Password,
keyboardActions: KeyboardActions = KeyboardActions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default,
errorText: String? = null,
onValueChange: (String)->Unit, onValueChange: (String)->Unit,
bringIntoViewRequester: BringIntoViewRequester, bringIntoViewRequester: BringIntoViewRequester,
isHiddenByDefault: Boolean = true isHiddenByDefault: Boolean = true
@ -132,6 +148,7 @@ fun AllInPasswordField(
} }
AllInTextField( AllInTextField(
modifier = modifier, modifier = modifier,
errorText = errorText,
placeholder = placeholder, placeholder = placeholder,
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
visualTransformation = if (hidden) PasswordVisualTransformation() else VisualTransformation.None, 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) @OptIn(ExperimentalFoundationApi::class)
@Preview @Preview
@Composable @Composable

@ -7,7 +7,6 @@ import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
@ -34,22 +33,34 @@ fun RegisterScreen(
) { ) {
val loading by remember{ registerViewModel.loading } 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 (username, setUsername) = remember{ registerViewModel.username }
val (email, setEmail) = remember{ registerViewModel.email } val (email, setEmail) = remember{ registerViewModel.email }
val (password, setPassword) = remember{ registerViewModel.password } val (password, setPassword) = remember{ registerViewModel.password }
val (passwordValidation, setPasswordValidation) = remember{ registerViewModel.passwordValidation } val (passwordValidation, setPasswordValidation) = remember{ registerViewModel.passwordValidation }
val bringIntoViewRequester = remember { BringIntoViewRequester() } 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 Modifier
.fillMaxSize() .fillMaxSize()
.background(AllInTheme.themeColors.main_surface) .background(AllInTheme.themeColors.main_surface)
.padding(horizontal = 44.dp) .padding(horizontal = 44.dp)
.verticalScroll(rememberScrollState())
) { ) {
Column( Column(
Modifier.align(Alignment.Center) Modifier
.weight(1f)
.verticalScroll(scrollState),
verticalArrangement = Arrangement.Center
) { ) {
Text( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -84,24 +95,27 @@ fun RegisterScreen(
) { ) {
AllInTextField( AllInTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeholder = stringResource(id = R.string.username), placeholder = usernameFieldName,
value = username, value = username,
onValueChange = setUsername, onValueChange = setUsername,
maxChar = 20, maxChar = 20,
errorText = usernameError.errorResource(),
bringIntoViewRequester = bringIntoViewRequester bringIntoViewRequester = bringIntoViewRequester
) )
AllInTextField( AllInTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeholder = stringResource(id = R.string.email), placeholder = emailFieldName,
value = email, value = email,
onValueChange = setEmail, onValueChange = setEmail,
errorText = emailError.errorResource(),
keyboardType = KeyboardType.Email, keyboardType = KeyboardType.Email,
bringIntoViewRequester = bringIntoViewRequester bringIntoViewRequester = bringIntoViewRequester
) )
AllInPasswordField( AllInPasswordField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeholder = stringResource(id = R.string.password), placeholder = passwordFieldName,
value = password, value = password,
errorText = passwordError.errorResource(),
onValueChange = setPassword, onValueChange = setPassword,
bringIntoViewRequester = bringIntoViewRequester bringIntoViewRequester = bringIntoViewRequester
) )
@ -109,21 +123,25 @@ fun RegisterScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeholder = stringResource(id = R.string.confirm_password), placeholder = stringResource(id = R.string.confirm_password),
value = passwordValidation, value = passwordValidation,
errorText = passwordValidationError.errorResource(),
onValueChange = setPasswordValidation, onValueChange = setPasswordValidation,
bringIntoViewRequester = bringIntoViewRequester bringIntoViewRequester = bringIntoViewRequester
) )
} }
Spacer(modifier = Modifier.height(67.dp))
} }
Column( Column(
Modifier Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 32.dp) .padding(bottom = 32.dp)
) { ) {
AllInGradientButton( AllInGradientButton(
text = stringResource(id = R.string.Register), text = stringResource(id = R.string.Register),
onClick = { onClick = {
registerViewModel.onRegister(navigateToDashboard) registerViewModel.onRegister(
usernameFieldName,
emailFieldName,
passwordFieldName,
navigateToDashboard
)
} }
) )
Spacer(modifier = Modifier.height(30.dp)) Spacer(modifier = Modifier.height(30.dp))

@ -5,37 +5,100 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import fr.iut.alldev.allin.data.repository.UserRepository 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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
private const val MIN_PASSWORD_SIZE = 8
private const val MIN_USERNAME_SIZE = 3
@HiltViewModel @HiltViewModel
class RegisterViewModel @Inject constructor( class RegisterViewModel @Inject constructor(
val userRepository: UserRepository private val userRepository: UserRepository
) : ViewModel() { ) : ViewModel() {
var loading = mutableStateOf(false) var loading = mutableStateOf(false)
var hasError = mutableStateOf(false)
val username = mutableStateOf("") val username = mutableStateOf("")
val email = mutableStateOf("") val email = mutableStateOf("")
val password = mutableStateOf("") val password = mutableStateOf("")
val passwordValidation = mutableStateOf("") val passwordValidation = mutableStateOf("")
val usernameError = mutableStateOf<FieldErrorState>(FieldErrorState.NoError)
val emailError = mutableStateOf<FieldErrorState>(FieldErrorState.NoError)
val passwordError = mutableStateOf<FieldErrorState>(FieldErrorState.NoError)
val passwordValidationError = mutableStateOf<FieldErrorState>(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( fun onRegister(
usernameFieldName:String,
emailFieldName: String,
passwordFieldName:String,
navigateToDashboard: ()->Unit navigateToDashboard: ()->Unit
){ ){
viewModelScope.launch { viewModelScope.launch {
loading.value = true loading.value = true
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
userRepository.register(
username.value, initErrorField()
email.value, verifyField(
password.value 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 loading.value = false
} }
} }

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!--Core-->
<string name="username">Pseudo</string> <string name="username">Pseudo</string>
<string name="email">Email</string> <string name="email">Email</string>
<string name="password">Mot de passe</string> <string name="password">Mot de passe</string>
@ -12,6 +13,10 @@
<string name="Validate">Valider</string> <string name="Validate">Valider</string>
<string name="Cancel">Annuler</string> <string name="Cancel">Annuler</string>
<string name="Ok">OK</string> <string name="Ok">OK</string>
<string name="FieldError_TooShort">Le %s doit contenir au moins %d caractères.</string>
<string name="FieldError_BadFormat">Le %s a un mauvais format : %s.</string>
<string name="FieldError_NotIdentical">Les champs ne sont pas identiques.</string>
<string name="FieldError_NoSpecialCharacter">Le %s doit contenir au moins un caractère spécial : %s.</string>
<!--Drawer--> <!--Drawer-->
<string name="bets">Bets</string> <string name="bets">Bets</string>

@ -15,6 +15,10 @@
<string name="Validate">Validate</string> <string name="Validate">Validate</string>
<string name="Cancel">Cancel</string> <string name="Cancel">Cancel</string>
<string name="Ok">OK</string> <string name="Ok">OK</string>
<string name="FieldError_TooShort">The %s must contain at least %d characters.</string>
<string name="FieldError_BadFormat">The %s has bad format : %s.</string>
<string name="FieldError_NotIdentical">The fields are not identical.</string>
<string name="FieldError_NoSpecialCharacter">The %s must contain at least one special character : %s.</string>
<!--Drawer--> <!--Drawer-->
<string name="bets">Bets</string> <string name="bets">Bets</string>

Loading…
Cancel
Save