diff --git a/src/main/kotlin/core/form/validation/CommonRules.kt b/src/main/kotlin/core/form/validation/CommonRules.kt index 5e152cc..578ff92 100644 --- a/src/main/kotlin/core/form/validation/CommonRules.kt +++ b/src/main/kotlin/core/form/validation/CommonRules.kt @@ -39,6 +39,28 @@ val passwordRule: ValidationRule = ValidationRule( errorMessage = "Password must be 8+ chars, with a number, symbol, upper & lower case." ) +val creditCardRule: ValidationRule = ValidationRule( + condition = { card -> + card.matches(Regex("^4[0-9]{12}(?:[0-9]{3})?$")) + || card.matches(Regex("^5[1-5][0-9]{14}$")) + }, + errorMessage = "Invalid Credit Card Number" +) + +val creditCardPinRule: ValidationRule = ValidationRule( + condition = { card -> + card.matches(Regex("^[0-9]{3,4}$")) + }, + errorMessage = "Invalid Credit Card PIN" +) + +val creditCardExpiryDateRule: ValidationRule = ValidationRule( + condition = { card -> + card.matches(Regex("^(0[1-9]|1[0-2])/([0-9]{2})$")) + }, + errorMessage = "Invalid Credit Card Expiry Date" +) + fun lengthRule(field: String, length: Int): ValidationRule { return ValidationRule( condition = { field.length >= length }, diff --git a/src/main/kotlin/ui/components/FormComponents.kt b/src/main/kotlin/ui/components/FormComponents.kt index 264a353..825a7d2 100644 --- a/src/main/kotlin/ui/components/FormComponents.kt +++ b/src/main/kotlin/ui/components/FormComponents.kt @@ -102,6 +102,60 @@ fun FormTextField( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FormTextArea( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + label: String = "", + minLines: Int = 3, + maxLines: Int = 5 +) { + val interactionSource = remember { MutableInteractionSource() } + + Surface( + modifier = modifier.height(IntrinsicSize.Min), + color = secondary, + shape = RoundedCornerShape(8.dp), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp) + ) { + if (label.isNotEmpty()) { + Text( + text = label, + fontSize = 10.sp, + fontFamily = Font.RussoOne, + color = Color.White, + modifier = Modifier.padding(top = 8.dp, bottom = 4.dp) + ) + } + + BasicTextField( + value = value, + onValueChange = onValueChange, + interactionSource = interactionSource, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + textStyle = TextStyle( + fontFamily = Font.RussoOne, + color = Color.White, + textAlign = TextAlign.Start, + fontSize = 12.sp, + lineHeight = 18.sp + ), + minLines = minLines, + maxLines = maxLines, + cursorBrush = SolidColor(Color.White), + ) + } + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun PasswordTextField( diff --git a/src/main/kotlin/ui/components/forms/passwordmgnt/CreditCardForm.kt b/src/main/kotlin/ui/components/forms/passwordmgnt/CreditCardForm.kt index b815a9c..739c69c 100644 --- a/src/main/kotlin/ui/components/forms/passwordmgnt/CreditCardForm.kt +++ b/src/main/kotlin/ui/components/forms/passwordmgnt/CreditCardForm.kt @@ -1,27 +1,49 @@ package ui.components.forms.passwordmgnt import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CreditCard +import androidx.compose.material.icons.filled.DateRange +import androidx.compose.material.icons.filled.Pin +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.koin.koinScreenModel import cafe.adriel.voyager.navigator.LocalNavigator +import ui.components.FormTextArea +import ui.components.FormTextField +import ui.theme.Font import ui.theme.primary import ui.theme.secondary import ui.theme.tertiary +import ui.validators.CreditCardFormFieldName +import ui.validators.creditCardFormValidator +import viewmodel.PasswordMgntScreenModel class CreditCardForm : Screen { @Composable override fun Content() { + val formValidator = remember { creditCardFormValidator() } + val isFormValid by formValidator.isValid + val screenModel = koinScreenModel() val navigator = LocalNavigator.current + val cardNumber = formValidator.getField(CreditCardFormFieldName.CARD_NUMBER) + val cvc = formValidator.getField(CreditCardFormFieldName.CARD_CVC) + val pin = formValidator.getField(CreditCardFormFieldName.CARD_PIN) + val expiry = formValidator.getField(CreditCardFormFieldName.CARD_EXPIRY) + val notes = formValidator.getField(CreditCardFormFieldName.CARD_NOTES) + Column( modifier = Modifier.fillMaxSize() .background(secondary) @@ -47,8 +69,126 @@ class CreditCardForm : Screen { horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { + Column( + modifier = Modifier.background(primary) + .padding(PaddingValues(end = 20.dp)), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterVertically) + ) { + Row(horizontalArrangement = Arrangement.spacedBy(25.dp)) { + Column(modifier = Modifier.height(80.dp)) { + FormTextField( + value = cardNumber?.value?.value ?: "", + onValueChange = { newValue -> + cardNumber?.value?.value = newValue + formValidator.validateField(CreditCardFormFieldName.CARD_NUMBER) + }, + label = CreditCardFormFieldName.CARD_NUMBER.fieldName, + icon = Icons.Filled.CreditCard, + modifier = Modifier.height(45.dp).width(400.dp) + ) + cardNumber?.error?.value?.let { + Text( + text = it, + color = Color.Red, + fontFamily = Font.RobotoRegular, + fontSize = 10.sp, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + Column(modifier = Modifier.height(80.dp)) { + FormTextField( + value = cvc?.value?.value ?: "", + onValueChange = { newValue -> + cvc?.value?.value = newValue + formValidator.validateField(CreditCardFormFieldName.CARD_CVC) + }, + label = CreditCardFormFieldName.CARD_CVC.fieldName, + icon = Icons.Filled.Pin, + modifier = Modifier.height(45.dp).width(400.dp) + ) + cvc?.error?.value?.let { + Text( + text = it, + color = Color.Red, + fontFamily = Font.RobotoRegular, + fontSize = 10.sp, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + } + + Row(horizontalArrangement = Arrangement.spacedBy(25.dp)) { + Column(modifier = Modifier.height(80.dp)) { + FormTextField( + value = pin?.value?.value ?: "", + onValueChange = { newValue -> + pin?.value?.value = newValue + formValidator.validateField(CreditCardFormFieldName.CARD_PIN) + }, + label = CreditCardFormFieldName.CARD_PIN.fieldName, + icon = Icons.Filled.Pin, + modifier = Modifier.height(45.dp).width(400.dp) + ) + pin?.error?.value?.let { + Text( + text = it, + color = Color.Red, + fontFamily = Font.RobotoRegular, + fontSize = 10.sp, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + Column(modifier = Modifier.height(80.dp)) { + FormTextField( + value = expiry?.value?.value ?: "", + onValueChange = { newValue -> + expiry?.value?.value = newValue + formValidator.validateField(CreditCardFormFieldName.CARD_EXPIRY) + }, + label = CreditCardFormFieldName.CARD_EXPIRY.fieldName, + icon = Icons.Filled.DateRange, + modifier = Modifier.height(45.dp).width(400.dp) + ) + expiry?.error?.value?.let { + Text( + text = it, + color = Color.Red, + fontFamily = Font.RobotoRegular, + fontSize = 10.sp, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + } + Row(horizontalArrangement = Arrangement.spacedBy(25.dp)) { + Column(modifier = Modifier.height(120.dp)) { + FormTextArea( + value = notes?.value?.value ?: "", + onValueChange = { newValue -> + notes?.value?.value = newValue + formValidator.validateField(CreditCardFormFieldName.CARD_NOTES) + }, + label = CreditCardFormFieldName.CARD_NOTES.fieldName, + modifier = Modifier.width(400.dp) + ) + notes?.error?.value?.let { + Text( + text = it, + color = Color.Red, + fontFamily = Font.RobotoRegular, + fontSize = 10.sp, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + } + } } Row( @@ -61,7 +201,7 @@ class CreditCardForm : Screen { Footer( { }, { navigator?.pop() }, - true + isFormValid ) } diff --git a/src/main/kotlin/ui/validators/CreditCardFormValidator.kt b/src/main/kotlin/ui/validators/CreditCardFormValidator.kt new file mode 100644 index 0000000..d5d1db7 --- /dev/null +++ b/src/main/kotlin/ui/validators/CreditCardFormValidator.kt @@ -0,0 +1,47 @@ +package ui.validators + +import core.form.FormField +import core.form.FormFieldName +import core.form.validation.* + +fun creditCardFormValidator(): FormValidator { + return FormValidator() + .addField( + CreditCardFormFieldName.CARD_NUMBER, FormField( + validator = Validator().addRule(creditCardRule) + .addRule(notNullRule(CreditCardFormFieldName.CARD_NUMBER.fieldName)) + ) + ) + .addField( + CreditCardFormFieldName.CARD_PIN, FormField( + validator = Validator().addRule(creditCardPinRule) + .addRule(notNullRule(CreditCardFormFieldName.CARD_PIN.fieldName)) + ) + ) + .addField( + CreditCardFormFieldName.CARD_CVC, FormField( + validator = Validator().addRule(creditCardPinRule) + .addRule(notNullRule(CreditCardFormFieldName.CARD_CVC.fieldName)) + ) + ) + .addField( + CreditCardFormFieldName.CARD_EXPIRY, FormField( + validator = Validator().addRule(notNullRule(CreditCardFormFieldName.CARD_EXPIRY.fieldName)) + .addRule(creditCardExpiryDateRule) + ) + ) + .addField( + CreditCardFormFieldName.CARD_NOTES, FormField( + validator = Validator() + ) + ) + .validateAllFields() +} + +enum class CreditCardFormFieldName(val fieldName: String) : FormFieldName { + CARD_NUMBER("Card number"), + CARD_PIN("Pin"), + CARD_CVC("Cvc"), + CARD_EXPIRY("Expiry Date"), + CARD_NOTES("Notes") +} \ No newline at end of file