diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a89bea0893..e148fb93527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,12 @@ ### PaymentSheet -* [CHANGED][5487](https://github.com/stripe/stripe-android/pull/5487) Updated Google Pay button to match new brand guidelines. -* [ADDED][5502](https://github.com/stripe/stripe-android/pull/5502) Added phone number minimum length - validation +* [CHANGED][5487](https://github.com/stripe/stripe-android/pull/5487) Updated Google Pay button to + match new brand guidelines. +* [ADDED][5502](https://github.com/stripe/stripe-android/pull/5502) Added phone number minimum + length validation +* [ADDED][5518](https://github.com/stripe/stripe-android/pull/5518) Added state/province dropdown + for US and Canada. ## 20.11.0 - 2022-08-29 This release adds postal code validation for PaymentSheet and fixed a fileprovider naming bug for Identity. diff --git a/payments-ui-core/api/payments-ui-core.api b/payments-ui-core/api/payments-ui-core.api index e11b1107746..e71afa44075 100644 --- a/payments-ui-core/api/payments-ui-core.api +++ b/payments-ui-core/api/payments-ui-core.api @@ -145,6 +145,45 @@ public final class com/stripe/android/ui/core/elements/AddressType$ShippingExpan public fun toString ()Ljava/lang/String; } +public abstract class com/stripe/android/ui/core/elements/AdministrativeAreaConfig$Country { + public static final field $stable I + public synthetic fun (ILjava/util/List;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getAdministrativeAreas ()Ljava/util/List; + public fun getLabel ()I +} + +public final class com/stripe/android/ui/core/elements/AdministrativeAreaConfig$Country$Canada : com/stripe/android/ui/core/elements/AdministrativeAreaConfig$Country { + public static final field $stable I + public fun ()V + public fun (ILjava/util/List;)V + public synthetic fun (ILjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()I + public final fun component2 ()Ljava/util/List; + public final fun copy (ILjava/util/List;)Lcom/stripe/android/ui/core/elements/AdministrativeAreaConfig$Country$Canada; + public static synthetic fun copy$default (Lcom/stripe/android/ui/core/elements/AdministrativeAreaConfig$Country$Canada;ILjava/util/List;ILjava/lang/Object;)Lcom/stripe/android/ui/core/elements/AdministrativeAreaConfig$Country$Canada; + public fun equals (Ljava/lang/Object;)Z + public fun getAdministrativeAreas ()Ljava/util/List; + public fun getLabel ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/stripe/android/ui/core/elements/AdministrativeAreaConfig$Country$US : com/stripe/android/ui/core/elements/AdministrativeAreaConfig$Country { + public static final field $stable I + public fun ()V + public fun (ILjava/util/List;)V + public synthetic fun (ILjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()I + public final fun component2 ()Ljava/util/List; + public final fun copy (ILjava/util/List;)Lcom/stripe/android/ui/core/elements/AdministrativeAreaConfig$Country$US; + public static synthetic fun copy$default (Lcom/stripe/android/ui/core/elements/AdministrativeAreaConfig$Country$US;ILjava/util/List;ILjava/lang/Object;)Lcom/stripe/android/ui/core/elements/AdministrativeAreaConfig$Country$US; + public fun equals (Ljava/lang/Object;)Z + public fun getAdministrativeAreas ()Ljava/util/List; + public fun getLabel ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/stripe/android/ui/core/elements/AffirmElementUIKt { } 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 9786e2422af..f7de7ad2442 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 @@ -4,6 +4,9 @@ import androidx.annotation.StringRes 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.AdministrativeAreaConfig +import com.stripe.android.ui.core.elements.AdministrativeAreaElement +import com.stripe.android.ui.core.elements.DropdownFieldController import com.stripe.android.ui.core.elements.IdentifierSpec import com.stripe.android.ui.core.elements.PostalCodeConfig import com.stripe.android.ui.core.elements.RowController @@ -13,6 +16,7 @@ import com.stripe.android.ui.core.elements.SectionSingleFieldElement import com.stripe.android.ui.core.elements.SimpleTextElement import com.stripe.android.ui.core.elements.SimpleTextFieldConfig import com.stripe.android.ui.core.elements.SimpleTextFieldController +import com.stripe.android.ui.core.elements.TextFieldConfig import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString @@ -213,44 +217,95 @@ internal fun parseAddressesSchema(inputStream: InputStream?) = private fun getJsonStringFromInputStream(inputStream: InputStream?) = inputStream?.bufferedReader().use { it?.readText() } -internal fun List.transformToElementList(countryCode: String): List { +internal fun List.transformToElementList( + countryCode: String +): List { val countryAddressElements = this .filterNot { it.type == FieldType.SortingCode || it.type == FieldType.DependentLocality } .mapNotNull { addressField -> - addressField.type?.let { - val textFieldConfig = when (it) { - FieldType.PostalCode -> { - 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) - ) - } - } + addressField.type?.toElement( + identifierSpec = addressField.type.identifierSpec, + label = addressField.schema?.nameType?.stringResId + ?: addressField.type.defaultLabel, + capitalization = addressField.type.capitalization(), + keyboardType = getKeyboard(addressField.schema), + countryCode = countryCode, + showOptionalLabel = !addressField.required + ) + } + + // Put it in a single row + return combineCityAndPostal(countryAddressElements) +} - SimpleTextElement( - addressField.type.identifierSpec, - SimpleTextFieldController( - textFieldConfig = textFieldConfig, - showOptionalLabel = !addressField.required +private fun FieldType.toElement( + identifierSpec: IdentifierSpec, + label: Int, + capitalization: KeyboardCapitalization, + keyboardType: KeyboardType, + countryCode: String, + showOptionalLabel: Boolean +): SectionSingleFieldElement { + val simpleTextElement = SimpleTextElement( + identifierSpec, + SimpleTextFieldController( + textFieldConfig = toConfig( + label = label, + capitalization = capitalization, + keyboardType = keyboardType, + countryCode = countryCode + ), + showOptionalLabel = showOptionalLabel + ) + ) + return when (this) { + FieldType.AdministrativeArea -> { + val supportsAdministrativeAreaDropdown = listOf( + "CA", + "US" + ).contains(countryCode) + if (supportsAdministrativeAreaDropdown) { + val country = when (countryCode) { + "CA" -> AdministrativeAreaConfig.Country.Canada() + "US" -> AdministrativeAreaConfig.Country.US() + else -> throw IllegalArgumentException() + } + AdministrativeAreaElement( + identifierSpec, + DropdownFieldController( + AdministrativeAreaConfig(country) ) ) + } else { + simpleTextElement } } + else -> simpleTextElement + } +} - // Put it in a single row - return combineCityAndPostal(countryAddressElements) +private fun FieldType.toConfig( + label: Int, + capitalization: KeyboardCapitalization, + keyboardType: KeyboardType, + countryCode: String +): TextFieldConfig { + return when (this) { + FieldType.PostalCode -> PostalCodeConfig( + label = label, + capitalization = capitalization, + keyboard = keyboardType, + country = countryCode + ) + else -> SimpleTextFieldConfig( + label = label, + capitalization = capitalization, + keyboard = keyboardType + ) + } } private fun combineCityAndPostal(countryAddressElements: List) = diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/AdministrativeAreaConfig.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/AdministrativeAreaConfig.kt new file mode 100644 index 00000000000..0b713e6e42a --- /dev/null +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/AdministrativeAreaConfig.kt @@ -0,0 +1,125 @@ +package com.stripe.android.ui.core.elements + +import androidx.annotation.RestrictTo +import androidx.annotation.StringRes +import com.stripe.android.ui.core.R + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class AdministrativeAreaConfig( + country: Country +) : DropdownConfig { + private val shortAdministrativeAreaNames = country.administrativeAreas.map { it.first } + private val fullAdministrativeAreaNames = country.administrativeAreas.map { it.second } + + override val tinyMode: Boolean = false + override val debugLabel = "administrativeArea" + + @StringRes + override val label = country.label + + override val rawItems = shortAdministrativeAreaNames + + override val displayItems: List = fullAdministrativeAreaNames + + override fun getSelectedItemLabel(index: Int) = fullAdministrativeAreaNames[index] + + override fun convertFromRaw(rawValue: String): String { + return if (shortAdministrativeAreaNames.contains(rawValue)) { + fullAdministrativeAreaNames[shortAdministrativeAreaNames.indexOf(rawValue)] + } else { + fullAdministrativeAreaNames[0] + } + } + + sealed class Country( + open val label: Int, + open val administrativeAreas: List> + ) { + data class Canada( + override val label: Int = R.string.address_label_province, + override val administrativeAreas: List> = listOf( + Pair("AB", "Alberta"), + Pair("BC", "British Columbia"), + Pair("MB", "Manitoba"), + Pair("NB", "New Brunswick"), + Pair("NL", "Newfoundland and Labrador"), + Pair("NT", "Northwest Territories"), + Pair("NS", "Nova Scotia"), + Pair("NU", "Nunavut"), + Pair("ON", "Ontario"), + Pair("PE", "Prince Edward Island"), + Pair("QC", "Quebec"), + Pair("SK", "Saskatchewan"), + Pair("YT", "Yukon") + ) + ) : Country(label, administrativeAreas) + + data class US( + override val label: Int = R.string.address_label_state, + override val administrativeAreas: List> = listOf( + Pair("AL", "Alabama"), + Pair("AK", "Alaska"), + Pair("AS", "American Samoa"), + Pair("AZ", "Arizona"), + Pair("AR", "Arkansas"), + Pair("AA", "Armed Forces (AA)"), + Pair("AE", "Armed Forces (AE)"), + Pair("AP", "Armed Forces (AP)"), + Pair("CA", "California"), + Pair("CO", "Colorado"), + Pair("CT", "Connecticut"), + Pair("DE", "Delaware"), + Pair("DC", "District of Columbia"), + Pair("FL", "Florida"), + Pair("GA", "Georgia"), + Pair("GU", "Guam"), + Pair("HI", "Hawaii"), + Pair("ID", "Idaho"), + Pair("IL", "Illinois"), + Pair("IN", "Indiana"), + Pair("IA", "Iowa"), + Pair("KS", "Kansas"), + Pair("KY", "Kentucky"), + Pair("LA", "Louisiana"), + Pair("ME", "Maine"), + Pair("MH", "Marshal Islands"), + Pair("MD", "Maryland"), + Pair("MA", "Massachusetts"), + Pair("MI", "Michigan"), + Pair("FM", "Micronesia"), + Pair("MN", "Minnesota"), + Pair("MS", "Mississippi"), + Pair("MO", "Missouri"), + Pair("MT", "Montana"), + Pair("NE", "Nebraska"), + Pair("NV", "Nevada"), + Pair("NH", "New Hampshire"), + Pair("NJ", "New Jersey"), + Pair("NM", "New Mexico"), + Pair("NY", "New York"), + Pair("NC", "North Carolina"), + Pair("ND", "North Dakota"), + Pair("MP", "Northern Mariana Islands"), + Pair("OH", "Ohio"), + Pair("OK", "Oklahoma"), + Pair("OR", "Oregon"), + Pair("PW", "Palau"), + Pair("PA", "Pennsylvania"), + Pair("PR", "Puerto Rico"), + Pair("RI", "Rhode Island"), + Pair("SC", "South Carolina"), + Pair("SD", "South Dakota"), + Pair("TN", "Tennessee"), + Pair("TX", "Texas"), + Pair("UT", "Utah"), + Pair("VT", "Vermont"), + Pair("VI", "Virgin Islands"), + Pair("VA", "Virginia"), + Pair("WA", "Washington"), + Pair("WV", "West Virginia"), + Pair("WI", "Wisconsin"), + Pair("WY", "Wyoming") + ) + ) : Country(label, administrativeAreas) + } +} diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/AdministrativeAreaElement.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/AdministrativeAreaElement.kt new file mode 100644 index 00000000000..83ee959bb36 --- /dev/null +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/AdministrativeAreaElement.kt @@ -0,0 +1,9 @@ +package com.stripe.android.ui.core.elements + +import androidx.annotation.RestrictTo + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class AdministrativeAreaElement( + override val identifier: IdentifierSpec, + override val controller: DropdownFieldController +) : SectionSingleFieldElement(identifier) 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 fe3a2992b1a..29c1e996819 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 @@ -4,6 +4,8 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import com.google.common.truth.Truth.assertThat import com.stripe.android.ui.core.R import com.stripe.android.ui.core.address.AddressRepository.Companion.supportedCountries +import com.stripe.android.ui.core.elements.AdministrativeAreaConfig +import com.stripe.android.ui.core.elements.AdministrativeAreaElement import com.stripe.android.ui.core.elements.Capitalization import com.stripe.android.ui.core.elements.IdentifierSpec import com.stripe.android.ui.core.elements.KeyboardType @@ -48,14 +50,6 @@ class TransformAddressToElementTest { showOptionalLabel = false ) - val state = SimpleTextSpec( - IdentifierSpec.State, - R.string.address_label_state, - Capitalization.Words, - KeyboardType.Text, - showOptionalLabel = false - ) - val zip = SimpleTextSpec( IdentifierSpec.PostalCode, R.string.address_label_zip_code, @@ -82,9 +76,15 @@ class TransformAddressToElementTest { cityZipRow.fields[1], zip ) - verifySimpleTextSpecInTextFieldController( - simpleTextList[3] as SectionSingleFieldElement, - state + + // US has state dropdown + val stateDropdownElement = simpleTextList[3] as AdministrativeAreaElement + val stateDropdownController = stateDropdownElement.controller + assertThat(stateDropdownController.displayItems).isEqualTo( + AdministrativeAreaConfig.Country.US().administrativeAreas.map { it.second } + ) + assertThat(stateDropdownController.label.first()).isEqualTo( + R.string.address_label_state ) } diff --git a/payments-ui-core/src/test/java/com/stripe/android/ui/core/elements/AdministrativeAreaConfigTest.kt b/payments-ui-core/src/test/java/com/stripe/android/ui/core/elements/AdministrativeAreaConfigTest.kt new file mode 100644 index 00000000000..0abac0350dd --- /dev/null +++ b/payments-ui-core/src/test/java/com/stripe/android/ui/core/elements/AdministrativeAreaConfigTest.kt @@ -0,0 +1,42 @@ +package com.stripe.android.ui.core.elements + +import com.google.common.truth.Truth +import org.junit.Test + +class AdministrativeAreaConfigTest { + @Test + fun `display values are full names of the state`() { + val config = AdministrativeAreaConfig( + AdministrativeAreaConfig.Country.US() + ) + Truth.assertThat(config.convertFromRaw("CA")) + .isEqualTo("California") + } + + @Test + fun `us displays state`() { + val config = AdministrativeAreaConfig( + AdministrativeAreaConfig.Country.US() + ) + Truth.assertThat(config.label) + .isEqualTo(com.stripe.android.core.R.string.address_label_state) + } + + @Test + fun `canada displays province`() { + val config = AdministrativeAreaConfig( + AdministrativeAreaConfig.Country.Canada() + ) + Truth.assertThat(config.label) + .isEqualTo(com.stripe.android.core.R.string.address_label_province) + } + + @Test + fun `dropdown defaults to first element if raw value doesn't exist`() { + val config = AdministrativeAreaConfig( + AdministrativeAreaConfig.Country.US() + ) + Truth.assertThat(config.convertFromRaw("ABC")) + .isEqualTo("Alabama") + } +}