diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/address/AddressRepository.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/address/AddressRepository.kt index 7968212dbe6..151ba4361c2 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/address/AddressRepository.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/address/AddressRepository.kt @@ -45,7 +45,7 @@ class AddressRepository @Inject constructor( countryAddressSchemaPair.map { (countryCode, schemaList) -> countryCode to requireNotNull( schemaList - .transformToElementList() + .transformToElementList(countryCode) ) }.forEach { add(it.first, it.second) } } diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/address/TransformAddressToElement.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/address/TransformAddressToElement.kt index e07bbe8b39e..a2899b46d2b 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/address/TransformAddressToElement.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/address/TransformAddressToElement.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import com.stripe.android.ui.core.R import com.stripe.android.ui.core.elements.IdentifierSpec +import com.stripe.android.ui.core.elements.PostalCodeConfig import com.stripe.android.ui.core.elements.RowController import com.stripe.android.ui.core.elements.RowElement import com.stripe.android.ui.core.elements.SectionFieldElement @@ -212,7 +213,7 @@ internal fun parseAddressesSchema(inputStream: InputStream?) = private fun getJsonStringFromInputStream(inputStream: InputStream?) = inputStream?.bufferedReader().use { it?.readText() } -internal fun List.transformToElementList(): List { +internal fun List.transformToElementList(countryCode: String): List { val countryAddressElements = this .filterNot { it.type == FieldType.SortingCode || @@ -220,14 +221,29 @@ internal fun List.transformToElementList(): List addressField.type?.let { - SimpleTextElement( - addressField.type.identifierSpec, - SimpleTextFieldController( + val textFieldConfig = when (it) { + FieldType.PostalCode -> { + addressField.schema?.nameType + PostalCodeConfig( + label = addressField.schema?.nameType?.stringResId ?: it.defaultLabel, + capitalization = it.capitalization(), + keyboard = getKeyboard(addressField.schema), + country = countryCode + ) + } + else -> { SimpleTextFieldConfig( label = addressField.schema?.nameType?.stringResId ?: it.defaultLabel, capitalization = it.capitalization(), keyboard = getKeyboard(addressField.schema) - ), + ) + } + } + + SimpleTextElement( + addressField.type.identifierSpec, + SimpleTextFieldController( + textFieldConfig = textFieldConfig, showOptionalLabel = !addressField.required ) ) diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/PostalCodeConfig.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/PostalCodeConfig.kt new file mode 100644 index 00000000000..e435cc30f63 --- /dev/null +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/PostalCodeConfig.kt @@ -0,0 +1,80 @@ +package com.stripe.android.ui.core.elements + +import androidx.annotation.StringRes +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.VisualTransformation +import kotlinx.coroutines.flow.MutableStateFlow + +class PostalCodeConfig( + @StringRes override val label: Int, + override val capitalization: KeyboardCapitalization = KeyboardCapitalization.Words, + override val keyboard: KeyboardType = KeyboardType.Text, + override val trailingIcon: MutableStateFlow = MutableStateFlow(null), + country: String +) : TextFieldConfig { + override val debugLabel: String = "postal_code_text" + override val visualTransformation: VisualTransformation? = null + override val loading: MutableStateFlow = MutableStateFlow(false) + private val format = CountryPostalFormat.forCountry(country) + + override fun determineState(input: String): TextFieldState = object : TextFieldState { + override fun shouldShowError(hasFocus: Boolean) = false + + override fun isValid(): Boolean { + return input.length in format.minimumLength..format.maximumLength && + input.matches(format.regexPattern) + } + + override fun getError(): FieldError? = null + + override fun isFull(): Boolean = input.length >= format.minimumLength + + override fun isBlank(): Boolean = input.isBlank() + } + + override fun filter(userTyped: String): String = + if ( + setOf(KeyboardType.Number, KeyboardType.NumberPassword).contains(keyboard) + ) { + userTyped.filter { it.isDigit() } + } else { + userTyped + }.replace(Regex("\\s*"), "") + + override fun convertToRaw(displayName: String) = displayName + + override fun convertFromRaw(rawValue: String) = rawValue + + sealed class CountryPostalFormat( + val minimumLength: Int, + val maximumLength: Int, + val regexPattern: Regex + ) { + object CA : CountryPostalFormat( + minimumLength = 6, + maximumLength = 6, + regexPattern = Regex("([a-zA-Z]\\d)+") + ) + object US : CountryPostalFormat( + minimumLength = 5, + maximumLength = 9, + regexPattern = Regex("\\d+") + ) + object Other : CountryPostalFormat( + minimumLength = 4, + maximumLength = 12, + regexPattern = Regex(".+") + ) + + companion object { + fun forCountry(country: String): CountryPostalFormat { + return when (country) { + "US" -> US + "CA" -> CA + else -> Other + } + } + } + } +} diff --git a/payments-ui-core/src/test/java/com/stripe/android/ui/core/address/TransformAddressToElementTest.kt b/payments-ui-core/src/test/java/com/stripe/android/ui/core/address/TransformAddressToElementTest.kt index b9b43b00e45..fe3a2992b1a 100644 --- a/payments-ui-core/src/test/java/com/stripe/android/ui/core/address/TransformAddressToElementTest.kt +++ b/payments-ui-core/src/test/java/com/stripe/android/ui/core/address/TransformAddressToElementTest.kt @@ -22,7 +22,7 @@ class TransformAddressToElementTest { @Test fun `Read US Json`() = runBlocking { val addressSchema = readFile("src/main/assets/addressinfo/US.json")!! - val simpleTextList = addressSchema.transformToElementList() + val simpleTextList = addressSchema.transformToElementList("US") val addressLine1 = SimpleTextSpec( IdentifierSpec.Line1, diff --git a/payments-ui-core/src/test/java/com/stripe/android/ui/core/elements/PostalCodeConfigTest.kt b/payments-ui-core/src/test/java/com/stripe/android/ui/core/elements/PostalCodeConfigTest.kt new file mode 100644 index 00000000000..a10c451d1f6 --- /dev/null +++ b/payments-ui-core/src/test/java/com/stripe/android/ui/core/elements/PostalCodeConfigTest.kt @@ -0,0 +1,54 @@ +package com.stripe.android.ui.core.elements + +import androidx.compose.ui.text.input.KeyboardType +import com.google.common.truth.Truth +import com.stripe.android.core.R +import org.junit.Test + +class PostalCodeConfigTest { + + @Test + fun `verify US postal codes`() { + with(createConfigForCountry("US", KeyboardType.Number)) { + Truth.assertThat(determineStateForInput("").isValid()).isFalse() + Truth.assertThat(determineStateForInput("").isFull()).isFalse() + Truth.assertThat(determineStateForInput("12345").isValid()).isTrue() + Truth.assertThat(determineStateForInput("12345").isFull()).isTrue() + Truth.assertThat(determineStateForInput("1234567890").isValid()).isFalse() + Truth.assertThat(determineStateForInput("1234567890").isFull()).isFalse() + Truth.assertThat(determineStateForInput("abcde").isValid()).isFalse() + Truth.assertThat(determineStateForInput("abcde").isFull()).isFalse() + } + } + + @Test + fun `verify CA postal codes`() { + with(createConfigForCountry("CA")) { + Truth.assertThat(determineStateForInput("").isValid()).isFalse() + Truth.assertThat(determineStateForInput("").isFull()).isFalse() + Truth.assertThat(determineStateForInput("AAA AAA").isValid()).isFalse() + Truth.assertThat(determineStateForInput("AAAAAA").isValid()).isFalse() + Truth.assertThat(determineStateForInput("A0A 0A0").isValid()).isTrue() + Truth.assertThat(determineStateForInput("A0A 0A0").isFull()).isTrue() + Truth.assertThat(determineStateForInput("A0A0A0").isValid()).isTrue() + Truth.assertThat(determineStateForInput("A0A0A0").isFull()).isTrue() + Truth.assertThat(determineStateForInput("A0A0A0A0").isValid()).isFalse() + Truth.assertThat(determineStateForInput("A0A0A0A0").isFull()).isTrue() + } + } + + private fun createConfigForCountry( + country: String, + keyboardType: KeyboardType = KeyboardType.Text + ): PostalCodeConfig { + return PostalCodeConfig( + label = R.string.address_label_postal_code, + country = country, + keyboard = keyboardType + ) + } + + private fun PostalCodeConfig.determineStateForInput(input: String): TextFieldState { + return determineState(filter(input)) + } +}