Implemented validating and auto correcting user input in TransferMoneyDialog

This commit is contained in:
dankito 2020-08-24 12:08:58 +02:00
parent 321814a0ca
commit 137d35ac02
11 changed files with 1156 additions and 189 deletions

View File

@ -16,6 +16,8 @@ expect class BigDecimal {
constructor(double: Double) constructor(double: Double)
val isPositive: Boolean
fun format(countDecimalPlaces: Int): String fun format(countDecimalPlaces: Int): String
} }

View File

@ -12,7 +12,7 @@ actual fun Collection<BigDecimal>.sum(): BigDecimal {
} }
actual class BigDecimal(val decimal: NSDecimalNumber) { // it's almost impossible to derive from NSDecimalNumber so i keep it as property actual class BigDecimal(val decimal: NSDecimalNumber) : Comparable<BigDecimal> { // it's almost impossible to derive from NSDecimalNumber so i keep it as property
actual companion object { actual companion object {
actual val Zero = BigDecimal(0.0) actual val Zero = BigDecimal(0.0)
@ -23,6 +23,9 @@ actual class BigDecimal(val decimal: NSDecimalNumber) { // it's almost impossibl
actual constructor(decimal: String) : this(decimal.toDouble()) actual constructor(decimal: String) : this(decimal.toDouble())
actual val isPositive: Boolean
get() = this >= Zero
actual fun format(countDecimalPlaces: Int): String { actual fun format(countDecimalPlaces: Int): String {
val formatter = NSNumberFormatter() val formatter = NSNumberFormatter()
@ -33,9 +36,18 @@ actual class BigDecimal(val decimal: NSDecimalNumber) { // it's almost impossibl
} }
override fun compareTo(other: BigDecimal): Int {
return when (decimal.compare(other.decimal)) {
NSOrderedSame -> 0
NSOrderedAscending -> -1
NSOrderedDescending -> 1
else -> 0
}
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (other is BigDecimal) { if (other is BigDecimal) {
return this.decimal.compare(other.decimal) == NSOrderedSame return this.compareTo(other) == 0
} }
return super.equals(other) return super.equals(other)

View File

@ -26,6 +26,9 @@ actual class BigDecimal actual constructor(decimal: String) : java.math.BigDecim
actual constructor(double: Double) : this(java.math.BigDecimal.valueOf(double).toPlainString()) // for object deserializers actual constructor(double: Double) : this(java.math.BigDecimal.valueOf(double).toPlainString()) // for object deserializers
actual val isPositive: Boolean
get() = this >= ZERO
actual fun format(countDecimalPlaces: Int): String { actual fun format(countDecimalPlaces: Int): String {
return String.format("%.0${countDecimalPlaces}f", this) return String.format("%.0${countDecimalPlaces}f", this)
} }

View File

@ -34,6 +34,7 @@ open class SepaMessageCreator : ISepaMessageCreator {
override fun containsOnlyAllowedCharacters(stringToTest: String): Boolean { override fun containsOnlyAllowedCharacters(stringToTest: String): Boolean {
return AllowedSepaCharactersPattern.matches(stringToTest) return AllowedSepaCharactersPattern.matches(stringToTest)
&& convertDiacriticsAndReservedXmlCharacters(stringToTest) == stringToTest
} }
override fun convertDiacriticsAndReservedXmlCharacters(input: String): String { override fun convertDiacriticsAndReservedXmlCharacters(input: String): String {

View File

@ -12,6 +12,7 @@ import android.widget.EditText
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.Autocomplete
import kotlinx.android.synthetic.main.dialog_transfer_money.* import kotlinx.android.synthetic.main.dialog_transfer_money.*
import kotlinx.android.synthetic.main.dialog_transfer_money.view.* import kotlinx.android.synthetic.main.dialog_transfer_money.view.*
@ -31,6 +32,7 @@ import net.dankito.banking.ui.model.responses.BankingClientResponse
import net.dankito.banking.ui.presenter.BankingPresenter import net.dankito.banking.ui.presenter.BankingPresenter
import net.dankito.banking.util.InputValidator import net.dankito.banking.util.InputValidator
import net.dankito.banking.bankfinder.BankInfo import net.dankito.banking.bankfinder.BankInfo
import net.dankito.banking.util.ValidationResult
import net.dankito.utils.multiplatform.toBigDecimal import net.dankito.utils.multiplatform.toBigDecimal
import net.dankito.utils.android.extensions.asActivity import net.dankito.utils.android.extensions.asActivity
import java.math.BigDecimal import java.math.BigDecimal
@ -57,7 +59,17 @@ open class TransferMoneyDialog : DialogFragment() {
protected val inputValidator = InputValidator() // TODO: move to presenter protected val inputValidator = InputValidator() // TODO: move to presenter
protected var foundBankForEnteredIban = false protected var validRemitteeNameEntered = false
protected var validRemitteeIbanEntered = false
protected var validRemitteeBicEntered = false
protected var validUsageEntered = true
protected var validAmountEntered = false
protected var didJustCorrectInput = mutableMapOf<TextInputLayout, Boolean>()
@Inject @Inject
@ -113,10 +125,13 @@ open class TransferMoneyDialog : DialogFragment() {
initRemitteeAutocompletion(rootView.edtxtRemitteeName) initRemitteeAutocompletion(rootView.edtxtRemitteeName)
rootView.edtxtRemitteeName.addTextChangedListener(checkRequiredDataWatcher { rootView.edtxtRemitteeName.addTextChangedListener(checkRequiredDataWatcher {
checkIfEnteredRemitteeNameIsValid() checkIfEnteredRemitteeNameIsValidWhileUserIsTyping()
}) })
rootView.edtxtRemitteeIban.addTextChangedListener(StandardTextWatcher { tryToGetBicFromIban(it) }) rootView.edtxtRemitteeIban.addTextChangedListener(StandardTextWatcher {
checkIfEnteredRemitteeIbanIsValidWhileUserIsTyping()
tryToGetBicFromIban(it)
})
rootView.edtxtRemitteeBic.addTextChangedListener(checkRequiredDataWatcher()) rootView.edtxtRemitteeBic.addTextChangedListener(checkRequiredDataWatcher())
rootView.edtxtAmount.addTextChangedListener(checkRequiredDataWatcher()) rootView.edtxtAmount.addTextChangedListener(checkRequiredDataWatcher())
@ -124,8 +139,8 @@ open class TransferMoneyDialog : DialogFragment() {
checkIfEnteredUsageTextIsValid() checkIfEnteredUsageTextIsValid()
}) })
rootView.edtxtRemitteeName.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeNameIsValid() } rootView.edtxtRemitteeName.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeNameIsValidAfterFocusLost() }
rootView.edtxtRemitteeIban.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeIbanIsValid() } rootView.edtxtRemitteeIban.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeIbanIsValidAfterFocusLost() }
rootView.edtxtRemitteeBic.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeBicIsValid() } rootView.edtxtRemitteeBic.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeBicIsValid() }
rootView.edtxtAmount.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredAmountIsValid() } rootView.edtxtAmount.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredAmountIsValid() }
rootView.edtxtUsage.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredUsageTextIsValid() } rootView.edtxtUsage.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredUsageTextIsValid() }
@ -290,8 +305,8 @@ open class TransferMoneyDialog : DialogFragment() {
} }
} }
protected open fun tryToGetBicFromIban(enteredText: CharSequence) { protected open fun tryToGetBicFromIban(enteredIban: CharSequence) {
presenter.findUniqueBankForIbanAsync(enteredText.toString()) { foundBank -> presenter.findUniqueBankForIbanAsync(enteredIban.toString()) { foundBank ->
context?.asActivity()?.runOnUiThread { context?.asActivity()?.runOnUiThread {
showValuesForFoundBankOnUiThread(foundBank) showValuesForFoundBankOnUiThread(foundBank)
} }
@ -299,72 +314,51 @@ open class TransferMoneyDialog : DialogFragment() {
} }
private fun showValuesForFoundBankOnUiThread(foundBank: BankInfo?) { private fun showValuesForFoundBankOnUiThread(foundBank: BankInfo?) {
foundBankForEnteredIban = foundBank != null validRemitteeBicEntered = foundBank != null
edtxtRemitteeBank.setText(if (foundBank != null) (foundBank.name + " " + foundBank.city) else "") edtxtRemitteeBank.setText(if (foundBank != null) (foundBank.name + " " + foundBank.city) else "")
edtxtRemitteeBic.setText(foundBank?.bic ?: "") // TODO: check if user entered BIC to not overwrite self entered BIC edtxtRemitteeBic.setText(foundBank?.bic ?: "") // TODO: check if user entered BIC to not overwrite self entered BIC
lytRemitteeBic.error = null lytRemitteeBic.error = null // TODO: show information here if BIC hasn't been found
if (foundBankForEnteredIban) {
lytRemitteeIban.error = null
}
checkIfRequiredDataEnteredOnUiThread() checkIfRequiredDataEnteredOnUiThread()
} }
protected open fun checkIfRequiredDataEnteredOnUiThread() { protected open fun checkIfRequiredDataEnteredOnUiThread() {
btnTransferMoney.isEnabled = isRemitteeNameValid() && isRemitteeIbanValid() btnTransferMoney.isEnabled = validRemitteeNameEntered && validRemitteeIbanEntered
&& isRemitteeBicValid() && validRemitteeBicEntered
&& isAmountGreaterZero() && isUsageTextValid() && validAmountEntered && validUsageEntered
} }
protected open fun checkIfEnteredRemitteeNameIsValid() { protected open fun checkIfEnteredRemitteeNameIsValidWhileUserIsTyping() {
if (isRemitteeNameValid()) {
lytRemitteeName.error = null
}
else {
val enteredName = edtxtRemitteeName.text.toString()
if (enteredName.isEmpty()) {
lytRemitteeName.error = context?.getString(R.string.error_no_name_entered)
}
else if (inputValidator.hasRemitteeNameValidLength(enteredName) == false) {
lytRemitteeName.error = context?.getString(R.string.error_entered_name_too_long)
}
else {
lytRemitteeName.error = context?.getString(
R.string.error_invalid_sepa_characters_entered, inputValidator.getInvalidSepaCharacters(enteredName))
}
}
}
protected open fun isRemitteeNameValid(): Boolean {
val enteredRemitteeName = edtxtRemitteeName.text.toString() val enteredRemitteeName = edtxtRemitteeName.text.toString()
val validationResult = inputValidator.validateRemitteeNameWhileTyping(enteredRemitteeName)
return inputValidator.isRemitteeNameValid(enteredRemitteeName) this.validRemitteeNameEntered = validationResult.validationSuccessfulOrCouldCorrectString
showValidationResult(lytRemitteeName, validationResult)
} }
protected open fun checkIfEnteredRemitteeIbanIsValid() { protected open fun checkIfEnteredRemitteeNameIsValidAfterFocusLost() {
val enteredRemitteeName = edtxtRemitteeName.text.toString()
val validationResult = inputValidator.validateRemitteeName(enteredRemitteeName)
this.validRemitteeNameEntered = validationResult.validationSuccessfulOrCouldCorrectString
if (validationResult.validationSuccessful == false) { // only update hint / error if validation fails, don't hide previous hint / error otherwise
showValidationResult(lytRemitteeName, validationResult)
}
}
protected open fun checkIfEnteredRemitteeIbanIsValidWhileUserIsTyping() {
val enteredIban = edtxtRemitteeIban.text.toString() val enteredIban = edtxtRemitteeIban.text.toString()
val validationResult = inputValidator.validateIbanWhileTyping(enteredIban)
if (isRemitteeIbanValid()) { this.validRemitteeIbanEntered = validationResult.validationSuccessfulOrCouldCorrectString
lytRemitteeIban.error = null
}
else if (enteredIban.isBlank()) {
lytRemitteeIban.error = context?.getString(R.string.error_no_iban_entered)
}
else {
val invalidIbanCharacters = inputValidator.getInvalidIbanCharacters(enteredIban)
if (invalidIbanCharacters.isNotEmpty()) {
lytRemitteeIban.error = context?.getString(R.string.error_invalid_iban_characters_entered, invalidIbanCharacters)
}
else {
lytRemitteeIban.error = context?.getString(R.string.error_invalid_iban_pattern_entered)
}
}
if (foundBankForEnteredIban || enteredIban.isBlank()) { showValidationResult(lytRemitteeIban, validationResult)
if (validRemitteeBicEntered || enteredIban.isBlank()) {
lytRemitteeBic.error = null lytRemitteeBic.error = null
} }
else { else {
@ -372,56 +366,31 @@ open class TransferMoneyDialog : DialogFragment() {
} }
} }
protected open fun isRemitteeIbanValid(): Boolean { protected open fun checkIfEnteredRemitteeIbanIsValidAfterFocusLost() {
return inputValidator.isValidIban(edtxtRemitteeIban.text.toString()) val validationResult = inputValidator.validateIban(edtxtRemitteeIban.text.toString())
this.validRemitteeIbanEntered = validationResult.validationSuccessfulOrCouldCorrectString
if (validationResult.validationSuccessful == false) { // only update hint / error if validation fails, don't hide previous hint / error otherwise
showValidationResult(lytRemitteeIban, validationResult)
}
} }
protected open fun checkIfEnteredRemitteeBicIsValid() { protected open fun checkIfEnteredRemitteeBicIsValid() {
if (isRemitteeBicValid()) { val enteredBic = edtxtRemitteeBic.text.toString()
lytRemitteeBic.error = null val validationResult = inputValidator.validateBic(enteredBic)
}
else {
val enteredBic = edtxtRemitteeBic.text.toString()
if (enteredBic.isBlank()) { this.validRemitteeBicEntered = validationResult.validationSuccessfulOrCouldCorrectString
lytRemitteeBic.error = context?.getString(R.string.error_no_bic_entered)
}
else {
val invalidBicCharacters = inputValidator.getInvalidBicCharacters(enteredBic)
if (invalidBicCharacters.isNotEmpty()) {
lytRemitteeBic.error = context?.getString(R.string.error_invalid_bic_characters_entered, invalidBicCharacters)
}
else {
lytRemitteeBic.error = context?.getString(R.string.error_invalid_bic_pattern_entered)
}
}
}
}
protected open fun isRemitteeBicValid(): Boolean { showValidationResult(lytRemitteeBic, validationResult)
return inputValidator.isValidBic(edtxtRemitteeBic.text.toString())
} }
protected open fun checkIfEnteredAmountIsValid() { protected open fun checkIfEnteredAmountIsValid() {
if (isAmountGreaterZero()) { val validationResult = inputValidator.validateAmount(edtxtAmount.text.toString())
lytAmount.error = null
}
else if (edtxtAmount.text.toString().isBlank()) {
lytAmount.error = context?.getString(R.string.error_no_amount_entered)
}
else {
lytAmount.error = context?.getString(R.string.error_invalid_amount_entered)
}
}
protected open fun isAmountGreaterZero(): Boolean { this.validAmountEntered = validationResult.validationSuccessfulOrCouldCorrectString
try {
getEnteredAmount()?.let { amount ->
return amount > BigDecimal.ZERO
}
} catch (ignored: Exception) { }
return false showValidationResult(lytAmount, validationResult)
} }
protected open fun getEnteredAmount(): BigDecimal? { protected open fun getEnteredAmount(): BigDecimal? {
@ -435,22 +404,40 @@ open class TransferMoneyDialog : DialogFragment() {
} }
protected open fun checkIfEnteredUsageTextIsValid() { protected open fun checkIfEnteredUsageTextIsValid() {
val enteredUsage = edtxtUsage.text.toString() val validationResult = inputValidator.validateUsage(edtxtUsage.text.toString())
if (isUsageTextValid()) { this.validUsageEntered = validationResult.validationSuccessfulOrCouldCorrectString
lytUsage.error = null
} showValidationResult(lytUsage, validationResult)
else if (inputValidator.hasUsageValidLength(enteredUsage) == false) {
lytUsage.error = context?.getString(R.string.error_entered_usage_too_long)
}
else {
lytUsage.error = context?.getString(R.string.error_invalid_sepa_characters_entered,
inputValidator.getInvalidSepaCharacters(enteredUsage))
}
} }
protected open fun isUsageTextValid(): Boolean { protected open fun showValidationResult(textInputLayout: TextInputLayout, validationResult: ValidationResult) {
return inputValidator.isUsageValid(edtxtUsage.text.toString()) if (didJustCorrectInput.containsKey(textInputLayout)) { // we have just auto corrected TextInputLayout's EditText's text below, don't overwrite its displayed hints and error
return
}
if (validationResult.didCorrectString) {
textInputLayout.editText?.let { editText ->
val selectionStart = editText.selectionStart
val selectionEnd = editText.selectionEnd
val lengthDiff = validationResult.correctedInputString.length - validationResult.inputString.length
didJustCorrectInput.put(textInputLayout, true)
editText.setText(validationResult.correctedInputString)
if (validationResult.correctedInputString.isNotEmpty()) {
editText.setSelection(selectionStart + lengthDiff, selectionEnd + lengthDiff)
}
didJustCorrectInput.remove(textInputLayout)
}
}
textInputLayout.error = validationResult.validationError
if (validationResult.validationError == null) { // don't overwrite error text
textInputLayout.helperText = validationResult.validationHint
}
} }
} }

View File

@ -2,6 +2,7 @@ package net.dankito.banking.util
import net.dankito.banking.fints.messages.segmente.implementierte.sepa.ISepaMessageCreator import net.dankito.banking.fints.messages.segmente.implementierte.sepa.ISepaMessageCreator
import net.dankito.banking.fints.messages.segmente.implementierte.sepa.SepaMessageCreator import net.dankito.banking.fints.messages.segmente.implementierte.sepa.SepaMessageCreator
import net.dankito.utils.multiplatform.BigDecimal
open class InputValidator { open class InputValidator {
@ -10,6 +11,10 @@ open class InputValidator {
const val RemitteNameMaxLength = 70 const val RemitteNameMaxLength = 70
const val IbanMaxLength = 34
const val BicMaxLength = 11
const val UsageMaxLength = 140 const val UsageMaxLength = 140
@ -58,6 +63,94 @@ open class InputValidator {
protected val sepaMessageCreator: ISepaMessageCreator = SepaMessageCreator() protected val sepaMessageCreator: ISepaMessageCreator = SepaMessageCreator()
open fun validateRemitteeNameWhileTyping(remitteeNameToTest: String): ValidationResult {
return validateRemitteeName(remitteeNameToTest, true)
}
open fun validateRemitteeName(remitteeNameToTest: String): ValidationResult {
return validateRemitteeName(remitteeNameToTest, false)
}
open fun validateRemitteeName(remitteeNameToTest: String, userIsStillTyping: Boolean = false): ValidationResult {
if (isRemitteeNameValid(remitteeNameToTest)) {
return ValidationResult(remitteeNameToTest, true)
}
if (remitteeNameToTest.isEmpty()) {
if (userIsStillTyping) { // if user is still typing, don't check if something has been entered yet
return ValidationResult(remitteeNameToTest, true)
}
return ValidationResult(remitteeNameToTest, false, validationError = "Bitte geben Sie den Namen des Empfängers ein") // TODO: translate
}
if (hasRemitteeNameValidLength(remitteeNameToTest) == false) {
val correctedString = remitteeNameToTest.substring(0, RemitteNameMaxLength)
return ValidationResult(remitteeNameToTest, isRemitteeNameValid(correctedString), true, correctedString, "Name darf maximal 70 Zeichen lang sein") // TODO: translate
}
val invalidRemitteeNameCharacters = getInvalidSepaCharacters(remitteeNameToTest)
val correctedString = getCorrectedString(remitteeNameToTest, invalidRemitteeNameCharacters, true)
return ValidationResult(remitteeNameToTest, isRemitteeNameValid(correctedString), true, correctedString, null, "Unzulässige(s) Zeichen eingegeben: $invalidRemitteeNameCharacters") // TODO: translate
}
open fun isRemitteeNameValid(stringToTest: String): Boolean {
return hasRemitteeNameValidLength(stringToTest)
&& containsOnlyValidSepaCharacters(stringToTest)
}
open fun hasRemitteeNameValidLength(stringToTest: String): Boolean {
return stringToTest.length in 1..RemitteNameMaxLength
}
/**
* Validate entered IBAN while user is still typing. Just checks (and corrects) if invalid
* characters have been entered or string is too long.
*
* Doesn't check yet if entered text has the correct pattern, min length etc. as user may is
* just about to enter this information.
*/
open fun validateIbanWhileTyping(ibanToTest: String): ValidationResult {
return validateIban(ibanToTest, true)
}
open fun validateIban(ibanToTest: String): ValidationResult {
return validateIban(ibanToTest, false)
}
protected open fun validateIban(ibanToTest: String, userIsStillTyping: Boolean = false): ValidationResult {
if (isValidIban(ibanToTest)) {
return ValidationResult(ibanToTest, true)
}
if (ibanToTest.isBlank()) {
if (userIsStillTyping) { // if user is still typing, don't check if something has been entered yet
return ValidationResult(ibanToTest, true)
}
return ValidationResult(ibanToTest, false, validationError = "Bitte geben Sie die IBAN des Empfängers ein") // TODO: translate
}
if (ibanToTest.length > IbanMaxLength) {
val correctedString = ibanToTest.substring(0, IbanMaxLength)
return ValidationResult(ibanToTest, isValidIban(correctedString), true, correctedString, null, "Eine IBAN darf maximal 34 Zeichen lang sein") // TODO: translate // TODO: may test country specific IBAN length, e.g. German IBANs have 22 charactersaa
}
val invalidIbanCharacters = getInvalidIbanCharacters(ibanToTest)
if (invalidIbanCharacters.isNotEmpty()) {
val correctedString = getCorrectedString(ibanToTest, invalidIbanCharacters)
return ValidationResult(ibanToTest, isValidIban(correctedString), true, correctedString, null, "Unzulässige(s) Zeichen eingegeben: $invalidIbanCharacters") // TODO: translate
}
else if (userIsStillTyping) { // entered IBAN hasn't required pattern yet but that's ok as user is may just about to provide that information
return ValidationResult(ibanToTest, true)
}
else {
return ValidationResult(ibanToTest, false, validationError = "IBANs haben folgendes Muster: DE12 1234 5678 9012 3456 78") // TODO: translate
}
}
open fun isValidIban(stringToTest: String): Boolean { open fun isValidIban(stringToTest: String): Boolean {
return IbanPattern.matches(stringToTest.replace(" ", "")) return IbanPattern.matches(stringToTest.replace(" ", ""))
} }
@ -67,6 +160,31 @@ open class InputValidator {
} }
open fun validateBic(bicToTest: String): ValidationResult {
if (isValidBic(bicToTest)) {
return ValidationResult(bicToTest, true)
}
else {
if (bicToTest.isBlank()) {
return ValidationResult(bicToTest, false, validationError = "Bitte geben Sie die BIC des Empfängers ein") // TODO: translate
}
else if (bicToTest.length > BicMaxLength) {
val correctedString = bicToTest.substring(0, BicMaxLength)
return ValidationResult(bicToTest, isValidBic(correctedString), true, correctedString, null, "Eine IBAN darf maximal 11 Zeichen lang sein") // TODO: translate // TODO: may test country specific IBAN length, e.g. German IBANs have 22 charactersaa
}
else {
val invalidBicCharacters = getInvalidBicCharacters(bicToTest)
if (invalidBicCharacters.isNotEmpty()) {
val correctedString = getCorrectedString(bicToTest, invalidBicCharacters)
return ValidationResult(bicToTest, isValidBic(correctedString), true, correctedString, null, "Unzulässige(s) Zeichen eingegeben: $invalidBicCharacters") // TODO: translate
}
else {
return ValidationResult(bicToTest, false, validationError = "Eine BIC besteht aus 8 oder 11 Zeichen und folgt dem Muster: ABCDED12(XYZ)") // TODO: translate
}
}
}
}
open fun isValidBic(stringToTest: String): Boolean { open fun isValidBic(stringToTest: String): Boolean {
return BicPattern.matches(stringToTest) return BicPattern.matches(stringToTest)
} }
@ -76,23 +194,50 @@ open class InputValidator {
} }
open fun isRemitteeNameValid(stringToTest: String): Boolean { open fun validateAmount(enteredAmountString: String): ValidationResult {
val convertedString = convertToAllowedSepaCharacters(stringToTest) if (enteredAmountString.isBlank()) {
return ValidationResult(enteredAmountString, false, validationError = "Bitte geben Sie den zu überweisenden Betrag ein") // TODO: translate
}
return hasRemitteeNameValidLength(convertedString) convertAmountString(enteredAmountString)?.let { amount ->
&& containsOnlyValidSepaCharacters(convertedString) if (amount.isPositive && amount != BigDecimal.Zero) {
return ValidationResult(enteredAmountString, true)
}
}
return ValidationResult(enteredAmountString, false, validationError = "Bitte geben Sie einen Betrag größer 0 ein.") // TODO: translate
} }
open fun hasRemitteeNameValidLength(stringToTest: String): Boolean { open fun convertAmountString(enteredAmountString: String): BigDecimal? {
return stringToTest.length in 1..RemitteNameMaxLength try {
val amountString = enteredAmountString.replace(',', '.')
return BigDecimal(amountString)
} catch (ignored: Exception) { }
return null
} }
open fun validateUsage(usageToTest: String): ValidationResult {
if (isUsageValid(usageToTest)) {
return ValidationResult(usageToTest, true)
}
if (hasUsageValidLength(usageToTest) == false) {
val correctedString = usageToTest.substring(0, UsageMaxLength)
return ValidationResult(usageToTest, isUsageValid(correctedString), true, correctedString, "Verwendungszweck darf nur 140 Zeichen lang sein") // TODO: translate
}
val invalidUsageCharacters = getInvalidSepaCharacters(usageToTest)
val correctedString = getCorrectedString(usageToTest, invalidUsageCharacters, true)
return ValidationResult(usageToTest, isUsageValid(correctedString), true, correctedString, null, "Unzulässige(s) Zeichen eingegeben: $invalidUsageCharacters") // TODO: translate return ValidationResult(remitteeNameToTest, false, validationError = "Unzulässige(s) Zeichen eingegeben: ") // TODO: translate
}
open fun isUsageValid(stringToTest: String): Boolean { open fun isUsageValid(stringToTest: String): Boolean {
val convertedString = convertToAllowedSepaCharacters(stringToTest) return hasUsageValidLength(stringToTest)
&& containsOnlyValidSepaCharacters(stringToTest)
return hasUsageValidLength(convertedString)
&& containsOnlyValidSepaCharacters(convertedString)
} }
open fun hasUsageValidLength(stringToTest: String): Boolean { open fun hasUsageValidLength(stringToTest: String): Boolean {
@ -105,7 +250,7 @@ open class InputValidator {
} }
open fun getInvalidSepaCharacters(string: String): String { open fun getInvalidSepaCharacters(string: String): String {
return getInvalidCharacters(convertToAllowedSepaCharacters(string), InvalidSepaCharactersPattern) return getInvalidCharacters(string, InvalidSepaCharactersPattern)
} }
open fun convertToAllowedSepaCharacters(string: String): String { open fun convertToAllowedSepaCharacters(string: String): String {
@ -114,7 +259,19 @@ open class InputValidator {
open fun getInvalidCharacters(string: String, pattern: Regex): String { open fun getInvalidCharacters(string: String, pattern: Regex): String {
return pattern.findAll(string).joinToString("") return pattern.findAll(string).map { it.value }.joinToString("")
}
// TODO: do not convert XML entities in user's. User will a) not understand what happened and b) afterwards auto correction will not work anymore (i think the issue lies in used Regex: '(&\w{2,4};)').
// But take converted XML entities length into account when checking if remittee's name and usage length isn't too long
protected open fun getCorrectedString(inputString: String, invalidCharacters: String, convertToAllowedSepaCharacters: Boolean = false): String {
var correctedString = if (convertToAllowedSepaCharacters) convertToAllowedSepaCharacters(inputString) else inputString
invalidCharacters.forEach { invalidChar ->
correctedString = correctedString.replace(invalidChar.toString(), "")
}
return correctedString
} }
} }

View File

@ -0,0 +1,15 @@
package net.dankito.banking.util
open class ValidationResult(
open val inputString: String,
open val validationSuccessful: Boolean,
open val didCorrectString: Boolean = false,
open val correctedInputString: String = inputString,
open val validationError: String? = null,
open val validationHint: String? = null
) {
open val validationSuccessfulOrCouldCorrectString: Boolean = validationSuccessful || didCorrectString
}

View File

@ -0,0 +1,687 @@
package net.dankito.banking.util
import ch.tutteli.atrium.api.fluent.en_GB.notToBeNull
import ch.tutteli.atrium.api.fluent.en_GB.toBe
import ch.tutteli.atrium.api.verbs.expect
import org.junit.Test
class InputValidatorTest {
companion object {
const val ValidRemitteeName = "Marieke Musterfrau"
const val ValidIban = "DE11123456780987654321"
const val ValidBic = "ABCDDEBBXXX"
const val ValidUsage = "Usage"
const val InvalidSepaCharacter = "!"
const val InvalidUmlaut = "ö"
const val ConvertedInvalidUmlaut = "o"
}
private val underTest = InputValidator()
@Test
fun getInvalidIbanCharacters() {
// given
val invalidIbanCharacters = "ajvz!@#$%^&*()-_=+[]{}'\"\\|/?.,;:<>"
// when
val result = underTest.getInvalidIbanCharacters("EN${invalidIbanCharacters}1234")
// then
expect(result).toBe(invalidIbanCharacters)
}
@Test
fun getInvalidSepaCharacters() {
// given
val invalidSepaCharacters = "!€@#$%^*=[]\\|<>"
// when
val result = underTest.getInvalidSepaCharacters("abcd${invalidSepaCharacters}1234")
// then
expect(result).toBe(invalidSepaCharacters)
}
@Test
fun validateRemitteeName_EmptyStringEntered() {
// given
val enteredName = ""
// when
val result = underTest.validateRemitteeName(enteredName)
// then
expect(result.validationSuccessful).toBe(false)
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredName)
expect(result.correctedInputString).toBe(enteredName)
expect(result.validationHint).toBe(null)
expect(result.validationError).notToBeNull()
}
@Test
fun validateRemitteeName_ValidNameEntered() {
// given
val enteredName = ValidRemitteeName
// when
val result = underTest.validateRemitteeName(enteredName)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredName)
expect(result.correctedInputString).toBe(enteredName)
expect(result.validationHint).toBe(null)
expect(result.validationError).toBe(null)
}
@Test
fun validateRemitteeName_UmlautGetsConverted() {
// given
val enteredName = ValidRemitteeName + InvalidUmlaut
// when
val result = underTest.validateRemitteeName(enteredName)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredName)
expect(result.correctedInputString).toBe(ValidRemitteeName + ConvertedInvalidUmlaut)
expect(result.validationHint?.contains(InvalidUmlaut)).toBe(true)
expect(result.validationError).toBe(null)
}
@Test
fun validateRemitteeName_InvalidCharacterGetsRemoved() {
// given
val enteredName = ValidRemitteeName + InvalidSepaCharacter
// when
val result = underTest.validateRemitteeName(enteredName)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredName)
expect(result.correctedInputString).toBe(ValidRemitteeName)
expect(result.validationHint?.contains(InvalidSepaCharacter)).toBe(true)
expect(result.validationError).toBe(null)
}
@Test
fun validateRemitteeName_TooLong() {
// given
val nameWithMaxLength = IntRange(0, InputValidator.RemitteNameMaxLength - 1).map { "a" }.joinToString("")
val enteredName = nameWithMaxLength + "a"
// when
val result = underTest.validateRemitteeName(enteredName)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredName)
expect(result.correctedInputString).toBe(nameWithMaxLength)
expect(result.validationHint).toBe(null)
expect(result.validationError).notToBeNull()
}
@Test
fun validateIban_EmptyStringEntered() {
// given
val enteredIban = ""
// when
val result = underTest.validateIban(enteredIban)
// then
expect(result.validationSuccessful).toBe(false)
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredIban)
expect(result.correctedInputString).toBe(enteredIban)
expect(result.validationHint).toBe(null)
expect(result.validationError).notToBeNull()
}
@Test
fun validateIban_ValidIbanEntered() {
// given
val enteredIban = ValidIban
// when
val result = underTest.validateIban(enteredIban)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredIban)
expect(result.correctedInputString).toBe(enteredIban)
expect(result.validationHint).toBe(null)
expect(result.validationError).toBe(null)
}
@Test
fun validateIban_IbanTooShort() {
// given
val enteredIban = "DE11"
// when
val result = underTest.validateIban(enteredIban)
// then
expect(result.validationSuccessful).toBe(false)
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredIban)
expect(result.correctedInputString).toBe(enteredIban)
expect(result.validationHint).toBe(null)
expect(result.validationError).notToBeNull()
}
@Test
fun validateIban_UmlautGetsRemoved() {
// given
val enteredIban = ValidIban + InvalidUmlaut
// when
val result = underTest.validateIban(enteredIban)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredIban)
expect(result.correctedInputString).toBe(ValidIban)
expect(result.validationHint?.contains(InvalidUmlaut)).toBe(true)
expect(result.validationError).toBe(null)
}
@Test
fun validateIban_InvalidCharacterGetsRemoved() {
// given
val enteredIban = ValidIban + InvalidSepaCharacter
// when
val result = underTest.validateIban(enteredIban)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredIban)
expect(result.correctedInputString).toBe(ValidIban)
expect(result.validationHint?.contains(InvalidSepaCharacter)).toBe(true)
expect(result.validationError).toBe(null)
}
@Test
fun validateIban_TooLong() {
// given
val ibanWithMaxLength = IntRange(0, InputValidator.IbanMaxLength - 1).map { "1" }.joinToString("")
val enteredIban = ibanWithMaxLength + "1"
// when
val result = underTest.validateIban(enteredIban)
// then
expect(result.validationSuccessful).toBe(false)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredIban)
expect(result.correctedInputString).toBe(ibanWithMaxLength)
expect(result.validationHint).notToBeNull()
expect(result.validationError).toBe(null)
}
@Test
fun validateIbanWhileTyping_EmptyStringEntered() {
// given
val enteredIban = ""
// when
val result = underTest.validateIbanWhileTyping(enteredIban)
// then
expect(result.validationSuccessful).toBe(true) // while user is typing an empty string is ok
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredIban)
expect(result.correctedInputString).toBe(enteredIban)
expect(result.validationHint).toBe(null)
expect(result.validationError).toBe(null)
}
@Test
fun validateIbanWhileTyping_ValidIbanEntered() {
// given
val enteredIban = ValidIban
// when
val result = underTest.validateIbanWhileTyping(enteredIban)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredIban)
expect(result.correctedInputString).toBe(enteredIban)
expect(result.validationHint).toBe(null)
expect(result.validationError).toBe(null)
}
@Test
fun validateIbanWhileTyping_IbanTooShort() {
// given
val enteredIban = "DE11"
// when
val result = underTest.validateIbanWhileTyping(enteredIban)
// then
expect(result.validationSuccessful).toBe(true) // while user is typing an incomplete IBAN is ok
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredIban)
expect(result.correctedInputString).toBe(enteredIban)
expect(result.validationHint).toBe(null)
expect(result.validationError).toBe(null)
}
@Test
fun validateIbanWhileTyping_UmlautGetsRemoved() {
// given
val enteredIban = ValidIban + InvalidUmlaut
// when
val result = underTest.validateIbanWhileTyping(enteredIban)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredIban)
expect(result.correctedInputString).toBe(ValidIban)
expect(result.validationHint?.contains(InvalidUmlaut)).toBe(true)
expect(result.validationError).toBe(null)
}
@Test
fun validateIbanWhileTyping_InvalidCharacterGetsRemoved() {
// given
val enteredIban = ValidIban + InvalidSepaCharacter
// when
val result = underTest.validateIbanWhileTyping(enteredIban)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredIban)
expect(result.correctedInputString).toBe(ValidIban)
expect(result.validationHint?.contains(InvalidSepaCharacter)).toBe(true)
expect(result.validationError).toBe(null)
}
@Test
fun validateIbanWhileTyping_TooLong() {
// given
val ibanWithMaxLength = IntRange(0, InputValidator.IbanMaxLength - 1).map { "1" }.joinToString("")
val enteredIban = ibanWithMaxLength + "1"
// when
val result = underTest.validateIbanWhileTyping(enteredIban)
// then
expect(result.validationSuccessful).toBe(false)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredIban)
expect(result.correctedInputString).toBe(ibanWithMaxLength)
expect(result.validationHint).notToBeNull()
expect(result.validationError).toBe(null)
}
@Test
fun validateBic_EmptyStringEntered() {
// given
val enteredBic = ""
// when
val result = underTest.validateBic(enteredBic)
// then
expect(result.validationSuccessful).toBe(false)
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredBic)
expect(result.correctedInputString).toBe(enteredBic)
expect(result.validationHint).toBe(null)
expect(result.validationError).notToBeNull()
}
@Test
fun validateBic_ValidBicEntered() {
// given
val enteredBic = ValidBic
// when
val result = underTest.validateBic(enteredBic)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredBic)
expect(result.correctedInputString).toBe(enteredBic)
expect(result.validationHint).toBe(null)
expect(result.validationError).toBe(null)
}
@Test
fun validateBic_BicTooShort() {
// given
val enteredBic = "ABCD"
// when
val result = underTest.validateBic(enteredBic)
// then
expect(result.validationSuccessful).toBe(false)
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredBic)
expect(result.correctedInputString).toBe(enteredBic)
expect(result.validationHint).toBe(null)
expect(result.validationError).notToBeNull()
}
@Test
fun validateBic_UmlautGetsRemoved() {
// given
val bicWithoutLastPlace = ValidBic.substring(0, ValidBic.length - 1)
val enteredBic = bicWithoutLastPlace + InvalidUmlaut
// when
val result = underTest.validateBic(enteredBic)
// then
expect(result.validationSuccessful).toBe(false)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredBic)
expect(result.correctedInputString).toBe(bicWithoutLastPlace)
expect(result.validationHint?.contains(InvalidUmlaut)).toBe(true)
expect(result.validationError).toBe(null)
}
@Test
fun validateBic_InvalidCharacterGetsRemoved() {
// given
val bicWithoutLastPlace = ValidBic.substring(0, ValidBic.length - 1)
val enteredBic = bicWithoutLastPlace + InvalidSepaCharacter
// when
val result = underTest.validateBic(enteredBic)
// then
expect(result.validationSuccessful).toBe(false)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredBic)
expect(result.correctedInputString).toBe(bicWithoutLastPlace)
expect(result.validationHint?.contains(InvalidSepaCharacter)).toBe(true)
expect(result.validationError).toBe(null)
}
@Test
fun validateBic_TooLong() {
// given
val bicWithMaxLength = IntRange(0, InputValidator.BicMaxLength - 1).map { "A" }.joinToString("")
val enteredBic = bicWithMaxLength + "A"
// when
val result = underTest.validateBic(enteredBic)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredBic)
expect(result.correctedInputString).toBe(bicWithMaxLength)
expect(result.validationHint).notToBeNull()
expect(result.validationError).toBe(null)
}
@Test
fun validateUsage_EmptyStringEntered() {
// given
val enteredUsage = ""
// when
val result = underTest.validateUsage(enteredUsage)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredUsage)
expect(result.correctedInputString).toBe(enteredUsage)
expect(result.validationHint).toBe(null)
expect(result.validationError).toBe(null)
}
@Test
fun validateUsage_ValidUsageEntered() {
// given
val enteredUsage = ValidUsage
// when
val result = underTest.validateUsage(enteredUsage)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredUsage)
expect(result.correctedInputString).toBe(enteredUsage)
expect(result.validationHint).toBe(null)
expect(result.validationError).toBe(null)
}
@Test
fun validateUsage_UmlautGetsConverted() {
// given
val enteredUsage = ValidUsage + InvalidUmlaut
// when
val result = underTest.validateUsage(enteredUsage)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredUsage)
expect(result.correctedInputString).toBe(ValidUsage + ConvertedInvalidUmlaut)
expect(result.validationHint?.contains(InvalidUmlaut)).toBe(true)
expect(result.validationError).toBe(null)
}
@Test
fun validateUsage_InvalidCharacterGetsRemoved() {
// given
val enteredUsage = ValidUsage + InvalidSepaCharacter
// when
val result = underTest.validateUsage(enteredUsage)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredUsage)
expect(result.correctedInputString).toBe(ValidUsage)
expect(result.validationHint?.contains(InvalidSepaCharacter)).toBe(true)
expect(result.validationError).toBe(null)
}
// TODO: does not work yet
@Test
fun validateUsage_AmpersandGetsRemoved() {
// given
val invalidSepaCharacter = "&"
val enteredUsage = ValidUsage + invalidSepaCharacter
// when
val result = underTest.validateUsage(enteredUsage)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredUsage)
expect(result.correctedInputString).toBe(ValidUsage)
expect(result.validationHint?.contains(invalidSepaCharacter)).toBe(true)
expect(result.validationError).toBe(null)
}
// TODO: does not work yet
@Test
fun validateUsage_EnteringACharacterAfterConvertingAXmlEntityDoesNotFail() {
// given
val convertedXmlEntity = "&amp;"
val validSepaCharacter = "h"
val enteredUsage = ValidUsage + convertedXmlEntity + validSepaCharacter
// when
val result = underTest.validateUsage(enteredUsage)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredUsage)
expect(result.correctedInputString).toBe(ValidUsage + convertedXmlEntity + validSepaCharacter)
expect(result.validationHint).toBe(null)
expect(result.validationError).toBe(null)
}
@Test
fun validateUsage_TooLong() {
// given
val usageWithMaxLength = IntRange(0, InputValidator.UsageMaxLength - 1).map { "a" }.joinToString("")
val enteredUsage = usageWithMaxLength + "a"
// when
val result = underTest.validateUsage(enteredUsage)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(true)
expect(result.inputString).toBe(enteredUsage)
expect(result.correctedInputString).toBe(usageWithMaxLength)
expect(result.validationHint).toBe(null)
expect(result.validationError).notToBeNull()
}
@Test
fun validateAmount_EmptyStringEntered() {
// given
val enteredAmount = ""
// when
val result = underTest.validateAmount(enteredAmount)
// then
expect(result.validationSuccessful).toBe(false)
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredAmount)
expect(result.correctedInputString).toBe(enteredAmount)
expect(result.validationHint).toBe(null)
expect(result.validationError).notToBeNull()
}
@Test
fun validateAmount_ValidAmountEntered() {
// given
val enteredAmount = "84,25"
// when
val result = underTest.validateAmount(enteredAmount)
// then
expect(result.validationSuccessful).toBe(true)
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredAmount)
expect(result.correctedInputString).toBe(enteredAmount)
expect(result.validationHint).toBe(null)
expect(result.validationError).toBe(null)
}
@Test
fun validateAmount_ZeroEntered() {
// given
val enteredAmount = "0"
// when
val result = underTest.validateAmount(enteredAmount)
// then
expect(result.validationSuccessful).toBe(false)
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredAmount)
expect(result.correctedInputString).toBe(enteredAmount)
expect(result.validationHint).toBe(null)
expect(result.validationError).notToBeNull()
}
@Test
fun validateAmount_NegativeAmountEntered() {
// given
val enteredAmount = "-84,25"
// when
val result = underTest.validateAmount(enteredAmount)
// then
expect(result.validationSuccessful).toBe(false)
expect(result.didCorrectString).toBe(false)
expect(result.inputString).toBe(enteredAmount)
expect(result.correctedInputString).toBe(enteredAmount)
expect(result.validationHint).toBe(null)
expect(result.validationError).notToBeNull()
}
}

