Implemented showing validation errors on UI

This commit is contained in:
dankito 2020-04-23 03:03:37 +02:00
parent 88ae4cb045
commit 9d10078db1
7 changed files with 314 additions and 80 deletions

View File

@ -20,4 +20,8 @@ dependencies {
// TODO: try to get rid of this import
api project(':fints4javaLib')
testImplementation "junit:junit:$junitVersion"
testImplementation "org.assertj:assertj-core:$assertJVersion"
}

View File

@ -0,0 +1,86 @@
package net.dankito.banking.util
import net.dankito.fints.messages.segmente.implementierte.sepa.ISepaMessageCreator
import net.dankito.fints.messages.segmente.implementierte.sepa.SepaMessageCreator
import java.util.regex.Pattern
open class InputValidator {
companion object {
/**
* The IBAN consists of up to 34 alphanumeric characters, as follows:
* - country code using ISO 3166-1 alpha-2 two letters,
* - check digits two digits, and
* - Basic Bank Account Number (BBAN) up to 30 alphanumeric characters that are country-specific.
* (https://en.wikipedia.org/wiki/International_Bank_Account_Number#Structure)
*/
const val IbanPatternString = "[A-Z]{2}\\d{2}[A-Z0-9]{10,30}"
val IbanPattern = Pattern.compile("^" + IbanPatternString + "\$")
/**
* The IBAN should not contain spaces when transmitted electronically. When printed it is expressed in groups
* of four characters separated by a single space, the last group being of variable length as shown in the example below
* (https://en.wikipedia.org/wiki/International_Bank_Account_Number#Structure)
*/
const val IbanWithSpacesPatternString = "[A-Z]{2}\\d{2}\\s([A-Z0-9]{4}\\s){3}[A-Z0-9\\s]{1,18}"
val IbanWithSpacesPattern = Pattern.compile("^" + IbanWithSpacesPatternString + "\$")
val InvalidIbanCharactersPattern = Pattern.compile("[^A-Z0-9 ]")
/**
* The SWIFT code is 8 or 11 characters, made up of:
* - 4 letters: institution code or bank code.
* - 2 letters: ISO 3166-1 alpha-2 country code (exceptionally, SWIFT has assigned the code XK to Republic of Kosovo, which does not have an ISO 3166-1 country code)
* - 2 letters or digits: location code
* -- if the second character is "0", then it is typically a test BIC as opposed to a BIC used on the live network.
* -- if the second character is "1", then it denotes a passive participant in the SWIFT network
* -- if the second character is "2", then it typically indicates a reverse billing BIC, where the recipient pays for the message as opposed to the more usual mode whereby the sender pays for the message.
* - 3 letters or digits: branch code, optional ('XXX' for primary office)
* Where an eight digit code is given, it may be assumed that it refers to the primary office.
*/
const val BicPatternString = "[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}(?:\\b|[A-Z0-9]{03})"
val BicPattern = Pattern.compile("^" + BicPatternString + "$")
val InvalidSepaCharactersPattern = Pattern.compile("[^${SepaMessageCreator.AllowedSepaCharacters}]+")
}
protected val sepaMessageCreator: ISepaMessageCreator = SepaMessageCreator()
open fun isValidIban(stringToTest: String): Boolean {
return IbanPattern.matcher(stringToTest).matches() ||
IbanWithSpacesPattern.matcher(stringToTest).matches()
}
open fun getInvalidIbanCharacters(string: String): String {
return getInvalidCharacters(string, InvalidIbanCharactersPattern)
}
open fun containsOnlyValidSepaCharacters(stringToTest: String): Boolean {
return sepaMessageCreator.containsOnlyAllowedCharacters(stringToTest)
}
open fun getInvalidSepaCharacters(string: String): String {
return getInvalidCharacters(string, InvalidSepaCharactersPattern)
}
open fun getInvalidCharacters(string: String, pattern: Pattern): String {
val illegalCharacters = mutableSetOf<String>()
val matcher = pattern.matcher(string)
while (matcher.find()) {
illegalCharacters.add(matcher.group())
}
return illegalCharacters.joinToString("")
}
}

View File

@ -0,0 +1,38 @@
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

