Implemented validating and auto correcting user input in TransferMoneyDialog
This commit is contained in:
parent
321814a0ca
commit
137d35ac02
|
@ -16,6 +16,8 @@ expect class BigDecimal {
|
|||
constructor(double: Double)
|
||||
|
||||
|
||||
val isPositive: Boolean
|
||||
|
||||
fun format(countDecimalPlaces: Int): String
|
||||
|
||||
}
|
|
@ -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 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 val isPositive: Boolean
|
||||
get() = this >= Zero
|
||||
|
||||
actual fun format(countDecimalPlaces: Int): String {
|
||||
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 {
|
||||
if (other is BigDecimal) {
|
||||
return this.decimal.compare(other.decimal) == NSOrderedSame
|
||||
return this.compareTo(other) == 0
|
||||
}
|
||||
|
||||
return super.equals(other)
|
||||
|
|
|
@ -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 val isPositive: Boolean
|
||||
get() = this >= ZERO
|
||||
|
||||
actual fun format(countDecimalPlaces: Int): String {
|
||||
return String.format("%.0${countDecimalPlaces}f", this)
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ open class SepaMessageCreator : ISepaMessageCreator {
|
|||
|
||||
override fun containsOnlyAllowedCharacters(stringToTest: String): Boolean {
|
||||
return AllowedSepaCharactersPattern.matches(stringToTest)
|
||||
&& convertDiacriticsAndReservedXmlCharacters(stringToTest) == stringToTest
|
||||
}
|
||||
|
||||
override fun convertDiacriticsAndReservedXmlCharacters(input: String): String {
|
||||
|
|
|
@ -12,6 +12,7 @@ import android.widget.EditText
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.otaliastudios.autocomplete.Autocomplete
|
||||
import kotlinx.android.synthetic.main.dialog_transfer_money.*
|
||||
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.util.InputValidator
|
||||
import net.dankito.banking.bankfinder.BankInfo
|
||||
import net.dankito.banking.util.ValidationResult
|
||||
import net.dankito.utils.multiplatform.toBigDecimal
|
||||
import net.dankito.utils.android.extensions.asActivity
|
||||
import java.math.BigDecimal
|
||||
|
@ -57,7 +59,17 @@ open class TransferMoneyDialog : DialogFragment() {
|
|||
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
|
||||
|
@ -113,10 +125,13 @@ open class TransferMoneyDialog : DialogFragment() {
|
|||
initRemitteeAutocompletion(rootView.edtxtRemitteeName)
|
||||
|
||||
rootView.edtxtRemitteeName.addTextChangedListener(checkRequiredDataWatcher {
|
||||
checkIfEnteredRemitteeNameIsValid()
|
||||
checkIfEnteredRemitteeNameIsValidWhileUserIsTyping()
|
||||
})
|
||||
|
||||
rootView.edtxtRemitteeIban.addTextChangedListener(StandardTextWatcher { tryToGetBicFromIban(it) })
|
||||
rootView.edtxtRemitteeIban.addTextChangedListener(StandardTextWatcher {
|
||||
checkIfEnteredRemitteeIbanIsValidWhileUserIsTyping()
|
||||
tryToGetBicFromIban(it)
|
||||
})
|
||||
|
||||
rootView.edtxtRemitteeBic.addTextChangedListener(checkRequiredDataWatcher())
|
||||
rootView.edtxtAmount.addTextChangedListener(checkRequiredDataWatcher())
|
||||
|
@ -124,8 +139,8 @@ open class TransferMoneyDialog : DialogFragment() {
|
|||
checkIfEnteredUsageTextIsValid()
|
||||
})
|
||||
|
||||
rootView.edtxtRemitteeName.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeNameIsValid() }
|
||||
rootView.edtxtRemitteeIban.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeIbanIsValid() }
|
||||
rootView.edtxtRemitteeName.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeNameIsValidAfterFocusLost() }
|
||||
rootView.edtxtRemitteeIban.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeIbanIsValidAfterFocusLost() }
|
||||
rootView.edtxtRemitteeBic.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeBicIsValid() }
|
||||
rootView.edtxtAmount.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredAmountIsValid() }
|
||||
rootView.edtxtUsage.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredUsageTextIsValid() }
|
||||
|
@ -290,8 +305,8 @@ open class TransferMoneyDialog : DialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
protected open fun tryToGetBicFromIban(enteredText: CharSequence) {
|
||||
presenter.findUniqueBankForIbanAsync(enteredText.toString()) { foundBank ->
|
||||
protected open fun tryToGetBicFromIban(enteredIban: CharSequence) {
|
||||
presenter.findUniqueBankForIbanAsync(enteredIban.toString()) { foundBank ->
|
||||
context?.asActivity()?.runOnUiThread {
|
||||
showValuesForFoundBankOnUiThread(foundBank)
|
||||
}
|
||||
|
@ -299,72 +314,51 @@ open class TransferMoneyDialog : DialogFragment() {
|
|||
}
|
||||
|
||||
private fun showValuesForFoundBankOnUiThread(foundBank: BankInfo?) {
|
||||
foundBankForEnteredIban = foundBank != null
|
||||
validRemitteeBicEntered = foundBank != null
|
||||
|
||||
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
|
||||
lytRemitteeBic.error = null
|
||||
|
||||
if (foundBankForEnteredIban) {
|
||||
lytRemitteeIban.error = null
|
||||
}
|
||||
lytRemitteeBic.error = null // TODO: show information here if BIC hasn't been found
|
||||
|
||||
checkIfRequiredDataEnteredOnUiThread()
|
||||
}
|
||||
|
||||
protected open fun checkIfRequiredDataEnteredOnUiThread() {
|
||||
btnTransferMoney.isEnabled = isRemitteeNameValid() && isRemitteeIbanValid()
|
||||
&& isRemitteeBicValid()
|
||||
&& isAmountGreaterZero() && isUsageTextValid()
|
||||
btnTransferMoney.isEnabled = validRemitteeNameEntered && validRemitteeIbanEntered
|
||||
&& validRemitteeBicEntered
|
||||
&& validAmountEntered && validUsageEntered
|
||||
}
|
||||
|
||||
protected open fun checkIfEnteredRemitteeNameIsValid() {
|
||||
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 {
|
||||
protected open fun checkIfEnteredRemitteeNameIsValidWhileUserIsTyping() {
|
||||
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 validationResult = inputValidator.validateIbanWhileTyping(enteredIban)
|
||||
|
||||
if (isRemitteeIbanValid()) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
this.validRemitteeIbanEntered = validationResult.validationSuccessfulOrCouldCorrectString
|
||||
|
||||
if (foundBankForEnteredIban || enteredIban.isBlank()) {
|
||||
showValidationResult(lytRemitteeIban, validationResult)
|
||||
|
||||
if (validRemitteeBicEntered || enteredIban.isBlank()) {
|
||||
lytRemitteeBic.error = null
|
||||
}
|
||||
else {
|
||||
|
@ -372,56 +366,31 @@ open class TransferMoneyDialog : DialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
protected open fun isRemitteeIbanValid(): Boolean {
|
||||
return inputValidator.isValidIban(edtxtRemitteeIban.text.toString())
|
||||
protected open fun checkIfEnteredRemitteeIbanIsValidAfterFocusLost() {
|
||||
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() {
|
||||
if (isRemitteeBicValid()) {
|
||||
lytRemitteeBic.error = null
|
||||
}
|
||||
else {
|
||||
val enteredBic = edtxtRemitteeBic.text.toString()
|
||||
val enteredBic = edtxtRemitteeBic.text.toString()
|
||||
val validationResult = inputValidator.validateBic(enteredBic)
|
||||
|
||||
if (enteredBic.isBlank()) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.validRemitteeBicEntered = validationResult.validationSuccessfulOrCouldCorrectString
|
||||
|
||||
protected open fun isRemitteeBicValid(): Boolean {
|
||||
return inputValidator.isValidBic(edtxtRemitteeBic.text.toString())
|
||||
showValidationResult(lytRemitteeBic, validationResult)
|
||||
}
|
||||
|
||||
protected open fun checkIfEnteredAmountIsValid() {
|
||||
if (isAmountGreaterZero()) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
val validationResult = inputValidator.validateAmount(edtxtAmount.text.toString())
|
||||
|
||||
protected open fun isAmountGreaterZero(): Boolean {
|
||||
try {
|
||||
getEnteredAmount()?.let { amount ->
|
||||
return amount > BigDecimal.ZERO
|
||||
}
|
||||
} catch (ignored: Exception) { }
|
||||
this.validAmountEntered = validationResult.validationSuccessfulOrCouldCorrectString
|
||||
|
||||
return false
|
||||
showValidationResult(lytAmount, validationResult)
|
||||
}
|
||||
|
||||
protected open fun getEnteredAmount(): BigDecimal? {
|
||||
|
@ -435,22 +404,40 @@ open class TransferMoneyDialog : DialogFragment() {
|
|||
}
|
||||
|
||||
protected open fun checkIfEnteredUsageTextIsValid() {
|
||||
val enteredUsage = edtxtUsage.text.toString()
|
||||
val validationResult = inputValidator.validateUsage(edtxtUsage.text.toString())
|
||||
|
||||
if (isUsageTextValid()) {
|
||||
lytUsage.error = null
|
||||
}
|
||||
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))
|
||||
}
|
||||
this.validUsageEntered = validationResult.validationSuccessfulOrCouldCorrectString
|
||||
|
||||
showValidationResult(lytUsage, validationResult)
|
||||
}
|
||||
|
||||
protected open fun isUsageTextValid(): Boolean {
|
||||
return inputValidator.isUsageValid(edtxtUsage.text.toString())
|
||||
protected open fun showValidationResult(textInputLayout: TextInputLayout, validationResult: ValidationResult) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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.SepaMessageCreator
|
||||
import net.dankito.utils.multiplatform.BigDecimal
|
||||
|
||||
|
||||
open class InputValidator {
|
||||
|
@ -10,6 +11,10 @@ open class InputValidator {
|
|||
|
||||
const val RemitteNameMaxLength = 70
|
||||
|
||||
const val IbanMaxLength = 34
|
||||
|
||||
const val BicMaxLength = 11
|
||||
|
||||
const val UsageMaxLength = 140
|
||||
|
||||
|
||||
|
@ -58,6 +63,94 @@ open class InputValidator {
|
|||
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 {
|
||||
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 {
|
||||
return BicPattern.matches(stringToTest)
|
||||
}
|
||||
|
@ -76,23 +194,50 @@ open class InputValidator {
|
|||
}
|
||||
|
||||
|
||||
open fun isRemitteeNameValid(stringToTest: String): Boolean {
|
||||
val convertedString = convertToAllowedSepaCharacters(stringToTest)
|
||||
open fun validateAmount(enteredAmountString: String): ValidationResult {
|
||||
if (enteredAmountString.isBlank()) {
|
||||
return ValidationResult(enteredAmountString, false, validationError = "Bitte geben Sie den zu überweisenden Betrag ein") // TODO: translate
|
||||
}
|
||||
|
||||
return hasRemitteeNameValidLength(convertedString)
|
||||
&& containsOnlyValidSepaCharacters(convertedString)
|
||||
convertAmountString(enteredAmountString)?.let { amount ->
|
||||
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 {
|
||||
return stringToTest.length in 1..RemitteNameMaxLength
|
||||
open fun convertAmountString(enteredAmountString: String): BigDecimal? {
|
||||
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 {
|
||||
val convertedString = convertToAllowedSepaCharacters(stringToTest)
|
||||
|
||||
return hasUsageValidLength(convertedString)
|
||||
&& containsOnlyValidSepaCharacters(convertedString)
|
||||
return hasUsageValidLength(stringToTest)
|
||||
&& containsOnlyValidSepaCharacters(stringToTest)
|
||||
}
|
||||
|
||||
open fun hasUsageValidLength(stringToTest: String): Boolean {
|
||||
|
@ -105,7 +250,7 @@ open class InputValidator {
|
|||
}
|
||||
|
||||
open fun getInvalidSepaCharacters(string: String): String {
|
||||
return getInvalidCharacters(convertToAllowedSepaCharacters(string), InvalidSepaCharactersPattern)
|
||||
return getInvalidCharacters(string, InvalidSepaCharactersPattern)
|
||||
}
|
||||
|
||||
open fun convertToAllowedSepaCharacters(string: String): String {
|
||||
|
@ -114,7 +259,19 @@ open class InputValidator {
|
|||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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 = "&"
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
|
@ -16,26 +16,35 @@ struct TransferMoneyDialog: View {
|
|||
|
||||
@State private var remitteeName: String = ""
|
||||
@State private var isValidRemitteeNameEntered = false
|
||||
@State private var remitteeNameValidationResult: ValidationResult? = nil
|
||||
|
||||
@State private var showRemitteeAutocompleteList = false
|
||||
@State private var remitteeSearchResults = [Remittee]()
|
||||
|
||||
@State private var remitteeIban: String = ""
|
||||
@State private var isValidRemitteeIbanEntered = false
|
||||
@State private var remitteeIbanValidationResult: ValidationResult? = nil
|
||||
|
||||
@State private var remitteeBic: String = ""
|
||||
@State private var isValidRemitteeBicEntered = false
|
||||
@State private var remitteeBicValidationResult: ValidationResult? = nil
|
||||
|
||||
@State private var amount = ""
|
||||
@State private var isValidAmountEntered = false
|
||||
@State private var amountValidationResult: ValidationResult? = nil
|
||||
|
||||
@State private var usage: String = ""
|
||||
@State private var isValidUsageEntered = true
|
||||
@State private var usageValidationResult: ValidationResult? = nil
|
||||
|
||||
@State private var instantPayment = false
|
||||
|
||||
@State private var transferMoneyResponseMessage: Message? = nil
|
||||
|
||||
private let inputValidator = InputValidator()
|
||||
|
||||
@State private var didJustCorrectEnteredValue = false
|
||||
|
||||
|
||||
private var account: BankAccount? {
|
||||
if (self.selectedAccountIndex < self.accountsSupportingTransferringMoney.count) {
|
||||
|
@ -95,9 +104,12 @@ struct TransferMoneyDialog: View {
|
|||
}
|
||||
|
||||
Section {
|
||||
UIKitTextField("Remittee Name", text: $remitteeName, focusOnStart: true, focusNextTextFieldOnReturnKeyPress: true, actionOnReturnKeyPress: handleReturnKeyPress) { newValue in
|
||||
self.isValidRemitteeNameEntered = self.remitteeName.isNotBlank
|
||||
}
|
||||
LabelledUIKitTextField(label: "Remittee Name", text: $remitteeName, focusOnStart: true, focusNextTextFieldOnReturnKeyPress: true,
|
||||
isFocussedChanged: validateRemitteeNameOnFocusLost, actionOnReturnKeyPress: handleReturnKeyPress, textChanged: validateRemitteeName)
|
||||
|
||||
remitteeNameValidationResult.map { validationError in
|
||||
ValidationLabel(validationError)
|
||||
}
|
||||
|
||||
if self.showRemitteeAutocompleteList {
|
||||
Section {
|
||||
|
@ -109,26 +121,26 @@ struct TransferMoneyDialog: View {
|
|||
}
|
||||
}
|
||||
|
||||
UIKitTextField("Remittee IBAN", text: $remitteeIban, focusNextTextFieldOnReturnKeyPress: true, actionOnReturnKeyPress: handleReturnKeyPress) { newValue in
|
||||
self.isValidRemitteeIbanEntered = newValue.count > 14 // TODO: implement real check if IBAN is valid
|
||||
self.tryToGetBicFromIban(newValue)
|
||||
}
|
||||
LabelledUIKitTextField(label: "Remittee IBAN", text: $remitteeIban, focusNextTextFieldOnReturnKeyPress: true, isFocussedChanged: validateRemitteeIbanOnFocusLost,
|
||||
actionOnReturnKeyPress: handleReturnKeyPress, textChanged: validateRemitteeIban)
|
||||
|
||||
remitteeIbanValidationResult.map { validationError in
|
||||
ValidationLabel(validationError)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
UIKitTextField("Amount", text: $amount, keyboardType: .decimalPad, focusNextTextFieldOnReturnKeyPress: true, actionOnReturnKeyPress: handleReturnKeyPress) { newValue in
|
||||
// TODO: implement DecimalTextField / NumericTextField
|
||||
let filtered = newValue.filter { "0123456789,".contains($0) }
|
||||
if filtered != newValue {
|
||||
self.amount = filtered
|
||||
}
|
||||
LabelledUIKitTextField(label: "Amount", text: $amount, keyboardType: .decimalPad, focusNextTextFieldOnReturnKeyPress: true, actionOnReturnKeyPress: handleReturnKeyPress, textChanged: validateAmount)
|
||||
|
||||
self.isValidAmountEntered = self.amount.isNotBlank
|
||||
}
|
||||
amountValidationResult.map { validationError in
|
||||
ValidationLabel(validationError)
|
||||
}
|
||||
|
||||
UIKitTextField("Usage", text: $usage, actionOnReturnKeyPress: handleReturnKeyPress) { newValue in
|
||||
self.isValidUsageEntered = true
|
||||
}
|
||||
LabelledUIKitTextField(label: "Usage", text: $usage, actionOnReturnKeyPress: handleReturnKeyPress, textChanged: validateUsage)
|
||||
|
||||
usageValidationResult.map { validationError in
|
||||
ValidationLabel(validationError)
|
||||
}
|
||||
}
|
||||
|
||||
if supportsInstantPayment {
|
||||
|
@ -171,7 +183,34 @@ struct TransferMoneyDialog: View {
|
|||
}
|
||||
|
||||
|
||||
func tryToGetBicFromIban(_ enteredIban: String) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
&& isValidRemitteeNameEntered
|
||||
&& isValidRemitteeIbanEntered
|
||||
|
@ -195,15 +277,17 @@ struct TransferMoneyDialog: View {
|
|||
&& isValidUsageEntered
|
||||
}
|
||||
|
||||
func transferMoney() {
|
||||
let data = TransferMoneyData(creditorName: remitteeName, creditorIban: remitteeIban, creditorBic: remitteeBic, amount: CommonBigDecimal(decimal: amount.replacingOccurrences(of: ",", with: ".")), usage: usage, instantPayment: instantPayment)
|
||||
private func transferMoney() {
|
||||
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) {
|
||||
self.transferMoneyResponseMessage = Message(message: Text("Successfully transferred \(data.amount) \("€") to \(data.creditorName)"), primaryButton: .ok {
|
||||
self.presentation.wrappedValue.dismiss()
|
||||
|
|
Loading…
Reference in New Issue