From 74c3f3b4abfcce4be1b401e0c42e9d2c400d5108 Mon Sep 17 00:00:00 2001 From: Till Hellmund Date: Thu, 17 Oct 2024 16:31:48 -0400 Subject: [PATCH] Pass billing address in `ElementsSessionContext` --- ...FinancialConnectionsPlaygroundViewModel.kt | 1 + .../api/financial-connections.api | 16 ++++ .../FinancialConnectionsSheet.kt | 46 +++++++++++ .../domain/CreateInstantDebitsResult.kt | 9 +++ ...ialConnectionsConsumerSessionRepository.kt | 10 ++- .../FinancialConnectionsSheetViewModelTest.kt | 2 + .../RealCreateInstantDebitsResultTest.kt | 79 ++++++++++++++++++- ...ctionsConsumerSessionRepositoryImplTest.kt | 2 + .../ConsumerPaymentDetailsCreateParams.kt | 14 +++- .../android/repository/ConsumersApiService.kt | 3 + .../ach/USBankAccountFormViewModel.kt | 14 ++++ .../ach/USBankAccountFormViewModelTest.kt | 3 + 12 files changed, 196 insertions(+), 3 deletions(-) diff --git a/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/FinancialConnectionsPlaygroundViewModel.kt b/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/FinancialConnectionsPlaygroundViewModel.kt index 4991b1548c9..8b17b210133 100644 --- a/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/FinancialConnectionsPlaygroundViewModel.kt +++ b/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/FinancialConnectionsPlaygroundViewModel.kt @@ -136,6 +136,7 @@ internal class FinancialConnectionsPlaygroundViewModel( amount = it.amount, currency = it.currency, linkMode = LinkMode.LinkPaymentMethod, + billingAddress = null, ), experience = settings.get().selectedOption, integrationType = settings.get().selectedOption, diff --git a/financial-connections/api/financial-connections.api b/financial-connections/api/financial-connections.api index 741f78df917..fa2b1212fd5 100644 --- a/financial-connections/api/financial-connections.api +++ b/financial-connections/api/financial-connections.api @@ -145,6 +145,22 @@ public final class com/stripe/android/financialconnections/FinancialConnectionsS public synthetic fun newArray (I)[Ljava/lang/Object; } +public final class com/stripe/android/financialconnections/FinancialConnectionsSheet$ElementsSessionContext$BillingAddress$Address$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$ElementsSessionContext$BillingAddress$Address; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$ElementsSessionContext$BillingAddress$Address; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/stripe/android/financialconnections/FinancialConnectionsSheet$ElementsSessionContext$BillingAddress$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$ElementsSessionContext$BillingAddress; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$ElementsSessionContext$BillingAddress; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + public final class com/stripe/android/financialconnections/FinancialConnectionsSheet$ElementsSessionContext$Creator : android/os/Parcelable$Creator { public fun ()V public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$ElementsSessionContext; diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheet.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheet.kt index 40192823c0b..9ed96b1e364 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheet.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheet.kt @@ -46,6 +46,7 @@ class FinancialConnectionsSheet internal constructor( val amount: Long?, val currency: String?, val linkMode: LinkMode?, + val billingAddress: BillingAddress?, ) : Parcelable { val paymentIntentId: String? @@ -69,6 +70,51 @@ class FinancialConnectionsSheet internal constructor( @Parcelize data object DeferredIntent : InitializationMode } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Parcelize + data class BillingAddress( + val name: String? = null, + val phone: String? = null, + val address: Address? = null, + ) : Parcelable { + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Parcelize + data class Address( + val line1: String? = null, + val line2: String? = null, + val postalCode: String? = null, + val city: String? = null, + val state: String? = null, + val country: String? = null, + ) : Parcelable { + + fun consumerApiParams(): Map { + return buildMap { + line1?.let { put("line_1", it) } + line2?.let { put("line_2", it) } + postalCode?.let { put("postal_code", it) } + city?.let { put("locality", it) } + state?.let { put("administrative_area", it) } + country?.let { put("country_code", it) } + }.filter { entry -> + entry.value.isNotBlank() + } + } + } + + fun consumerApiParams(): Map { + val contactParams = buildMap { + name?.let { put("name", it) } + }.filter { entry -> + entry.value.isNotBlank() + } + + val addressParams = address?.consumerApiParams().orEmpty() + return contactParams + addressParams + } + } } /** diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/CreateInstantDebitsResult.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/CreateInstantDebitsResult.kt index d7757da2028..5bc70a9875b 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/CreateInstantDebitsResult.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/CreateInstantDebitsResult.kt @@ -26,13 +26,22 @@ internal class RealCreateInstantDebitsResult @Inject constructor( bankAccountId: String, ): InstantDebitsResult { val consumerSession = consumerSessionProvider.provideConsumerSession() + val clientSecret = requireNotNull(consumerSession?.clientSecret) { "Consumer session client secret cannot be null" } + val billingEmailAddress = requireNotNull(consumerSession?.emailAddress) { + "Consumer session email address cannot be null" + } + + val billingAddress = elementsSessionContext?.billingAddress + val response = consumerRepository.createPaymentDetails( consumerSessionClientSecret = clientSecret, bankAccountId = bankAccountId, + billingAddress = billingAddress, + billingEmailAddress = billingEmailAddress, ) val paymentDetails = response.paymentDetails.filterIsInstance().first() diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsConsumerSessionRepository.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsConsumerSessionRepository.kt index db77e3939ad..5a6c81e426f 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsConsumerSessionRepository.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsConsumerSessionRepository.kt @@ -3,6 +3,7 @@ package com.stripe.android.financialconnections.repository import com.stripe.android.core.Logger import com.stripe.android.core.frauddetection.FraudDetectionDataRepository import com.stripe.android.financialconnections.FinancialConnectionsSheet.ElementsSessionContext +import com.stripe.android.financialconnections.FinancialConnectionsSheet.ElementsSessionContext.BillingAddress import com.stripe.android.financialconnections.domain.IsLinkWithStripe import com.stripe.android.financialconnections.repository.api.FinancialConnectionsConsumersApiService import com.stripe.android.financialconnections.repository.api.ProvideApiRequestOptions @@ -57,6 +58,8 @@ internal interface FinancialConnectionsConsumerSessionRepository { suspend fun createPaymentDetails( bankAccountId: String, consumerSessionClientSecret: String, + billingAddress: BillingAddress?, + billingEmailAddress: String, ): ConsumerPaymentDetails suspend fun sharePaymentDetails( @@ -200,12 +203,16 @@ private class FinancialConnectionsConsumerSessionRepositoryImpl( override suspend fun createPaymentDetails( bankAccountId: String, - consumerSessionClientSecret: String + consumerSessionClientSecret: String, + billingAddress: BillingAddress?, + billingEmailAddress: String, ): ConsumerPaymentDetails { return consumersApiService.createPaymentDetails( consumerSessionClientSecret = consumerSessionClientSecret, paymentDetailsCreateParams = ConsumerPaymentDetailsCreateParams.BankAccount( bankAccountId = bankAccountId, + billingAddress = billingAddress?.consumerApiParams(), + billingEmailAddress = billingEmailAddress, ), requestSurface = requestSurface, requestOptions = provideApiRequestOptions(useConsumerPublishableKey = true), @@ -223,6 +230,7 @@ private class FinancialConnectionsConsumerSessionRepositoryImpl( consumerSessionClientSecret = consumerSessionClientSecret, paymentDetailsId = paymentDetailsId, expectedPaymentMethodType = expectedPaymentMethodType, + billingPhone = elementsSessionContext?.billingAddress?.phone?.takeIf { it.isNotBlank() }, requestSurface = requestSurface, requestOptions = provideApiRequestOptions(useConsumerPublishableKey = false), extraParams = fraudDetectionData, diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt index 176c64dfdbc..44ed8aed7c3 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt @@ -149,6 +149,7 @@ class FinancialConnectionsSheetViewModelTest { amount = 123, currency = "usd", linkMode = LinkMode.LinkPaymentMethod, + billingAddress = null, ), ) ) @@ -180,6 +181,7 @@ class FinancialConnectionsSheetViewModelTest { amount = 123, currency = "usd", linkMode = null, + billingAddress = null, ), ) ) diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/domain/RealCreateInstantDebitsResultTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/domain/RealCreateInstantDebitsResultTest.kt index 7a7b2a8c208..34deeaa02bc 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/domain/RealCreateInstantDebitsResultTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/domain/RealCreateInstantDebitsResultTest.kt @@ -1,6 +1,7 @@ package com.stripe.android.financialconnections.domain import com.stripe.android.financialconnections.FinancialConnectionsSheet.ElementsSessionContext +import com.stripe.android.financialconnections.FinancialConnectionsSheet.ElementsSessionContext.BillingAddress import com.stripe.android.financialconnections.model.PaymentMethod import com.stripe.android.financialconnections.repository.CachedConsumerSession import com.stripe.android.financialconnections.repository.FinancialConnectionsConsumerSessionRepository @@ -11,6 +12,7 @@ import com.stripe.android.model.SharePaymentDetails import kotlinx.coroutines.test.runTest import org.junit.Test import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.verify @@ -87,6 +89,79 @@ class RealCreateInstantDebitsResultTest { ) } + @Test + fun `Passes along billing details to createPaymentMethod if available`() = runTest { + val consumerRepository = makeConsumerSessionRepository() + val repository = makeRepository() + + val billingDetails = BillingAddress( + name = "Some name", + phone = "+15555555555", + address = BillingAddress.Address( + city = "San Francisco", + country = "US", + line1 = "123 Main St", + line2 = "Apt 4", + postalCode = "94111", + state = "CA", + ), + ) + + val createInstantDebitResult = RealCreateInstantDebitsResult( + consumerRepository = consumerRepository, + repository = repository, + consumerSessionProvider = { makeCachedConsumerSession() }, + elementsSessionContext = makeElementsSessionContext( + linkMode = null, + billingAddress = billingDetails, + ), + ) + + createInstantDebitResult("bank_account_id_001") + + verify(repository).createPaymentMethod( + paymentDetailsId = "ba_1234", + consumerSessionClientSecret = "clientSecret", + ) + } + + @Test + fun `Passes along billing details to sharePaymentDetails if available`() = runTest { + val consumerRepository = makeConsumerSessionRepository() + val repository = makeRepository() + + val billingDetails = BillingAddress( + name = "Some name", + phone = "+15555555555", + address = BillingAddress.Address( + city = "San Francisco", + country = "US", + line1 = "123 Main St", + line2 = "Apt 4", + postalCode = "94111", + state = "CA", + ), + ) + + val createInstantDebitResult = RealCreateInstantDebitsResult( + consumerRepository = consumerRepository, + repository = repository, + consumerSessionProvider = { makeCachedConsumerSession() }, + elementsSessionContext = makeElementsSessionContext( + linkMode = LinkMode.LinkCardBrand, + billingAddress = billingDetails, + ), + ) + + createInstantDebitResult("bank_account_id_001") + + verify(consumerRepository).sharePaymentDetails( + paymentDetailsId = "ba_1234", + consumerSessionClientSecret = "clientSecret", + expectedPaymentMethodType = "card", + ) + } + private fun makeConsumerSessionRepository(): FinancialConnectionsConsumerSessionRepository { val consumerPaymentDetails = ConsumerPaymentDetails( paymentDetails = listOf( @@ -103,7 +178,7 @@ class RealCreateInstantDebitsResultTest { ) return mock { - onBlocking { createPaymentDetails(any(), any()) } doReturn consumerPaymentDetails + onBlocking { createPaymentDetails(any(), any(), anyOrNull(), any()) } doReturn consumerPaymentDetails onBlocking { sharePaymentDetails(any(), any(), any()) } doReturn sharePaymentDetails } } @@ -130,12 +205,14 @@ class RealCreateInstantDebitsResultTest { private fun makeElementsSessionContext( linkMode: LinkMode?, + billingAddress: BillingAddress? = null, ): ElementsSessionContext { return ElementsSessionContext( initializationMode = ElementsSessionContext.InitializationMode.PaymentIntent("pi_123"), amount = 100L, currency = "usd", linkMode = linkMode, + billingAddress = billingAddress, ) } } diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/repository/FinancialConnectionsConsumerSessionRepositoryImplTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/repository/FinancialConnectionsConsumerSessionRepositoryImplTest.kt index 44197ad4490..c339c19b4c5 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/repository/FinancialConnectionsConsumerSessionRepositoryImplTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/repository/FinancialConnectionsConsumerSessionRepositoryImplTest.kt @@ -163,6 +163,7 @@ class FinancialConnectionsConsumerSessionRepositoryImplTest { amount = 1234, currency = "cad", linkMode = LinkMode.LinkPaymentMethod, + billingAddress = null, ) ) @@ -427,6 +428,7 @@ class FinancialConnectionsConsumerSessionRepositoryImplTest { consumerSessionClientSecret = anyOrNull(), paymentDetailsId = anyOrNull(), expectedPaymentMethodType = anyOrNull(), + billingPhone = anyOrNull(), requestSurface = anyOrNull(), requestOptions = anyOrNull(), extraParams = eq(fraudParams.params), diff --git a/payments-model/src/main/java/com/stripe/android/model/ConsumerPaymentDetailsCreateParams.kt b/payments-model/src/main/java/com/stripe/android/model/ConsumerPaymentDetailsCreateParams.kt index b3ecb7eb99b..ad06c39b21c 100644 --- a/payments-model/src/main/java/com/stripe/android/model/ConsumerPaymentDetailsCreateParams.kt +++ b/payments-model/src/main/java/com/stripe/android/model/ConsumerPaymentDetailsCreateParams.kt @@ -77,15 +77,27 @@ sealed interface ConsumerPaymentDetailsCreateParams : StripeParamsModel, Parcela @Parcelize data class BankAccount( private val bankAccountId: String, + private val billingAddress: Map?, + private val billingEmailAddress: String, ) : ConsumerPaymentDetailsCreateParams { override fun toParamMap(): Map { - return mapOf( + val billingParams = buildMap { + put("billing_email_address", billingEmailAddress) + + if (!billingAddress.isNullOrEmpty()) { + put("billing_address", billingAddress) + } + } + + val accountParams = mapOf( "type" to "bank_account", "bank_account" to mapOf( "account" to bankAccountId, ), ) + + return accountParams + billingParams } } } diff --git a/payments-model/src/main/java/com/stripe/android/repository/ConsumersApiService.kt b/payments-model/src/main/java/com/stripe/android/repository/ConsumersApiService.kt index 8f6c78fc5b6..a08fb37cbb9 100644 --- a/payments-model/src/main/java/com/stripe/android/repository/ConsumersApiService.kt +++ b/payments-model/src/main/java/com/stripe/android/repository/ConsumersApiService.kt @@ -86,6 +86,7 @@ interface ConsumersApiService { consumerSessionClientSecret: String, paymentDetailsId: String, expectedPaymentMethodType: String, + billingPhone: String?, requestSurface: String, requestOptions: ApiRequest.Options, extraParams: Map, @@ -295,6 +296,7 @@ class ConsumersApiServiceImpl( consumerSessionClientSecret: String, paymentDetailsId: String, expectedPaymentMethodType: String, + billingPhone: String?, requestSurface: String, requestOptions: ApiRequest.Options, extraParams: Map, @@ -312,6 +314,7 @@ class ConsumersApiServiceImpl( "credentials" to mapOf( "consumer_session_client_secret" to consumerSessionClientSecret ), + "billing_phone" to billingPhone, ) + extraParams, ), responseJsonParser = SharePaymentDetailsJsonParser, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormViewModel.kt index c269921665b..a3f700512ca 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormViewModel.kt @@ -508,6 +508,20 @@ internal class USBankAccountFormViewModel @Inject internal constructor( amount = args.formArgs.amount?.value, currency = args.formArgs.amount?.currencyCode, linkMode = args.linkMode, + billingAddress = ElementsSessionContext.BillingAddress( + name = name.value, + phone = phone.value?.let { phoneController.getE164PhoneNumber(it) }, + address = address.value?.let { + ElementsSessionContext.BillingAddress.Address( + line1 = it.line1, + line2 = it.line2, + postalCode = it.postalCode, + city = it.city, + state = it.state, + country = it.country, + ) + }, + ), ), ) } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormViewModelTest.kt index 9ae86207c48..03d1be657a4 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormViewModelTest.kt @@ -1086,6 +1086,9 @@ class USBankAccountFormViewModelTest { amount = 5099, currency = "usd", linkMode = LinkMode.LinkCardBrand, + billingAddress = ElementsSessionContext.BillingAddress( + name = "Jenny Rose", + ), ), ) ),