@ -18,8 +18,7 @@ import net.dankito.banking.ui.model.BankAccount
import net.dankito.banking.ui.model.parameters.TransferMoneyData
import net.dankito.banking.ui.model.responses.BankingClientResponse
import net.dankito.banking.ui.presenter.BankingPresenter
import net.dankito.fints.messages.segmente.implementierte.sepa.ISepaMessageCreator
import net.dankito.fints.messages.segmente.implementierte.sepa.SepaMessageCreator
import net.dankito.banking.util.InputValidator
import net.dankito.fints.model.BankInfo
import net.dankito.utils.android.extensions.asActivity
import java.math.BigDecimal
@ -40,7 +39,10 @@ open class TransferMoneyDialog : DialogFragment() {
protected var preselectedValues: TransferMoneyData? = null
protected val sepaMessageCreator: ISepaMessageCreator = SepaMessageCreator()
protected val inputValidator = InputValidator() // TODO: move to presenter
protected var foundBankForEnteredIban = false
open fun show(activity: AppCompatActivity, presenter: BankingPresenter, preselectedBankAccount: BankAccount?, fullscreen: Boolean = false) {
@ -93,6 +95,11 @@ open class TransferMoneyDialog : DialogFragment() {
rootView.edtxtAmount.addTextChangedListener(otherEditTextChangedWatcher)
rootView.edtxtUsage.addTextChangedListener(otherEditTextChangedWatcher)
rootView.edtxtRemitteeName.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeNameIsValid() }
rootView.edtxtRemitteeIban.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeIbanIsValid() }
rootView.edtxtAmount.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredAmountIsValid() }
rootView.edtxtUsage.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredUsageTextIsValid() }
rootView.btnCancel.setOnClickListener { dismiss() }
rootView.btnTransferMoney.setOnClickListener { transferMoney() }
@ -209,23 +216,80 @@ open class TransferMoneyDialog : DialogFragment() {
}
private fun showValuesForFoundBankOnUiThread(foundBank: BankInfo?) {
foundBankForEnteredIban = foundBank != null
edtxtRemitteeBank.setText(if (foundBank != null) (foundBank.name + " " + foundBank.city) else "")
edtxtRemitteeBic.setText(foundBank?.bic ?: "")
if (foundBankForEnteredIban) {
lytRemitteeBic.error = null
}
checkIfRequiredDataEnteredOnUiThread()
}
protected open fun checkIfRequiredDataEnteredOnUiThread() {
val requiredDataEntered =
edtxtRemitteeName.text.toString().isNotEmpty()
&& sepaMessageCreator.containsOnlyAllowedCharacters(edtxtRemitteeName.text.toString()) // TODO: show error message for illegal characters
&& edtxtRemitteeIban.text.toString().isNotEmpty() // TODO: check if it is of length > 12, in Germany > 22?
&& edtxtRemitteeBic?.text.toString().isNotEmpty() // TODO: check if it is of length is 8 or 11?
&& isAmountGreaterZero()
&& sepaMessageCreator.containsOnlyAllowedCharacters(edtxtUsage.text.toString()) // TODO: show error message for illegal characters
val isRemitteeNameValid = isRemitteeNameValid()
val isValidIban = isRemitteeIbanValid()
val isAmountValid = isAmountGreaterZero()
val isUsageTextValid = isUsageTextValid()
btnTransferMoney.isEnabled = requiredDataEntered
btnTransferMoney.isEnabled = isRemitteeNameValid && isValidIban
&& edtxtRemitteeBic?.text.toString().isNotEmpty() // TODO: check if it is of length is 8 or 11?
&& isAmountValid && isUsageTextValid
}
protected open fun checkIfEnteredRemitteeNameIsValid() {
if (isRemitteeNameValid()) {
lytRemitteeName.error = null
}
else {
lytRemitteeName.error = context?.getString(R.string.error_invalid_sepa_characters_entered,
inputValidator.getInvalidSepaCharacters(edtxtRemitteeName.text.toString()))
}
}
protected open fun isRemitteeNameValid(): Boolean {
val enteredRemitteeName = edtxtRemitteeName.text.toString()
return enteredRemitteeName.isNotEmpty()
&& inputValidator.containsOnlyValidSepaCharacters(enteredRemitteeName)
}
protected open fun checkIfEnteredRemitteeIbanIsValid() {
if (isRemitteeIbanValid()) {
lytRemitteeIban.error = null
}
else {
val invalidIbanCharacters = inputValidator.getInvalidIbanCharacters(edtxtRemitteeIban.text.toString())
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) {
lytRemitteeBic.error = null
}
else {
lytRemitteeBic.error = context?.getString(R.string.error_no_bank_found_for_entered_iban)
}
}
protected open fun isRemitteeIbanValid(): Boolean {
return inputValidator.isValidIban(edtxtRemitteeIban.text.toString())
}
protected open fun checkIfEnteredAmountIsValid() {
if (isAmountGreaterZero()) {
lytAmount.error = null
}
else {
lytAmount.error = context?.getString(R.string.error_invalid_amount_entered)
}
}
protected open fun isAmountGreaterZero(): Boolean {
@ -248,4 +312,18 @@ open class TransferMoneyDialog : DialogFragment() {
return null
}
protected open fun checkIfEnteredUsageTextIsValid() {
if (isUsageTextValid()) {
lytUsage.error = null
}
else {
lytUsage.error = context?.getString(R.string.error_invalid_sepa_characters_entered,
inputValidator.getInvalidSepaCharacters(edtxtUsage.text.toString()))
}
}
protected open fun isUsageTextValid(): Boolean {
return inputValidator.containsOnlyValidSepaCharacters(edtxtUsage.text.toString())
}
}

