Skip to content

Commit

Permalink
Add postal validation
Browse files Browse the repository at this point in the history
  • Loading branch information
jameswoo-stripe committed Aug 24, 2022
1 parent d8f7323 commit 06251fc
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 7 deletions.
Expand Up @@ -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) }
}
Expand Down
Expand Up @@ -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
Expand Down Expand Up @@ -212,22 +213,37 @@ internal fun parseAddressesSchema(inputStream: InputStream?) =
private fun getJsonStringFromInputStream(inputStream: InputStream?) =
inputStream?.bufferedReader().use { it?.readText() }

internal fun List<CountryAddressSchema>.transformToElementList(): List<SectionFieldElement> {
internal fun List<CountryAddressSchema>.transformToElementList(countryCode: String): List<SectionFieldElement> {
val countryAddressElements = this
.filterNot {
it.type == FieldType.SortingCode ||
it.type == FieldType.DependentLocality
}
.mapNotNull { addressField ->
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
)
)
Expand Down
@@ -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<TextFieldIcon?> = MutableStateFlow(null),
country: String
) : TextFieldConfig {
override val debugLabel: String = "postal_code_text"
override val visualTransformation: VisualTransformation? = null
override val loading: MutableStateFlow<Boolean> = 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
}
}
}
}
}
Expand Up @@ -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,
Expand Down
@@ -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))
}
}

0 comments on commit 06251fc

Please sign in to comment.