View File

@ -1,38 +0,0 @@
package net.dankito.banking.util
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class InputValidatorTest {
private val underTest = InputValidator()
@Test
fun getInvalidIbanCharacters() {
// given
val invalidIbanCharacters = "ajvz!@#$%^&*()-_=+[]{}'\"\\|/?.,;:<>"
// when
val result = underTest.getInvalidIbanCharacters("EN${invalidIbanCharacters}1234")
// then
assertThat(result).isEqualTo(invalidIbanCharacters as Any)
}
@Test
fun getInvalidSepaCharacters() {
// given
val invalidSepaCharacters = "!€@#$%^&*_=[]{}\\|;<>"
// when
val result = underTest.getInvalidSepaCharacters("abcd${invalidSepaCharacters}1234")
// then
assertThat(result).isEqualTo(invalidSepaCharacters as Any)
}
}

View File

@ -0,0 +1,57 @@
import SwiftUI
import BankingUiSwift
struct ValidationLabel: View {
private let validationErrorOrHint: String
private let isHint: Bool
init(_ validationError: String) {
self.init(validationError, false)
}
init(_ validationResult: ValidationResult) {
self.init(validationResult.validationError ?? validationResult.validationHint ?? "",
validationResult.validationError == nil && validationResult.validationHint != nil)
}
init(_ validationErrorOrHint: String, _ isHint: Bool) {
self.validationErrorOrHint = validationErrorOrHint
self.isHint = isHint
}
var body: some View {
VStack {
Spacer()
.frame(height: 6)
HStack {
Text(validationErrorOrHint)
.padding(.leading, 16)
Spacer()
}
Spacer()
.frame(height: 18)
}
.font(.callout)
.foregroundColor(isHint ? Color.yellow : Color.red)
.systemGroupedBackground()
.listRowInsets(EdgeInsets())
}
}
struct ValidationLabel_Previews: PreviewProvider {
static var previews: some View {
ValidationLabel("Invalid characters used")
}
}