View File

@ -1,9 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/dialog_transfer_money_padding"
android:isScrollContainer="true"
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/dialog_transfer_money_padding"
android:isScrollContainer="true"
>
<LinearLayout
@ -42,16 +44,18 @@
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/dialog_transfer_money_remittee_name"
android:id="@+id/lytRemitteeName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/dialog_transfer_money_remittee_name"
app:errorEnabled="true"
>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edtxtRemitteeName"
android:layout_width="match_parent"
android:layout_height="@dimen/dialog_transfer_money_input_fields_height"
android:inputType="textPersonName"
android:id="@+id/edtxtRemitteeName"
android:layout_width="match_parent"
android:layout_height="@dimen/dialog_transfer_money_input_fields_height"
android:inputType="textPersonName"
>
<requestFocus />
@ -61,112 +65,122 @@
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/dialog_transfer_money_remittee_iban"
android:id="@+id/lytRemitteeIban"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/dialog_transfer_money_remittee_iban"
app:errorEnabled="true"
>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edtxtRemitteeIban"
android:layout_width="match_parent"
android:layout_height="@dimen/dialog_transfer_money_autocomplete_fields_height"
android:inputType="text"
android:id="@+id/edtxtRemitteeIban"
android:layout_width="match_parent"
android:layout_height="@dimen/dialog_transfer_money_autocomplete_fields_height"
android:inputType="text"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/dialog_transfer_money_remittee_bank"
android:id="@+id/lytRemitteeBank"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/dialog_transfer_money_remittee_bank"
app:errorEnabled="true"
>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edtxtRemitteeBank"
android:layout_width="match_parent"
android:layout_height="@dimen/dialog_transfer_money_input_fields_height"
android:inputType="text"
android:enabled="false"
android:id="@+id/edtxtRemitteeBank"
android:layout_width="match_parent"
android:layout_height="@dimen/dialog_transfer_money_input_fields_height"
android:inputType="text"
android:enabled="false"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/dialog_transfer_money_remittee_bic"
android:id="@+id/lytRemitteeBic"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/dialog_transfer_money_remittee_bic"
app:errorEnabled="true"
>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edtxtRemitteeBic"
android:layout_width="match_parent"
android:layout_height="@dimen/dialog_transfer_money_input_fields_height"
android:inputType="text"
android:enabled="false"
android:id="@+id/edtxtRemitteeBic"
android:layout_width="match_parent"
android:layout_height="@dimen/dialog_transfer_money_input_fields_height"
android:inputType="text"
android:enabled="false"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/dialog_transfer_money_amount"
android:id="@+id/lytAmount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/dialog_transfer_money_amount"
app:errorEnabled="true"
>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edtxtAmount"
android:layout_width="match_parent"
android:layout_height="@dimen/dialog_transfer_money_input_fields_height"
android:inputType="numberDecimal"
android:id="@+id/edtxtAmount"
android:layout_width="match_parent"
android:layout_height="@dimen/dialog_transfer_money_input_fields_height"
android:inputType="numberDecimal"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/dialog_transfer_money_usage"
android:id="@+id/lytUsage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/dialog_transfer_money_usage"
app:errorEnabled="true"
>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edtxtUsage"
android:layout_width="match_parent"
android:layout_height="@dimen/dialog_transfer_money_input_fields_height"
android:inputType="text"
android:id="@+id/edtxtUsage"
android:layout_width="match_parent"
android:layout_height="@dimen/dialog_transfer_money_input_fields_height"
android:inputType="text"
/>
</com.google.android.material.textfield.TextInputLayout>
<RelativeLayout
android:id="@+id/lytButtonBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/lytButtonBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<Button
android:id="@+id/btnTransferMoney"
android:layout_width="@dimen/dialog_transfer_money_buttons_width"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
style="?android:attr/buttonBarButtonStyle"
android:text="@string/dialog_transfer_money_transfer"
android:enabled="false"
android:id="@+id/btnTransferMoney"
android:layout_width="@dimen/dialog_transfer_money_buttons_width"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
style="?android:attr/buttonBarButtonStyle"
android:text="@string/dialog_transfer_money_transfer"
android:enabled="false"
/>
<Button
android:id="@+id/btnCancel"
android:layout_width="@dimen/dialog_transfer_money_buttons_width"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/btnTransferMoney"
android:layout_toStartOf="@+id/btnTransferMoney"
style="?android:attr/buttonBarButtonStyle"
android:text="@string/cancel"
android:id="@+id/btnCancel"
android:layout_width="@dimen/dialog_transfer_money_buttons_width"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/btnTransferMoney"
android:layout_toStartOf="@+id/btnTransferMoney"
style="?android:attr/buttonBarButtonStyle"
android:text="@string/cancel"
/>
</RelativeLayout>

