diff --git a/CHANGELOG.md b/CHANGELOG.md index d5eca112446..423c135a821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,13 @@ # CHANGELOG ## X.X.X + +### PaymentSheet +[FIXED][5321](https://github.com/stripe/stripe-android/pull/5321) Fixed issue with forever loading and mochi library. + ### Payments -[Fixed][5308](https://github.com/stripe/stripe-android/pull/5308) OXXO so that processing is considered a successful terminal state, similar to Konbini and Boleto. -[Fixed][5138](https://github.com/stripe/stripe-android/pull/5138) Fixed an issue where PaymentSheet will show a failure even when 3DS2 Payment/SetupIntent is successful +[FIXED][5308](https://github.com/stripe/stripe-android/pull/5308) OXXO so that processing is considered a successful terminal state, similar to Konbini and Boleto. +[FIXED][5138](https://github.com/stripe/stripe-android/pull/5138) Fixed an issue where PaymentSheet will show a failure even when 3DS2 Payment/SetupIntent is successful ## 20.7.0 - 2022-07-06 * This release adds additional support for Afterpay/Clearpay in PaymentSheet. diff --git a/payments-ui-core/api/payments-ui-core.api b/payments-ui-core/api/payments-ui-core.api index 253938e1b0a..2033cca0083 100644 --- a/payments-ui-core/api/payments-ui-core.api +++ b/payments-ui-core/api/payments-ui-core.api @@ -412,6 +412,7 @@ public final class com/stripe/android/ui/core/elements/IdentifierSpec$Companion public final fun getCardNumber ()Lcom/stripe/android/ui/core/elements/IdentifierSpec; public final fun getCity ()Lcom/stripe/android/ui/core/elements/IdentifierSpec; public final fun getCountry ()Lcom/stripe/android/ui/core/elements/IdentifierSpec; + public final fun getDependentLocality ()Lcom/stripe/android/ui/core/elements/IdentifierSpec; public final fun getEmail ()Lcom/stripe/android/ui/core/elements/IdentifierSpec; public final fun getLine1 ()Lcom/stripe/android/ui/core/elements/IdentifierSpec; public final fun getLine2 ()Lcom/stripe/android/ui/core/elements/IdentifierSpec; @@ -420,6 +421,7 @@ public final class com/stripe/android/ui/core/elements/IdentifierSpec$Companion public final fun getPhone ()Lcom/stripe/android/ui/core/elements/IdentifierSpec; public final fun getPostalCode ()Lcom/stripe/android/ui/core/elements/IdentifierSpec; public final fun getSaveForFutureUse ()Lcom/stripe/android/ui/core/elements/IdentifierSpec; + public final fun getSortingCode ()Lcom/stripe/android/ui/core/elements/IdentifierSpec; public final fun getState ()Lcom/stripe/android/ui/core/elements/IdentifierSpec; public final fun serializer ()Lkotlinx/serialization/KSerializer; } diff --git a/payments-ui-core/src/main/assets/addressinfo/CI.json b/payments-ui-core/src/main/assets/addressinfo/CI.json index ede0e179851..03798275369 100644 --- a/payments-ui-core/src/main/assets/addressinfo/CI.json +++ b/payments-ui-core/src/main/assets/addressinfo/CI.json @@ -20,12 +20,5 @@ "schema": { "nameType": "city" } - }, - { - "type": "sortingCode", - "required": false, - "schema": { - "nameType": "cedex" - } } -] \ No newline at end of file +] diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/FieldValuesToParamsMapConverter.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/FieldValuesToParamsMapConverter.kt index 57844b3de28..d9eb82a200f 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/FieldValuesToParamsMapConverter.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/FieldValuesToParamsMapConverter.kt @@ -92,16 +92,18 @@ class FieldValuesToParamsMapConverter { @VisibleForTesting internal fun addPath(map: MutableMap, keys: List, value: String?) { - val key = keys[0] - if (keys.size == 1) { - map[key] = value - } else { - var mapValueOfKey = map[key] as? MutableMap - if (mapValueOfKey == null) { - mapValueOfKey = mutableMapOf() - map[key] = mapValueOfKey + if (keys.isNotEmpty()) { + val key = keys[0] + if (keys.size == 1) { + map[key] = value + } else { + var mapValueOfKey = map[key] as? MutableMap + if (mapValueOfKey == null) { + mapValueOfKey = mutableMapOf() + map[key] = mapValueOfKey + } + addPath(mapValueOfKey, keys.subList(1, keys.size), value) } - addPath(mapValueOfKey, keys.subList(1, keys.size), value) } } 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 e9f601a0561..e07bbe8b39e 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 @@ -12,63 +12,81 @@ 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 kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.Json import java.io.InputStream import java.util.UUID -@Serializable(with = FieldTypeAsStringSerializer::class) +@Serializable internal enum class FieldType( val serializedValue: String, val identifierSpec: IdentifierSpec, - @StringRes val defaultLabel: Int, - val capitalization: KeyboardCapitalization + @StringRes val defaultLabel: Int ) { + @SerialName("addressLine1") AddressLine1( "addressLine1", IdentifierSpec.Line1, - R.string.address_label_address_line1, - KeyboardCapitalization.Words + R.string.address_label_address_line1 ), + + @SerialName("addressLine2") AddressLine2( "addressLine2", IdentifierSpec.Line2, - R.string.address_label_address_line2, - KeyboardCapitalization.Words + R.string.address_label_address_line2 ), + + @SerialName("locality") Locality( "locality", IdentifierSpec.City, - R.string.address_label_city, - KeyboardCapitalization.Words + R.string.address_label_city + ), + + @SerialName("dependentLocality") + DependentLocality( + "dependentLocality", + IdentifierSpec.DependentLocality, + R.string.address_label_city ), + + @SerialName("postalCode") PostalCode( "postalCode", IdentifierSpec.PostalCode, - R.string.address_label_postal_code, - KeyboardCapitalization.None - ), + R.string.address_label_postal_code + ) { + override fun capitalization() = KeyboardCapitalization.None + }, + + @SerialName("sortingCode") + SortingCode( + "sortingCode", + IdentifierSpec.SortingCode, + R.string.address_label_postal_code + ) { + override fun capitalization() = KeyboardCapitalization.None + }, + + @SerialName("administrativeArea") AdministrativeArea( "administrativeArea", IdentifierSpec.State, - NameType.State.stringResId, - KeyboardCapitalization.Words + NameType.State.stringResId ), + + @SerialName("name") Name( "name", IdentifierSpec.Name, - R.string.address_label_name, - KeyboardCapitalization.Words + R.string.address_label_name ); + open fun capitalization() = KeyboardCapitalization.Words + companion object { fun from(value: String) = values().firstOrNull { it.serializedValue == value @@ -167,7 +185,7 @@ internal class FieldSchema( @SerialName("isNumeric") val isNumeric: Boolean = false, @SerialName("examples") - val examples: List = emptyList(), + val examples: ArrayList = arrayListOf(), @SerialName("nameType") val nameType: NameType // label, ) @@ -186,43 +204,35 @@ private val format = Json { ignoreUnknownKeys = true } internal fun parseAddressesSchema(inputStream: InputStream?) = getJsonStringFromInputStream(inputStream)?.let { - format.decodeFromString>( + format.decodeFromString>( it ) } -private object FieldTypeAsStringSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("FieldType", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: FieldType?) { - encoder.encodeString(value?.serializedValue ?: "") - } - - override fun deserialize(decoder: Decoder): FieldType? { - return FieldType.from(decoder.decodeString()) - } -} - private fun getJsonStringFromInputStream(inputStream: InputStream?) = inputStream?.bufferedReader().use { it?.readText() } internal fun List.transformToElementList(): List { - val countryAddressElements = this.mapNotNull { addressField -> - addressField.type?.let { - SimpleTextElement( - addressField.type.identifierSpec, - SimpleTextFieldController( - SimpleTextFieldConfig( - label = addressField.schema?.nameType?.stringResId ?: it.defaultLabel, - capitalization = it.capitalization, - keyboard = getKeyboard(addressField.schema) - ), - showOptionalLabel = !addressField.required + val countryAddressElements = this + .filterNot { + it.type == FieldType.SortingCode || + it.type == FieldType.DependentLocality + } + .mapNotNull { addressField -> + addressField.type?.let { + SimpleTextElement( + addressField.type.identifierSpec, + SimpleTextFieldController( + SimpleTextFieldConfig( + label = addressField.schema?.nameType?.stringResId ?: it.defaultLabel, + capitalization = it.capitalization(), + keyboard = getKeyboard(addressField.schema) + ), + showOptionalLabel = !addressField.required + ) ) - ) + } } - } // Put it in a single row return combineCityAndPostal(countryAddressElements) diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/IdentifierSpec.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/IdentifierSpec.kt index d4a66330abe..09882fe7761 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/IdentifierSpec.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/IdentifierSpec.kt @@ -38,8 +38,13 @@ data class IdentifierSpec(val v1: String) { val City = IdentifierSpec("billing_details[address][city]") + // FieldValuesToParamsMapConverter will ignore this in the parameter list + val DependentLocality = IdentifierSpec("") + val PostalCode = IdentifierSpec("billing_details[address][postal_code]") + val SortingCode = IdentifierSpec("") + val State = IdentifierSpec("billing_details[address][state]") val Country = IdentifierSpec("billing_details[address][country]") diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/LpmSerializer.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/LpmSerializer.kt index fc92b8ae742..267b1832fd7 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/LpmSerializer.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/LpmSerializer.kt @@ -28,7 +28,7 @@ internal class LpmSerializer { emptyList() } else { try { - format.decodeFromString>(serializer(), str) + format.decodeFromString>(serializer(), str) } catch (e: Exception) { Log.w("STRIPE", "Error parsing LPMs", e) emptyList() diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/SharedDataSpec.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/SharedDataSpec.kt index c840f93a2e9..7cac48a528a 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/SharedDataSpec.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/SharedDataSpec.kt @@ -15,5 +15,5 @@ internal data class SharedDataSpec( // If a form is empty, it must still have an EmptyFormSpec // field to get the form into a complete state (i.e. PayPal). @SerialName("fields") - val fields: List = listOf(EmptyFormSpec) + val fields: ArrayList = arrayListOf(EmptyFormSpec) ) diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/resources/LpmRepository.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/resources/LpmRepository.kt index 5b8486d3115..7b8f013d9d7 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/resources/LpmRepository.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/resources/LpmRepository.kt @@ -103,7 +103,10 @@ class LpmRepository constructor( serverLpmSpecs: String?, force: Boolean = false ) { - if (!isLoaded() || force) { + // If the expectedLpms is different form last time, we still need to reload. + var lpmsNotParsedFromServerSpec = expectedLpms + .filter { !codeToSupportedPaymentMethod.containsKey(it) } + if (!isLoaded() || force || lpmsNotParsedFromServerSpec.isNotEmpty()) { serverSpecLoadingState = ServerSpecState.NoServerSpec(serverLpmSpecs) if (!serverLpmSpecs.isNullOrEmpty()) { serverSpecLoadingState = ServerSpecState.ServerNotParsed(serverLpmSpecs) @@ -116,7 +119,7 @@ class LpmRepository constructor( // If the server does not return specs, or they are not parsed successfully // we will use the LPM on disk if found - val lpmsNotParsedFromServerSpec = expectedLpms + lpmsNotParsedFromServerSpec = expectedLpms .filter { !codeToSupportedPaymentMethod.containsKey(it) } if (lpmsNotParsedFromServerSpec.isNotEmpty()) { val mapFromDisk: Map? = 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 1dad3ec5e02..32e1a4358e2 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 @@ -135,6 +135,23 @@ class TransformAddressToElementTest { } } + @Test + fun `Make sure sorting code and dependent locality is never required`() { + // Sorting code and dependent locality are not actually sent to the server. + supportedCountries.forEach { countryCode -> + val schemaList = readFile("src/main/assets/addressinfo/$countryCode.json") + val invalidNameType = schemaList?.filter { addressSchema -> + addressSchema.required && + ( + addressSchema.type == FieldType.SortingCode || + addressSchema.type == FieldType.DependentLocality + ) + } + invalidNameType?.forEach { println(it.type?.name) } + assertThat(invalidNameType).isEmpty() + } + } + @Test fun `Make sure all country code json files are serializable`() { supportedCountries.forEach { countryCode -> diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetActivity.kt index af209263e2e..556aa84b1f1 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetActivity.kt @@ -163,7 +163,12 @@ internal class PaymentSheetActivity : BaseSheetActivity() { if (config != null) { // We only want to do this if the loading fragment is shown. Otherwise this causes // a new fragment to be created if the activity was destroyed and recreated. - if (supportFragmentManager.fragments.firstOrNull() is PaymentSheetLoadingFragment) { + // If hyperion is an added dependency it is loaded on top of the + // PaymentSheetLoadingFragment + if (supportFragmentManager.fragments + .filterIsInstance() + .isNotEmpty() + ) { val target = if (viewModel.paymentMethods.value.isNullOrEmpty()) { viewModel.updateSelection(null) PaymentSheetViewModel.TransitionTarget.AddPaymentMethodSheet(config)