View File

@ -16,35 +16,44 @@ struct TransferMoneyDialog: View {
@State private var remitteeName: String = "" @State private var remitteeName: String = ""
@State private var isValidRemitteeNameEntered = false @State private var isValidRemitteeNameEntered = false
@State private var remitteeNameValidationResult: ValidationResult? = nil
@State private var showRemitteeAutocompleteList = false @State private var showRemitteeAutocompleteList = false
@State private var remitteeSearchResults = [Remittee]() @State private var remitteeSearchResults = [Remittee]()
@State private var remitteeIban: String = "" @State private var remitteeIban: String = ""
@State private var isValidRemitteeIbanEntered = false @State private var isValidRemitteeIbanEntered = false
@State private var remitteeIbanValidationResult: ValidationResult? = nil
@State private var remitteeBic: String = "" @State private var remitteeBic: String = ""
@State private var isValidRemitteeBicEntered = false @State private var isValidRemitteeBicEntered = false
@State private var remitteeBicValidationResult: ValidationResult? = nil
@State private var amount = "" @State private var amount = ""
@State private var isValidAmountEntered = false @State private var isValidAmountEntered = false
@State private var amountValidationResult: ValidationResult? = nil
@State private var usage: String = "" @State private var usage: String = ""
@State private var isValidUsageEntered = true @State private var isValidUsageEntered = true
@State private var usageValidationResult: ValidationResult? = nil
@State private var instantPayment = false @State private var instantPayment = false
@State private var transferMoneyResponseMessage: Message? = nil @State private var transferMoneyResponseMessage: Message? = nil
private let inputValidator = InputValidator()
@State private var didJustCorrectEnteredValue = false
private var account: BankAccount? { private var account: BankAccount? {
if (self.selectedAccountIndex < self.accountsSupportingTransferringMoney.count) { if (self.selectedAccountIndex < self.accountsSupportingTransferringMoney.count) {
return self.accountsSupportingTransferringMoney[selectedAccountIndex] return self.accountsSupportingTransferringMoney[selectedAccountIndex]
} }
return self.accountsSupportingTransferringMoney.first return self.accountsSupportingTransferringMoney.first
} }
private var supportsInstantPayment: Bool { private var supportsInstantPayment: Bool {
return self.account?.supportsInstantPaymentMoneyTransfer ?? false return self.account?.supportsInstantPaymentMoneyTransfer ?? false
} }
@ -95,9 +104,12 @@ struct TransferMoneyDialog: View {
} }
Section { Section {
UIKitTextField("Remittee Name", text: $remitteeName, focusOnStart: true, focusNextTextFieldOnReturnKeyPress: true, actionOnReturnKeyPress: handleReturnKeyPress) { newValue in LabelledUIKitTextField(label: "Remittee Name", text: $remitteeName, focusOnStart: true, focusNextTextFieldOnReturnKeyPress: true,
self.isValidRemitteeNameEntered = self.remitteeName.isNotBlank isFocussedChanged: validateRemitteeNameOnFocusLost, actionOnReturnKeyPress: handleReturnKeyPress, textChanged: validateRemitteeName)
}
remitteeNameValidationResult.map { validationError in
ValidationLabel(validationError)
}
if self.showRemitteeAutocompleteList { if self.showRemitteeAutocompleteList {
Section { Section {
@ -109,26 +121,26 @@ struct TransferMoneyDialog: View {
} }
} }
UIKitTextField("Remittee IBAN", text: $remitteeIban, focusNextTextFieldOnReturnKeyPress: true, actionOnReturnKeyPress: handleReturnKeyPress) { newValue in LabelledUIKitTextField(label: "Remittee IBAN", text: $remitteeIban, focusNextTextFieldOnReturnKeyPress: true, isFocussedChanged: validateRemitteeIbanOnFocusLost,
self.isValidRemitteeIbanEntered = newValue.count > 14 // TODO: implement real check if IBAN is valid actionOnReturnKeyPress: handleReturnKeyPress, textChanged: validateRemitteeIban)
self.tryToGetBicFromIban(newValue)
} remitteeIbanValidationResult.map { validationError in
ValidationLabel(validationError)
}
} }
Section { Section {
UIKitTextField("Amount", text: $amount, keyboardType: .decimalPad, focusNextTextFieldOnReturnKeyPress: true, actionOnReturnKeyPress: handleReturnKeyPress) { newValue in LabelledUIKitTextField(label: "Amount", text: $amount, keyboardType: .decimalPad, focusNextTextFieldOnReturnKeyPress: true, actionOnReturnKeyPress: handleReturnKeyPress, textChanged: validateAmount)
// TODO: implement DecimalTextField / NumericTextField
let filtered = newValue.filter { "0123456789,".contains($0) } amountValidationResult.map { validationError in
if filtered != newValue { ValidationLabel(validationError)
self.amount = filtered }
}
self.isValidAmountEntered = self.amount.isNotBlank
}
UIKitTextField("Usage", text: $usage, actionOnReturnKeyPress: handleReturnKeyPress) { newValue in LabelledUIKitTextField(label: "Usage", text: $usage, actionOnReturnKeyPress: handleReturnKeyPress, textChanged: validateUsage)
self.isValidUsageEntered = true
} usageValidationResult.map { validationError in
ValidationLabel(validationError)
}
} }
if supportsInstantPayment { if supportsInstantPayment {
@ -169,9 +181,36 @@ struct TransferMoneyDialog: View {
return false return false
} }
private func validateRemitteeName(enteredRemitteeName: String) {
validateField($remitteeName, $remitteeNameValidationResult, $isValidRemitteeNameEntered) {
inputValidator.validateRemitteeNameWhileTyping(remitteeNameToTest: remitteeName)
}
}
private func validateRemitteeNameOnFocusLost(_ isFocussed: Bool) {
if isFocussed == false {
validateField($remitteeName, $remitteeNameValidationResult, $isValidRemitteeNameEntered) {
inputValidator.validateRemitteeName(remitteeNameToTest: remitteeName)
}
}
}
func tryToGetBicFromIban(_ enteredIban: String) { private func validateRemitteeIban(_ enteredIban: String) {
validateField($remitteeIban, $remitteeIbanValidationResult, $isValidRemitteeIbanEntered) { inputValidator.validateIbanWhileTyping(ibanToTest: enteredIban) }
tryToGetBicFromIban(enteredIban)
}
private func validateRemitteeIbanOnFocusLost(_ isFocussed: Bool) {
if isFocussed == false {
validateField($remitteeIban, $remitteeIbanValidationResult, $isValidRemitteeIbanEntered) { inputValidator.validateIban(ibanToTest: remitteeIban) }
}
}
private func tryToGetBicFromIban(_ enteredIban: String) {
let foundBank = presenter.findUniqueBankForIban(iban: enteredIban) let foundBank = presenter.findUniqueBankForIban(iban: enteredIban)
if let foundBank = foundBank { if let foundBank = foundBank {
@ -186,7 +225,50 @@ struct TransferMoneyDialog: View {
} }
func isRequiredDataEntered() -> Bool { private func validateAmount(_ enteredAmount: String) {
// TODO: implement DecimalTextField / NumericTextField
let filtered = enteredAmount.filter { "0123456789,".contains($0) }
if filtered != enteredAmount {
self.amount = filtered
return // don't validate field after non decimal character has been entered
}
if amount.isNotBlank {
validateField($amount, $amountValidationResult, $isValidAmountEntered) { inputValidator.validateAmount(enteredAmountString: enteredAmount) }
}
else {
isValidAmountEntered = false
amountValidationResult = nil
}
}
private func validateUsage(enteredUsage: String) {
validateField($usage, $usageValidationResult, $isValidUsageEntered) { inputValidator.validateUsage(usageToTest: enteredUsage) }
}
private func validateField(_ newValue: Binding<String>, _ validationResult: Binding<ValidationResult?>, _ isValidValueEntered: Binding<Bool>, _ validateValue: () -> ValidationResult) {
if (didJustCorrectEnteredValue == false) {
let fieldValidationResult = validateValue()
isValidValueEntered.wrappedValue = fieldValidationResult.validationSuccessfulOrCouldCorrectString
validationResult.wrappedValue = fieldValidationResult.didCorrectString || fieldValidationResult.validationSuccessful == false ? fieldValidationResult : nil
if (fieldValidationResult.didCorrectString) {
didJustCorrectEnteredValue = true
newValue.wrappedValue = fieldValidationResult.correctedInputString
}
}
else {
didJustCorrectEnteredValue = false
}
}
private func isRequiredDataEntered() -> Bool {
return account != nil return account != nil
&& isValidRemitteeNameEntered && isValidRemitteeNameEntered
&& isValidRemitteeIbanEntered && isValidRemitteeIbanEntered
@ -195,15 +277,17 @@ struct TransferMoneyDialog: View {
&& isValidUsageEntered && isValidUsageEntered
} }
func transferMoney() { private func transferMoney() {
let data = TransferMoneyData(creditorName: remitteeName, creditorIban: remitteeIban, creditorBic: remitteeBic, amount: CommonBigDecimal(decimal: amount.replacingOccurrences(of: ",", with: ".")), usage: usage, instantPayment: instantPayment) if let amount = inputValidator.convertAmountString(enteredAmountString: self.amount) {
let data = TransferMoneyData(creditorName: remitteeName, creditorIban: remitteeIban, creditorBic: remitteeBic, amount: amount, usage: usage, instantPayment: instantPayment)
presenter.transferMoneyAsync(bankAccount: account!, data: data) { response in
self.handleTransferMoneyResponse(data, response) presenter.transferMoneyAsync(bankAccount: account!, data: data) { response in
self.handleTransferMoneyResponse(data, response)
}
} }
} }
func handleTransferMoneyResponse(_ data: TransferMoneyData, _ response: BankingClientResponse) { private func handleTransferMoneyResponse(_ data: TransferMoneyData, _ response: BankingClientResponse) {
if (response.isSuccessful) { if (response.isSuccessful) {
self.transferMoneyResponseMessage = Message(message: Text("Successfully transferred \(data.amount) \("") to \(data.creditorName)"), primaryButton: .ok { self.transferMoneyResponseMessage = Message(message: Text("Successfully transferred \(data.amount) \("") to \(data.creditorName)"), primaryButton: .ok {
self.presentation.wrappedValue.dismiss() self.presentation.wrappedValue.dismiss()