View File

@ -51,7 +51,7 @@
<string name="dialog_transfer_money_remittee_name">Name:</string>
<string name="dialog_transfer_money_remittee_iban">IBAN:</string>
<string name="dialog_transfer_money_remittee_bank">Bank (wird automatisch eingetragen):</string>
<string name="dialog_transfer_money_remittee_bic">BIC:</string>
<string name="dialog_transfer_money_remittee_bic">BIC (wird automatisch eingetragen):</string>
<string name="dialog_transfer_money_amount">Betrag:</string>
<string name="dialog_transfer_money_usage">Verwendungszweck:</string>
<string name="dialog_transfer_money_transfer">Überweisen</string>
@ -88,4 +88,11 @@
<string name="dialog_edit_account_ask_should_account_be_deleted">Möchten Sie das Konto \'%s\' wirklich löschen?
\n\nDies kann nicht rückgängig gemacht werden und die hierzu gespeicherten Daten gehen unwiederbringlich verloren.</string>
<string name="error_invalid_sepa_characters_entered">Unzulässige(s) Zeichen eingegeben: %s</string>
<string name="error_invalid_iban_characters_entered">Unzulässige(s) Zeichen eingegeben: %s</string>
<string name="error_invalid_iban_pattern_entered">IBANs bestehen aus folgendem Muster: DE12 1234 5678 9012 3456 78</string>
<string name="error_no_bank_found_for_entered_iban">Es wurde keine Bank zur eingegebenen IBAN gefunden.</string>
<string name="error_invalid_amount_entered">Bitte geben Sie einen Betrag größer 0 ein.</string>
</resources>

View File

@ -51,7 +51,7 @@
<string name="dialog_transfer_money_remittee_name">Name:</string>
<string name="dialog_transfer_money_remittee_iban">IBAN:</string>
<string name="dialog_transfer_money_remittee_bank">Bank (will be entered automatically):</string>
<string name="dialog_transfer_money_remittee_bic">BIC:</string>
<string name="dialog_transfer_money_remittee_bic">BIC (will be entered automatically):</string>
<string name="dialog_transfer_money_amount">Amount:</string>
<string name="dialog_transfer_money_usage">Usage:</string>
<string name="dialog_transfer_money_transfer">Transfer</string>
@ -88,4 +88,11 @@
<string name="dialog_edit_account_ask_should_account_be_deleted">Really delete account \'%s\'?
\n\nThis cannot be undone and data will be lost.</string>
<string name="error_invalid_sepa_characters_entered">Invalid character(s) entered: %s</string>
<string name="error_invalid_iban_characters_entered">Invalid character(s) entered: %s</string>
<string name="error_invalid_iban_pattern_entered">IBAN has to have pattern: EN12 1234 5678 9012 3456 78</string>
<string name="error_no_bank_found_for_entered_iban">No bank found for entered IBAN.</string>
<string name="error_invalid_amount_entered">Please enter an amount greater zero.</string>
</resources>