diff --git a/BankingUiCommon/build.gradle b/BankingUiCommon/build.gradle index 524fcba8..0370eab8 100644 --- a/BankingUiCommon/build.gradle +++ b/BankingUiCommon/build.gradle @@ -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" } \ No newline at end of file diff --git a/BankingUiCommon/src/main/java/net/dankito/banking/util/InputValidator.kt b/BankingUiCommon/src/main/java/net/dankito/banking/util/InputValidator.kt new file mode 100644 index 00000000..ecf3c9bf --- /dev/null +++ b/BankingUiCommon/src/main/java/net/dankito/banking/util/InputValidator.kt @@ -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() + + val matcher = pattern.matcher(string) + + while (matcher.find()) { + illegalCharacters.add(matcher.group()) + } + + return illegalCharacters.joinToString("") + } + +} \ No newline at end of file diff --git a/BankingUiCommon/src/test/kotlin/net/dankito/banking/util/InputValidatorTest.kt b/BankingUiCommon/src/test/kotlin/net/dankito/banking/util/InputValidatorTest.kt new file mode 100644 index 00000000..92db12d5 --- /dev/null +++ b/BankingUiCommon/src/test/kotlin/net/dankito/banking/util/InputValidatorTest.kt @@ -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) + } + +} \ No newline at end of file diff --git a/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/dialogs/TransferMoneyDialog.kt b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/dialogs/TransferMoneyDialog.kt index fa6667cd..09e96ece 100644 --- a/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/dialogs/TransferMoneyDialog.kt +++ b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/dialogs/TransferMoneyDialog.kt @@ -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()) + } + } \ No newline at end of file diff --git a/fints4javaAndroidApp/src/main/res/layout/dialog_transfer_money.xml b/fints4javaAndroidApp/src/main/res/layout/dialog_transfer_money.xml index c5cd163d..cd54455b 100644 --- a/fints4javaAndroidApp/src/main/res/layout/dialog_transfer_money.xml +++ b/fints4javaAndroidApp/src/main/res/layout/dialog_transfer_money.xml @@ -1,9 +1,11 @@ - @@ -61,112 +65,122 @@