-
Notifications
You must be signed in to change notification settings - Fork 629
/
IbanConfig.kt
130 lines (114 loc) · 4.67 KB
/
IbanConfig.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package com.stripe.android.ui.core.elements
import androidx.annotation.RestrictTo
import androidx.annotation.StringRes
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import com.stripe.android.ui.core.R
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.math.BigInteger
import java.util.Locale
/**
* A text field configuration for an IBAN, or International Bank Account Number, as defined in
* ISO 13616-1.
*
* @see [IBAN on Wikipedia](https://en.wikipedia.org/wiki/International_Bank_Account_Number)
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class IbanConfig : TextFieldConfig {
override val capitalization: KeyboardCapitalization = KeyboardCapitalization.Characters
override val debugLabel = "iban"
@StringRes
override val label = R.string.iban
override val keyboard = KeyboardType.Ascii
override val trailingIcon: MutableStateFlow<TextFieldIcon?> = MutableStateFlow(
TextFieldIcon.Trailing(
R.drawable.stripe_ic_bank_generic,
isTintable = true
)
)
override val loading: StateFlow<Boolean> = MutableStateFlow(false)
// Displays the IBAN in groups of 4 characters with spaces added between them
override val visualTransformation: VisualTransformation = VisualTransformation { text ->
val output = StringBuilder()
text.text.forEachIndexed { i, char ->
output.append(char)
if (i % 4 == 3 && i < MAX_LENGTH - 1) output.append(" ")
}
TransformedText(
AnnotatedString(output.toString()),
object : OffsetMapping {
override fun originalToTransformed(offset: Int) = offset + offset / 4
override fun transformedToOriginal(offset: Int) = offset - offset / 5
}
)
}
override fun filter(userTyped: String) =
userTyped.filter { VALID_INPUT_RANGES.contains(it) }.take(MAX_LENGTH).uppercase()
override fun convertToRaw(displayName: String) = displayName
override fun convertFromRaw(rawValue: String) = rawValue
override fun determineState(input: String): TextFieldState {
input.ifBlank {
return TextFieldStateConstants.Error.Blank
}
val countryCode = input.take(2).uppercase()
// First 2 characters represent the country code. Any number means it's invalid
if (countryCode.any { it.isDigit() }) {
return TextFieldStateConstants.Error.Invalid(
R.string.iban_invalid_start
)
}
if (countryCode.length < 2) {
// User might still be entering a valid country code
return TextFieldStateConstants.Error.Incomplete(
R.string.iban_incomplete
)
}
if (!Locale.getISOCountries().contains(countryCode)) {
return TextFieldStateConstants.Error.Invalid(
R.string.iban_invalid_country,
arrayOf(countryCode)
)
}
if (input.length < MIN_LENGTH) {
return TextFieldStateConstants.Error.Incomplete(
R.string.iban_incomplete
)
}
return if (isIbanValid(input)) {
if (input.length == MAX_LENGTH) {
TextFieldStateConstants.Valid.Full
} else {
TextFieldStateConstants.Valid.Limitless
}
} else {
TextFieldStateConstants.Error.Incomplete(
R.string.iban_invalid
)
}
}
/**
* Verify an IBAN based on the validation algorithm:
* https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
*
* 1. Move the four initial characters to the end of the string
* 2. Convert letters to numbers, where A = 10, B = 11, ..., Z = 35
* 3. Interpret the string as a decimal integer and check that the mod 97 is 1
*/
private fun isIbanValid(iban: String) =
iban.takeLast(iban.length - 4).plus(iban.take(4)).uppercase()
.replace(
Regex("[A-Z]")
) {
(it.value.first() - 'A' + 10).toString()
}.toBigInteger().mod(BigInteger("97")).equals(BigInteger.ONE)
private companion object {
const val MIN_LENGTH = 8 // Length varies per country, but is at least 8
const val MAX_LENGTH = 34
val VALID_INPUT_RANGES = ('0'..'9') + ('a'..'z') + ('A'..'Z')
}
}