From e6b85a33b0927d789401d4dfd8680a8ef2281a12 Mon Sep 17 00:00:00 2001 From: Samer Alabi Date: Mon, 18 Mar 2024 15:22:31 -0400 Subject: [PATCH 1/6] Add `setup_future_usage` for saved payment methods from passthrough mode. --- .../ConfirmStripeIntentParamsFactory.kt | 15 ++++-- .../ConfirmPaymentIntentParamsFactoryTest.kt | 38 +++++++++++++ .../customersheet/CustomerSheetViewModel.kt | 2 +- .../IntentConfirmationInterceptor.kt | 25 ++++++--- .../IntentConfirmationInterceptorKtx.kt | 2 +- .../android/paymentsheet/LinkHandler.kt | 11 +++- .../paymentsheet/model/PaymentSelection.kt | 1 + ...efaultIntentConfirmationInterceptorTest.kt | 54 +++++++++++++++---- .../android/paymentsheet/LinkHandlerTest.kt | 10 +++- .../FakeIntentConfirmationInterceptor.kt | 2 +- 10 files changed, 131 insertions(+), 29 deletions(-) diff --git a/payments-core/src/main/java/com/stripe/android/ConfirmStripeIntentParamsFactory.kt b/payments-core/src/main/java/com/stripe/android/ConfirmStripeIntentParamsFactory.kt index dc420273190..47ee204eb7c 100644 --- a/payments-core/src/main/java/com/stripe/android/ConfirmStripeIntentParamsFactory.kt +++ b/payments-core/src/main/java/com/stripe/android/ConfirmStripeIntentParamsFactory.kt @@ -17,7 +17,10 @@ import com.stripe.android.model.SetupIntent @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) sealed class ConfirmStripeIntentParamsFactory { - abstract fun create(paymentMethod: PaymentMethod): T + abstract fun create( + paymentMethod: PaymentMethod, + requiresSaveOnConfirmation: Boolean = false, + ): T abstract fun create( createParams: PaymentMethodCreateParams, @@ -29,7 +32,7 @@ sealed class ConfirmStripeIntentParamsFactory fun createFactory( clientSecret: String, - shipping: ConfirmPaymentIntentParams.Shipping? + shipping: ConfirmPaymentIntentParams.Shipping?, ) = when { PaymentIntent.ClientSecret.isMatch(clientSecret) -> { ConfirmPaymentIntentParamsFactory(clientSecret, shipping) @@ -49,14 +52,16 @@ internal class ConfirmPaymentIntentParamsFactory( private val shipping: ConfirmPaymentIntentParams.Shipping? ) : ConfirmStripeIntentParamsFactory() { - override fun create(paymentMethod: PaymentMethod): ConfirmPaymentIntentParams { + override fun create(paymentMethod: PaymentMethod, requiresSaveOnConfirmation: Boolean): ConfirmPaymentIntentParams { return ConfirmPaymentIntentParams.createWithPaymentMethodId( paymentMethodId = paymentMethod.id.orEmpty(), clientSecret = clientSecret, paymentMethodOptions = when (paymentMethod.type) { PaymentMethod.Type.Card -> { PaymentMethodOptionsParams.Card( - setupFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.Blank + setupFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.OffSession?.takeIf { + requiresSaveOnConfirmation + } ?: ConfirmPaymentIntentParams.SetupFutureUsage.Blank ) } PaymentMethod.Type.USBankAccount -> { @@ -90,7 +95,7 @@ internal class ConfirmPaymentIntentParamsFactory( internal class ConfirmSetupIntentParamsFactory( private val clientSecret: String, ) : ConfirmStripeIntentParamsFactory() { - override fun create(paymentMethod: PaymentMethod): ConfirmSetupIntentParams { + override fun create(paymentMethod: PaymentMethod, requiresSaveOnConfirmation: Boolean): ConfirmSetupIntentParams { return ConfirmSetupIntentParams.create( paymentMethodId = paymentMethod.id.orEmpty(), clientSecret = clientSecret, diff --git a/payments-core/src/test/java/com/stripe/android/ConfirmPaymentIntentParamsFactoryTest.kt b/payments-core/src/test/java/com/stripe/android/ConfirmPaymentIntentParamsFactoryTest.kt index 8e001316d08..03942424494 100644 --- a/payments-core/src/test/java/com/stripe/android/ConfirmPaymentIntentParamsFactoryTest.kt +++ b/payments-core/src/test/java/com/stripe/android/ConfirmPaymentIntentParamsFactoryTest.kt @@ -117,6 +117,44 @@ class ConfirmPaymentIntentParamsFactoryTest { assertThat(result.shipping).isEqualTo(shippingDetails) } + @Test + fun `create() with saved card and does not require save on confirmation`() { + val factoryWithConfig = ConfirmPaymentIntentParamsFactory( + clientSecret = CLIENT_SECRET, + shipping = null, + ) + + val result = factoryWithConfig.create( + paymentMethod = PaymentMethodFixtures.CARD_PAYMENT_METHOD, + requiresSaveOnConfirmation = false + ) + + assertThat(result.paymentMethodOptions).isEqualTo( + PaymentMethodOptionsParams.Card( + setupFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.Blank + ) + ) + } + + @Test + fun `create() with saved card and requires save on confirmation`() { + val factoryWithConfig = ConfirmPaymentIntentParamsFactory( + clientSecret = CLIENT_SECRET, + shipping = null, + ) + + val result = factoryWithConfig.create( + paymentMethod = PaymentMethodFixtures.CARD_PAYMENT_METHOD, + requiresSaveOnConfirmation = true + ) + + assertThat(result.paymentMethodOptions).isEqualTo( + PaymentMethodOptionsParams.Card( + setupFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.OffSession + ) + ) + } + private companion object { private const val CLIENT_SECRET = "client_secret" } diff --git a/paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheetViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheetViewModel.kt index 10c91b753ab..5530da78d92 100644 --- a/paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheetViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheetViewModel.kt @@ -954,7 +954,7 @@ internal class CustomerSheetViewModel( ), paymentMethod = paymentMethod, shippingValues = null, - customerRequestedSave = true, + requiresSaveOnConfirmation = true, ) unconfirmedPaymentMethod = paymentMethod diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/IntentConfirmationInterceptor.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/IntentConfirmationInterceptor.kt index 399505d963b..de986c1b60b 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/IntentConfirmationInterceptor.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/IntentConfirmationInterceptor.kt @@ -70,7 +70,7 @@ internal interface IntentConfirmationInterceptor { initializationMode: PaymentSheet.InitializationMode, paymentMethod: PaymentMethod, shippingValues: ConfirmPaymentIntentParams.Shipping?, - customerRequestedSave: Boolean, + requiresSaveOnConfirmation: Boolean, ): NextStep companion object { @@ -143,7 +143,7 @@ internal class DefaultIntentConfirmationInterceptor @Inject constructor( initializationMode: PaymentSheet.InitializationMode, paymentMethod: PaymentMethod, shippingValues: ConfirmPaymentIntentParams.Shipping?, - customerRequestedSave: Boolean, + requiresSaveOnConfirmation: Boolean, ): NextStep { return when (initializationMode) { is PaymentSheet.InitializationMode.DeferredIntent -> { @@ -151,7 +151,7 @@ internal class DefaultIntentConfirmationInterceptor @Inject constructor( intentConfiguration = initializationMode.intentConfiguration, paymentMethod = paymentMethod, shippingValues = shippingValues, - customerRequestedSave = customerRequestedSave, + shouldSavePaymentMethod = requiresSaveOnConfirmation, ) } @@ -160,6 +160,7 @@ internal class DefaultIntentConfirmationInterceptor @Inject constructor( clientSecret = initializationMode.clientSecret, shippingValues = shippingValues, paymentMethod = paymentMethod, + requiresSaveOnConfirmation = requiresSaveOnConfirmation, isDeferred = false, ) } @@ -169,6 +170,7 @@ internal class DefaultIntentConfirmationInterceptor @Inject constructor( clientSecret = initializationMode.clientSecret, shippingValues = shippingValues, paymentMethod = paymentMethod, + requiresSaveOnConfirmation = requiresSaveOnConfirmation, isDeferred = false, ) } @@ -198,7 +200,7 @@ internal class DefaultIntentConfirmationInterceptor @Inject constructor( intentConfiguration = intentConfiguration, paymentMethod = paymentMethod, shippingValues = shippingValues, - customerRequestedSave = customerRequestedSave, + shouldSavePaymentMethod = customerRequestedSave, ) }, onFailure = { error -> @@ -214,7 +216,7 @@ internal class DefaultIntentConfirmationInterceptor @Inject constructor( intentConfiguration: PaymentSheet.IntentConfiguration, paymentMethod: PaymentMethod, shippingValues: ConfirmPaymentIntentParams.Shipping?, - customerRequestedSave: Boolean, + shouldSavePaymentMethod: Boolean, ): NextStep { return when (val callback = IntentConfirmationInterceptor.createIntentCallback) { is CreateIntentCallback -> { @@ -222,7 +224,7 @@ internal class DefaultIntentConfirmationInterceptor @Inject constructor( createIntentCallback = callback, intentConfiguration = intentConfiguration, paymentMethod = paymentMethod, - shouldSavePaymentMethod = customerRequestedSave, + shouldSavePaymentMethod = shouldSavePaymentMethod, shippingValues = shippingValues, ) } @@ -293,7 +295,13 @@ internal class DefaultIntentConfirmationInterceptor @Inject constructor( NextStep.HandleNextAction(clientSecret) } else { DeferredIntentValidator.validate(intent, intentConfiguration, isFlowController) - createConfirmStep(clientSecret, shippingValues, paymentMethod, isDeferred = true) + createConfirmStep( + clientSecret, + shippingValues, + paymentMethod, + requiresSaveOnConfirmation = false, + isDeferred = true + ) } }.getOrElse { error -> NextStep.Fail( @@ -314,6 +322,7 @@ internal class DefaultIntentConfirmationInterceptor @Inject constructor( clientSecret: String, shippingValues: ConfirmPaymentIntentParams.Shipping?, paymentMethod: PaymentMethod, + requiresSaveOnConfirmation: Boolean, isDeferred: Boolean, ): NextStep.Confirm { val factory = ConfirmStripeIntentParamsFactory.createFactory( @@ -321,7 +330,7 @@ internal class DefaultIntentConfirmationInterceptor @Inject constructor( shipping = shippingValues, ) - val confirmParams = factory.create(paymentMethod) + val confirmParams = factory.create(paymentMethod, requiresSaveOnConfirmation) return NextStep.Confirm( confirmParams = confirmParams, isDeferred = isDeferred, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/IntentConfirmationInterceptorKtx.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/IntentConfirmationInterceptorKtx.kt index f21e7a2bfa8..c308e31e6e1 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/IntentConfirmationInterceptorKtx.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/IntentConfirmationInterceptorKtx.kt @@ -24,7 +24,7 @@ internal suspend fun IntentConfirmationInterceptor.intercept( initializationMode = initializationMode, paymentMethod = paymentSelection.paymentMethod, shippingValues = shippingValues, - customerRequestedSave = false, + requiresSaveOnConfirmation = paymentSelection.requiresSaveOnConfirmation, ) } else -> { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/LinkHandler.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/LinkHandler.kt index 3d1879d0f38..9591c9c92cb 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/LinkHandler.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/LinkHandler.kt @@ -17,6 +17,7 @@ import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.model.PaymentMethod import com.stripe.android.model.PaymentMethod.Type.Card import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.model.wallets.Wallet import com.stripe.android.payments.paymentlauncher.PaymentResult import com.stripe.android.paymentsheet.model.PaymentSelection import com.stripe.android.paymentsheet.state.LinkState @@ -132,6 +133,7 @@ internal class LinkHandler @Inject constructor( completeLinkInlinePayment( configuration, params, + paymentSelection.customerRequestedSave == PaymentSelection.CustomerRequestedSave.RequestReuse, userInput is UserInput.SignIn && shouldCompleteLinkInlineFlow ) } @@ -168,6 +170,7 @@ internal class LinkHandler @Inject constructor( private suspend fun completeLinkInlinePayment( configuration: LinkConfiguration, paymentMethodCreateParams: PaymentMethodCreateParams, + customerRequestedSave: Boolean, shouldCompleteLinkInlineFlow: Boolean ) { if (shouldCompleteLinkInlineFlow) { @@ -195,10 +198,16 @@ internal class LinkHandler @Inject constructor( paymentMethod = PaymentMethod.Builder() .setId(linkPaymentDetails.paymentDetails.id) .setCode(paymentMethodCreateParams.typeCode) - .setCard(PaymentMethod.Card(last4 = last4)) + .setCard( + PaymentMethod.Card( + last4 = last4, + wallet = Wallet.LinkWallet(last4) + ) + ) .setType(PaymentMethod.Type.Card) .build(), walletType = PaymentSelection.Saved.WalletType.Link, + requiresSaveOnConfirmation = customerRequestedSave ) } null -> null diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt index a6946e7ba7c..33a770dad99 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt @@ -69,6 +69,7 @@ internal sealed class PaymentSelection : Parcelable { data class Saved( val paymentMethod: PaymentMethod, val walletType: WalletType? = null, + val requiresSaveOnConfirmation: Boolean = false, ) : PaymentSelection() { enum class WalletType(val paymentSelection: PaymentSelection) { diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/DefaultIntentConfirmationInterceptorTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/DefaultIntentConfirmationInterceptorTest.kt index a797ab8051a..86d7dc61f2a 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/DefaultIntentConfirmationInterceptorTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/DefaultIntentConfirmationInterceptorTest.kt @@ -11,9 +11,11 @@ import com.stripe.android.model.PaymentMethod import com.stripe.android.model.PaymentMethodCreateParams import com.stripe.android.model.PaymentMethodCreateParamsFixtures import com.stripe.android.model.PaymentMethodFixtures +import com.stripe.android.model.PaymentMethodOptionsParams import com.stripe.android.model.StripeIntent import com.stripe.android.networking.StripeRepository import com.stripe.android.paymentsheet.PaymentSheet.InitializationMode +import com.stripe.android.paymentsheet.model.PaymentSelection import com.stripe.android.testing.AbsFakeStripeRepository import com.stripe.android.utils.IntentConfirmationInterceptorTestRule import kotlinx.coroutines.test.runTest @@ -52,7 +54,7 @@ class DefaultIntentConfirmationInterceptorTest { initializationMode = InitializationMode.PaymentIntent("pi_1234_secret_4321"), paymentMethod = paymentMethod, shippingValues = null, - customerRequestedSave = false, + requiresSaveOnConfirmation = false, ) val confirmNextStep = nextStep as? IntentConfirmationInterceptor.NextStep.Confirm @@ -88,6 +90,38 @@ class DefaultIntentConfirmationInterceptorTest { assertThat(confirmParams?.paymentMethodCreateParams).isEqualTo(createParams) } + @Test + fun `Returns confirm params with setup future usage set to off session when requires save on confirmation`() = + runTest { + val interceptor = DefaultIntentConfirmationInterceptor( + context = context, + stripeRepository = object : AbsFakeStripeRepository() {}, + publishableKeyProvider = { "pk" }, + stripeAccountIdProvider = { null }, + isFlowController = false, + ) + + val nextStep = interceptor.intercept( + initializationMode = InitializationMode.PaymentIntent("pi_1234_secret_4321"), + paymentSelection = PaymentSelection.Saved( + paymentMethod = PaymentMethodFixtures.CARD_PAYMENT_METHOD, + requiresSaveOnConfirmation = true + ), + shippingValues = null, + ) + + val confirmNextStep = nextStep as? IntentConfirmationInterceptor.NextStep.Confirm + val confirmParams = confirmNextStep?.confirmParams as? ConfirmPaymentIntentParams + + assertThat( + confirmParams?.paymentMethodOptions + ).isEqualTo( + PaymentMethodOptionsParams.Card( + setupFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.OffSession + ) + ) + } + @Test fun `Fails if invoked without a confirm callback for existing payment method`() = runTest { val interceptor = DefaultIntentConfirmationInterceptor( @@ -110,7 +144,7 @@ class DefaultIntentConfirmationInterceptorTest { ), paymentMethod = PaymentMethodFixtures.CARD_PAYMENT_METHOD, shippingValues = null, - customerRequestedSave = false, + requiresSaveOnConfirmation = false, ) } @@ -220,7 +254,7 @@ class DefaultIntentConfirmationInterceptorTest { initializationMode = InitializationMode.DeferredIntent(mock()), paymentMethod = PaymentMethodFixtures.CARD_PAYMENT_METHOD, shippingValues = null, - customerRequestedSave = false, + requiresSaveOnConfirmation = false, ) assertThat(nextStep).isEqualTo( @@ -249,7 +283,7 @@ class DefaultIntentConfirmationInterceptorTest { initializationMode = InitializationMode.DeferredIntent(mock()), paymentMethod = PaymentMethodFixtures.CARD_PAYMENT_METHOD, shippingValues = null, - customerRequestedSave = false, + requiresSaveOnConfirmation = false, ) assertThat(nextStep).isEqualTo( @@ -276,7 +310,7 @@ class DefaultIntentConfirmationInterceptorTest { initializationMode = InitializationMode.DeferredIntent(mock()), paymentMethod = PaymentMethodFixtures.CARD_PAYMENT_METHOD, shippingValues = null, - customerRequestedSave = false, + requiresSaveOnConfirmation = false, ) assertThat(nextStep).isEqualTo( @@ -324,7 +358,7 @@ class DefaultIntentConfirmationInterceptorTest { ), paymentMethod = paymentMethod, shippingValues = null, - customerRequestedSave = false, + requiresSaveOnConfirmation = false, ) assertThat(nextStep).isInstanceOf(IntentConfirmationInterceptor.NextStep.Confirm::class.java) @@ -363,7 +397,7 @@ class DefaultIntentConfirmationInterceptorTest { ), paymentMethod = paymentMethod, shippingValues = null, - customerRequestedSave = false, + requiresSaveOnConfirmation = false, ) assertThat(nextStep).isEqualTo( @@ -408,7 +442,7 @@ class DefaultIntentConfirmationInterceptorTest { ), paymentMethod = paymentMethod, shippingValues = null, - customerRequestedSave = false, + requiresSaveOnConfirmation = false, ) assertThat(nextStep).isEqualTo( @@ -457,7 +491,7 @@ class DefaultIntentConfirmationInterceptorTest { ), paymentMethod = paymentMethod, shippingValues = null, - customerRequestedSave = input, + requiresSaveOnConfirmation = input, ) } @@ -492,7 +526,7 @@ class DefaultIntentConfirmationInterceptorTest { ), paymentMethod = paymentMethod, shippingValues = null, - customerRequestedSave = false, + requiresSaveOnConfirmation = false, ) verify(stripeRepository, never()).retrieveStripeIntent(any(), any(), any()) diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt index a8e07c66fe1..ffdbc6b67af 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt @@ -3,7 +3,6 @@ package com.stripe.android.paymentsheet import androidx.lifecycle.SavedStateHandle import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test -import app.cash.turbine.testIn import app.cash.turbine.turbineScope import com.google.common.truth.Truth.assertThat import com.stripe.android.link.LinkActivityResult @@ -22,6 +21,7 @@ import com.stripe.android.model.CardBrand import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.model.PaymentMethod import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.model.wallets.Wallet import com.stripe.android.payments.paymentlauncher.PaymentResult import com.stripe.android.paymentsheet.model.PaymentSelection import com.stripe.android.paymentsheet.state.LinkState @@ -322,10 +322,16 @@ class LinkHandlerTest { paymentMethod = PaymentMethod.Builder() .setId("pm_123") .setCode("card") - .setCard(PaymentMethod.Card(last4 = "4242")) + .setCard( + PaymentMethod.Card( + last4 = "4242", + wallet = Wallet.LinkWallet("4242") + ) + ) .setType(PaymentMethod.Type.Card) .build(), walletType = PaymentSelection.Saved.WalletType.Link, + requiresSaveOnConfirmation = true, ), ) ) diff --git a/paymentsheet/src/test/java/com/stripe/android/utils/FakeIntentConfirmationInterceptor.kt b/paymentsheet/src/test/java/com/stripe/android/utils/FakeIntentConfirmationInterceptor.kt index be295a7e126..50ab38649b2 100644 --- a/paymentsheet/src/test/java/com/stripe/android/utils/FakeIntentConfirmationInterceptor.kt +++ b/paymentsheet/src/test/java/com/stripe/android/utils/FakeIntentConfirmationInterceptor.kt @@ -57,7 +57,7 @@ internal class FakeIntentConfirmationInterceptor : IntentConfirmationInterceptor initializationMode: PaymentSheet.InitializationMode, paymentMethod: PaymentMethod, shippingValues: ConfirmPaymentIntentParams.Shipping?, - customerRequestedSave: Boolean, + requiresSaveOnConfirmation: Boolean, ): IntentConfirmationInterceptor.NextStep { return channel.receive() } From 6dfb769fbe87f6a8b9c190496bc3884c4b05ba7c Mon Sep 17 00:00:00 2001 From: Samer Alabi Date: Mon, 18 Mar 2024 15:51:52 -0400 Subject: [PATCH 2/6] Add PM mode support --- .../android/paymentsheet/LinkHandler.kt | 9 +-- .../paymentsheet/model/PaymentSelection.kt | 8 +-- .../android/paymentsheet/LinkHandlerTest.kt | 70 ++++++++++++++++++- .../analytics/PaymentSheetEventTest.kt | 6 +- .../DefaultFlowControllerTest.kt | 9 ++- 5 files changed, 87 insertions(+), 15 deletions(-) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/LinkHandler.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/LinkHandler.kt index 9591c9c92cb..e143b522bb9 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/LinkHandler.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/LinkHandler.kt @@ -133,7 +133,7 @@ internal class LinkHandler @Inject constructor( completeLinkInlinePayment( configuration, params, - paymentSelection.customerRequestedSave == PaymentSelection.CustomerRequestedSave.RequestReuse, + paymentSelection.customerRequestedSave, userInput is UserInput.SignIn && shouldCompleteLinkInlineFlow ) } @@ -170,7 +170,7 @@ internal class LinkHandler @Inject constructor( private suspend fun completeLinkInlinePayment( configuration: LinkConfiguration, paymentMethodCreateParams: PaymentMethodCreateParams, - customerRequestedSave: Boolean, + customerRequestedSave: PaymentSelection.CustomerRequestedSave, shouldCompleteLinkInlineFlow: Boolean ) { if (shouldCompleteLinkInlineFlow) { @@ -184,7 +184,7 @@ internal class LinkHandler @Inject constructor( val paymentSelection = when (linkPaymentDetails) { is LinkPaymentDetails.New -> { - PaymentSelection.New.LinkInline(linkPaymentDetails) + PaymentSelection.New.LinkInline(linkPaymentDetails, customerRequestedSave) } is LinkPaymentDetails.Saved -> { val last4 = when (val paymentDetails = linkPaymentDetails.paymentDetails) { @@ -207,7 +207,8 @@ internal class LinkHandler @Inject constructor( .setType(PaymentMethod.Type.Card) .build(), walletType = PaymentSelection.Saved.WalletType.Link, - requiresSaveOnConfirmation = customerRequestedSave + requiresSaveOnConfirmation = customerRequestedSave == + PaymentSelection.CustomerRequestedSave.RequestReuse, ) } null -> null diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt index 33a770dad99..143b884a0f9 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt @@ -179,10 +179,10 @@ internal sealed class PaymentSelection : Parcelable { } @Parcelize - data class LinkInline(val linkPaymentDetails: LinkPaymentDetails) : New() { - @IgnoredOnParcel - override val customerRequestedSave = CustomerRequestedSave.NoRequest - + data class LinkInline( + val linkPaymentDetails: LinkPaymentDetails, + override val customerRequestedSave: CustomerRequestedSave, + ) : New() { @IgnoredOnParcel private val paymentDetails = linkPaymentDetails.paymentDetails diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt index ffdbc6b67af..57740df030a 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt @@ -263,14 +263,80 @@ class LinkHandlerTest { whenever(linkConfigurationCoordinator.signInWithUserInput(any(), any())) .thenReturn(Result.success(true)) testScope.launch { - handler.payWithLinkInline(userInput, cardSelection(), shouldCompleteLinkFlow) + handler.payWithLinkInline( + userInput, + cardSelection().copy(customerRequestedSave = PaymentSelection.CustomerRequestedSave.NoRequest), + shouldCompleteLinkFlow + ) } accountStatusFlow.emit(AccountStatus.SignedOut) assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.Started) assertThat(accountStatusTurbine.awaitItem()).isEqualTo(AccountStatus.SignedOut) accountStatusFlow.emit(AccountStatus.Verified) - assertThat(awaitItem()).isInstanceOf(LinkHandler.ProcessingState.PaymentDetailsCollected::class.java) + + val processingState = awaitItem() + assertThat(processingState).isInstanceOf(LinkHandler.ProcessingState.PaymentDetailsCollected::class.java) + + val selection = (processingState as LinkHandler.ProcessingState.PaymentDetailsCollected).paymentSelection + assertThat(selection).isInstanceOf(PaymentSelection.New.LinkInline::class.java) + + val linkInlineSelection = selection as PaymentSelection.New.LinkInline + assertThat(linkInlineSelection.customerRequestedSave).isEqualTo( + PaymentSelection.CustomerRequestedSave.NoRequest + ) + + assertThat(accountStatusTurbine.awaitItem()).isEqualTo(AccountStatus.Verified) + verify(linkLauncher, never()).present(eq(configuration)) + verify(linkStore).markLinkAsUsed() + } + + processingStateTurbine.cancelAndIgnoreRemainingEvents() // Validated above. + } + + @Test + fun `payWithLinkInline collects payment details and requests payment is saved`() = runLinkInlineTest( + accountStatusFlow = MutableSharedFlow(replay = 0), + shouldCompleteLinkFlowValues = listOf(false), + ) { + val userInput = UserInput.SignIn(email = "example@example.com") + + accountStatusTurbine.ensureAllEventsConsumed() + handler.setupLink( + state = LinkState( + loginState = LinkState.LoginState.LoggedOut, + configuration = configuration, + ) + ) + + handler.processingState.test { + ensureAllEventsConsumed() // Begin with no events. + whenever(linkConfigurationCoordinator.signInWithUserInput(any(), any())) + .thenReturn(Result.success(true)) + testScope.launch { + handler.payWithLinkInline( + userInput, + cardSelection().copy(customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestReuse), + shouldCompleteLinkFlow + ) + } + accountStatusFlow.emit(AccountStatus.SignedOut) + assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.Started) + assertThat(accountStatusTurbine.awaitItem()).isEqualTo(AccountStatus.SignedOut) + + accountStatusFlow.emit(AccountStatus.Verified) + + val processingState = awaitItem() + assertThat(processingState).isInstanceOf(LinkHandler.ProcessingState.PaymentDetailsCollected::class.java) + + val selection = (processingState as LinkHandler.ProcessingState.PaymentDetailsCollected).paymentSelection + assertThat(selection).isInstanceOf(PaymentSelection.New.LinkInline::class.java) + + val linkInlineSelection = selection as PaymentSelection.New.LinkInline + assertThat(linkInlineSelection.customerRequestedSave).isEqualTo( + PaymentSelection.CustomerRequestedSave.RequestReuse + ) + assertThat(accountStatusTurbine.awaitItem()).isEqualTo(AccountStatus.Verified) verify(linkLauncher, never()).present(eq(configuration)) verify(linkStore).markLinkAsUsed() diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEventTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEventTest.kt index 54bd811dbb7..74cec2a6ae4 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEventTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEventTest.kt @@ -394,7 +394,8 @@ class PaymentSheetEventTest { PaymentDetailsFixtures.CONSUMER_SINGLE_PAYMENT_DETAILS.paymentDetails.first(), mock(), mock() - ) + ), + PaymentSelection.CustomerRequestedSave.NoRequest, ), duration = 1.milliseconds, result = PaymentSheetEvent.Payment.Result.Success, @@ -575,7 +576,8 @@ class PaymentSheetEventTest { PaymentDetailsFixtures.CONSUMER_SINGLE_PAYMENT_DETAILS.paymentDetails.first(), mock(), mock() - ) + ), + PaymentSelection.CustomerRequestedSave.NoRequest, ), duration = 1.milliseconds, result = PaymentSheetEvent.Payment.Result.Failure( diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowControllerTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowControllerTest.kt index feebf10f20f..fab7f4cb902 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowControllerTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowControllerTest.kt @@ -721,7 +721,8 @@ internal class DefaultFlowControllerTest { PaymentDetailsFixtures.CONSUMER_SINGLE_PAYMENT_DETAILS.paymentDetails.first(), mock(), PaymentMethodCreateParamsFixtures.DEFAULT_CARD - ) + ), + PaymentSelection.CustomerRequestedSave.NoRequest, ) flowController.onPaymentOptionResult( @@ -760,7 +761,8 @@ internal class DefaultFlowControllerTest { PaymentDetailsFixtures.CONSUMER_SINGLE_PAYMENT_DETAILS.paymentDetails.first(), mock(), PaymentMethodCreateParamsFixtures.DEFAULT_CARD - ) + ), + PaymentSelection.CustomerRequestedSave.NoRequest, ) flowController.onPaymentOptionResult( @@ -810,7 +812,8 @@ internal class DefaultFlowControllerTest { PaymentDetailsFixtures.CONSUMER_SINGLE_PAYMENT_DETAILS.paymentDetails.first(), mock(), PaymentMethodCreateParamsFixtures.DEFAULT_CARD - ) + ), + PaymentSelection.CustomerRequestedSave.NoRequest, ) flowController.onPaymentOptionResult( From 0f649b126acc77a2b07a90a8f9deebe734167e21 Mon Sep 17 00:00:00 2001 From: Samer Alabi Date: Mon, 18 Mar 2024 17:32:22 -0400 Subject: [PATCH 3/6] Add network tests for `setup_future_usage` for Link. --- .../stripe/android/paymentsheet/LinkTest.kt | 239 ++++++++++++++++++ .../android/paymentsheet/PaymentSheetPage.kt | 16 +- .../payment-methods-get-success-empty.json | 6 + .../paymentsheet/model/PaymentSelection.kt | 4 +- ...efaultIntentConfirmationInterceptorTest.kt | 20 +- 5 files changed, 272 insertions(+), 13 deletions(-) create mode 100644 paymentsheet/src/androidTest/resources/payment-methods-get-success-empty.json diff --git a/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/LinkTest.kt b/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/LinkTest.kt index d306c4e55df..26f9041c81a 100644 --- a/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/LinkTest.kt +++ b/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/LinkTest.kt @@ -20,6 +20,7 @@ import com.stripe.android.paymentsheet.utils.LinkIntegrationType import com.stripe.android.paymentsheet.utils.LinkIntegrationTypeProvider import com.stripe.android.paymentsheet.utils.assertCompleted import com.stripe.android.paymentsheet.utils.runLinkTest +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -122,6 +123,121 @@ internal class LinkTest { page.clickPrimaryButton() } + @Test + fun testSuccessfulCardPaymentWithLinkSignUpAndSaveForFutureUsage() = + activityScenarioRule.runLinkTest( + integrationType = integrationType, + paymentOptionCallback = { paymentOption -> + assertThat(paymentOption?.label).endsWith("4242") + + @Suppress("DEPRECATION") + assertThat(paymentOption?.drawableResourceId).isEqualTo(R.drawable.stripe_ic_paymentsheet_link) + }, + resultCallback = ::assertCompleted, + ) { testContext -> + networkRule.enqueue( + host("api.stripe.com"), + method("GET"), + path("/v1/elements/sessions"), + ) { response -> + response.testBodyFromFile("elements-sessions-requires_payment_method.json") + } + + networkRule.enqueue( + host("api.stripe.com"), + method("GET"), + path("/v1/customers/cus_1"), + ) { response -> + response.testBodyFromFile("customer-get-success.json") + } + + networkRule.enqueue( + host("api.stripe.com"), + method("GET"), + path("/v1/payment_methods"), + ) { response -> + response.testBodyFromFile("payment-methods-get-success-empty.json") + } + + networkRule.enqueue( + method("POST"), + path("/v1/consumers/sessions/lookup"), + ) { response -> + response.testBodyFromFile("consumer-session-lookup-success.json") + } + + networkRule.enqueue( + method("POST"), + path("/v1/consumers/sessions/lookup"), + ) { response -> + response.testBodyFromFile("consumer-session-lookup-success.json") + } + + testContext.launch( + configuration = PaymentSheet.Configuration( + merchantDisplayName = "Merchant, Inc.", + customer = PaymentSheet.CustomerConfiguration( + id = "cus_1", + ephemeralKeySecret = "123" + ) + ) + ) + + page.fillOutCardDetails() + page.clickOnSaveForFutureUsage("Merchant, Inc.") + + closeSoftKeyboard() + + networkRule.enqueue( + method("POST"), + path("/v1/consumers/sessions/lookup"), + ) { response -> + response.testBodyFromFile("consumer-session-lookup-success.json") + } + + page.fillOutLinkPhone() + + closeSoftKeyboard() + + networkRule.enqueue( + method("POST"), + path("/v1/consumers/sessions/lookup"), + ) { response -> + response.testBodyFromFile("consumer-session-lookup-success.json") + } + + networkRule.enqueue( + method("POST"), + path("/v1/consumers/accounts/sign_up"), + ) { response -> + response.testBodyFromFile("consumer-accounts-signup-success.json") + } + + networkRule.enqueue( + method("POST"), + path("/v1/consumers/payment_details"), + ) { response -> + response.testBodyFromFile("consumer-payment-details-success.json") + } + + networkRule.enqueue( + method("POST"), + path("/v1/payment_intents/pi_example/confirm"), + bodyPart("payment_method_options%5Bcard%5D%5Bsetup_future_usage%5D", "off_session"), + ) { response -> + response.testBodyFromFile("payment-intent-confirm.json") + } + + networkRule.enqueue( + method("POST"), + path("/v1/consumers/sessions/log_out"), + ) { response -> + response.testBodyFromFile("consumer-session-logout-success.json") + } + + page.clickPrimaryButton() + } + @Test fun testSuccessfulCardPaymentWithLinkSignUpAndCardBrandChoice() = activityScenarioRule.runLinkTest( integrationType = integrationType, @@ -306,6 +422,129 @@ internal class LinkTest { page.clickPrimaryButton() } + @Test + @Ignore("test") + fun testSuccessfulCardPaymentWithLinkSignUpAndLinkPassthroughModeAndSaveForFutureUsage() = + activityScenarioRule.runLinkTest( + integrationType = integrationType, + paymentOptionCallback = { paymentOption -> + assertThat(paymentOption?.label).endsWith("4242") + + @Suppress("DEPRECATION") + assertThat(paymentOption?.drawableResourceId).isEqualTo(R.drawable.stripe_ic_paymentsheet_link) + }, + resultCallback = ::assertCompleted, + ) { testContext -> + networkRule.enqueue( + host("api.stripe.com"), + method("GET"), + path("/v1/elements/sessions"), + ) { response -> + response.testBodyFromFile("elements-sessions-requires_pm_with_link_ps_mode.json") + } + + networkRule.enqueue( + host("api.stripe.com"), + method("GET"), + path("/v1/customers/cus_1"), + ) { response -> + response.testBodyFromFile("customer-get-success.json") + } + + networkRule.enqueue( + host("api.stripe.com"), + method("GET"), + path("/v1/payment_methods"), + ) { response -> + response.testBodyFromFile("payment-methods-get-success-empty.json") + } + + networkRule.enqueue( + method("POST"), + path("/v1/consumers/sessions/lookup"), + ) { response -> + response.testBodyFromFile("consumer-session-lookup-success.json") + } + + networkRule.enqueue( + method("POST"), + path("/v1/consumers/sessions/lookup"), + ) { response -> + response.testBodyFromFile("consumer-session-lookup-success.json") + } + + testContext.launch( + configuration = PaymentSheet.Configuration( + merchantDisplayName = "Merchant, Inc.", + customer = PaymentSheet.CustomerConfiguration( + id = "cus_1", + ephemeralKeySecret = "123" + ) + ) + ) + + page.fillOutCardDetails() + page.clickOnSaveForFutureUsage("Merchant, Inc.") + + closeSoftKeyboard() + + networkRule.enqueue( + method("POST"), + path("/v1/consumers/sessions/lookup"), + ) { response -> + response.testBodyFromFile("consumer-session-lookup-success.json") + } + + page.fillOutLinkPhone() + + closeSoftKeyboard() + + networkRule.enqueue( + method("POST"), + path("/v1/consumers/sessions/lookup"), + ) { response -> + response.testBodyFromFile("consumer-session-lookup-success.json") + } + + networkRule.enqueue( + method("POST"), + path("/v1/consumers/accounts/sign_up"), + ) { response -> + response.testBodyFromFile("consumer-accounts-signup-success.json") + } + + networkRule.enqueue( + method("POST"), + path("/v1/consumers/payment_details"), + ) { response -> + response.testBodyFromFile("consumer-payment-details-success.json") + } + + networkRule.enqueue( + method("POST"), + path("/v1/consumers/payment_details/share"), + ) { response -> + response.testBodyFromFile("consumer-payment-details-share-success.json") + } + + networkRule.enqueue( + method("POST"), + path("/v1/payment_intents/pi_example/confirm"), + bodyPart("payment_method_options%5Bcard%5D%5Bsetup_future_usage%5D", "off_session"), + ) { response -> + response.testBodyFromFile("payment-intent-confirm.json") + } + + networkRule.enqueue( + method("POST"), + path("/v1/consumers/sessions/log_out"), + ) { response -> + response.testBodyFromFile("consumer-session-logout-success.json") + } + + page.clickPrimaryButton() + } + @Test fun testSuccessfulCardPaymentWithLinkSignUpPassthroughModeAndCardBrandChoice() = activityScenarioRule.runLinkTest( integrationType = integrationType, diff --git a/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/PaymentSheetPage.kt b/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/PaymentSheetPage.kt index e4c880c8e5c..c2105da9a8e 100644 --- a/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/PaymentSheetPage.kt +++ b/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/PaymentSheetPage.kt @@ -41,6 +41,14 @@ internal class PaymentSheetPage( clickViewWithText("Save your info for secure 1-click checkout with Link") } + fun clickOnSaveForFutureUsage(merchantName: String) { + Espresso.onIdle() + composeTestRule.waitForIdle() + + waitForText("Save for future $merchantName payments", true) + clickViewWithText("Save for future $merchantName payments") + } + fun clickOnLinkCheckbox() { Espresso.onIdle() composeTestRule.waitForIdle() @@ -72,8 +80,8 @@ internal class PaymentSheetPage( Espresso.onIdle() composeTestRule.waitForIdle() - waitForText("Phone number") - replaceText("Phone number", phoneNumber) + waitForText("Phone number", true) + replaceText("Phone number", phoneNumber, true) } fun fillOutLinkName() { @@ -147,8 +155,8 @@ internal class PaymentSheetPage( .performClick() } - fun replaceText(label: String, text: String) { - composeTestRule.onNode(hasText(label)) + fun replaceText(label: String, text: String, isLabelSubstring: Boolean = false) { + composeTestRule.onNode(hasText(label, substring = isLabelSubstring)) .performScrollTo() .performTextReplacement(text) } diff --git a/paymentsheet/src/androidTest/resources/payment-methods-get-success-empty.json b/paymentsheet/src/androidTest/resources/payment-methods-get-success-empty.json new file mode 100644 index 00000000000..9af747d994d --- /dev/null +++ b/paymentsheet/src/androidTest/resources/payment-methods-get-success-empty.json @@ -0,0 +1,6 @@ +{ + "object": "list", + "data": [], + "has_more": false, + "url": "/v1/payment_methods" +} \ No newline at end of file diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt index 143b884a0f9..03ca36e90d5 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt @@ -190,7 +190,9 @@ internal sealed class PaymentSelection : Parcelable { override val paymentMethodCreateParams = linkPaymentDetails.paymentMethodCreateParams @IgnoredOnParcel - override val paymentMethodOptionsParams = null + override val paymentMethodOptionsParams = PaymentMethodOptionsParams.Card( + setupFutureUsage = customerRequestedSave.setupFutureUsage + ) @IgnoredOnParcel override val paymentMethodExtraParams = null diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/DefaultIntentConfirmationInterceptorTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/DefaultIntentConfirmationInterceptorTest.kt index 86d7dc61f2a..6a89303a4ee 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/DefaultIntentConfirmationInterceptorTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/DefaultIntentConfirmationInterceptorTest.kt @@ -91,15 +91,9 @@ class DefaultIntentConfirmationInterceptorTest { } @Test - fun `Returns confirm params with setup future usage set to off session when requires save on confirmation`() = + fun `Returns confirm params with 'setup_future_usage' set to 'off_session' when requires save on confirmation`() = runTest { - val interceptor = DefaultIntentConfirmationInterceptor( - context = context, - stripeRepository = object : AbsFakeStripeRepository() {}, - publishableKeyProvider = { "pk" }, - stripeAccountIdProvider = { null }, - isFlowController = false, - ) + val interceptor = createIntentConfirmationInterceptor() val nextStep = interceptor.intercept( initializationMode = InitializationMode.PaymentIntent("pi_1234_secret_4321"), @@ -556,6 +550,16 @@ class DefaultIntentConfirmationInterceptorTest { } } + private fun createIntentConfirmationInterceptor(): DefaultIntentConfirmationInterceptor { + return DefaultIntentConfirmationInterceptor( + context = context, + stripeRepository = object : AbsFakeStripeRepository() {}, + publishableKeyProvider = { "pk" }, + stripeAccountIdProvider = { null }, + isFlowController = false, + ) + } + private class TestException(message: String? = null) : Exception(message) { override fun hashCode(): Int { From 5f5c61fe7aae2abbeece26ce5847d005f4f4f19b Mon Sep 17 00:00:00 2001 From: Samer Alabi Date: Mon, 18 Mar 2024 18:20:12 -0400 Subject: [PATCH 4/6] Add unit tests for checking if `customerRequestedSave` is properly passed to `IntentConfirmationInterceptor` --- .../stripe/android/paymentsheet/LinkTest.kt | 2 - .../paymentsheet/PaymentSheetViewModelTest.kt | 128 ++++++++++++++++-- .../utils/FakeLinkConfigurationCoordinator.kt | 27 ++-- 3 files changed, 128 insertions(+), 29 deletions(-) diff --git a/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/LinkTest.kt b/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/LinkTest.kt index 26f9041c81a..17c5c3a6c83 100644 --- a/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/LinkTest.kt +++ b/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/LinkTest.kt @@ -20,7 +20,6 @@ import com.stripe.android.paymentsheet.utils.LinkIntegrationType import com.stripe.android.paymentsheet.utils.LinkIntegrationTypeProvider import com.stripe.android.paymentsheet.utils.assertCompleted import com.stripe.android.paymentsheet.utils.runLinkTest -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -423,7 +422,6 @@ internal class LinkTest { } @Test - @Ignore("test") fun testSuccessfulCardPaymentWithLinkSignUpAndLinkPassthroughModeAndSaveForFutureUsage() = activityScenarioRule.runLinkTest( integrationType = integrationType, diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt index 47ca87f8ee6..e04117a5133 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt @@ -23,6 +23,9 @@ import com.stripe.android.googlepaylauncher.GooglePayPaymentMethodLauncher import com.stripe.android.googlepaylauncher.injection.GooglePayPaymentMethodLauncherFactory import com.stripe.android.link.LinkActivityResult import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.LinkConfigurationCoordinator +import com.stripe.android.link.LinkPaymentDetails +import com.stripe.android.link.model.AccountStatus import com.stripe.android.link.ui.inline.InlineSignupViewState import com.stripe.android.link.ui.inline.LinkSignupMode import com.stripe.android.link.ui.inline.SignUpConsentAction @@ -33,6 +36,7 @@ import com.stripe.android.model.Address import com.stripe.android.model.CardBrand import com.stripe.android.model.ConfirmPaymentIntentParams import com.stripe.android.model.ConfirmSetupIntentParams +import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.model.MandateDataParams import com.stripe.android.model.PaymentIntent import com.stripe.android.model.PaymentIntentFixtures @@ -661,20 +665,7 @@ internal class PaymentSheetViewModelTest { @Test fun `On inline link payment, should process with primary button`() = runTest { - val linkConfiguration = LinkConfiguration( - stripeIntent = mock { - on { linkFundingSources } doReturn listOf( - PaymentMethod.Type.Card.code - ) - }, - signupMode = LinkSignupMode.InsteadOfSaveForFutureUse, - customerInfo = LinkConfiguration.CustomerInfo(null, null, null, null), - flags = mapOf(), - merchantName = "Test merchant inc.", - merchantCountryCode = "US", - passthroughModeEnabled = false, - shippingValues = mapOf(), - ) + val linkConfiguration = createLinkConfiguration() val viewModel = createViewModel( linkState = LinkState( @@ -731,6 +722,86 @@ internal class PaymentSheetViewModelTest { } } + @Test + fun `On inline link payment with save requested, should set with 'requireSaveOnConfirmation' set to 'true'`() = + runTest { + val linkConfiguration = createLinkConfiguration() + val linkConfigurationCoordinator = FakeLinkConfigurationCoordinator( + attachNewCardToAccountResult = Result.success(LINK_SAVED_PAYMENT_DETAILS), + accountStatus = AccountStatus.Verified, + ) + + val intentConfirmationInterceptor = spy(fakeIntentConfirmationInterceptor) + + val viewModel = createViewModel( + linkConfigurationCoordinator = linkConfigurationCoordinator, + intentConfirmationInterceptor = intentConfirmationInterceptor, + linkState = LinkState( + configuration = linkConfiguration, + loginState = LinkState.LoginState.LoggedOut + ) + ) + + viewModel.linkHandler.payWithLinkInline( + userInput = UserInput.SignIn( + email = "email@email.com", + ), + paymentSelection = PaymentSelection.New.Card( + paymentMethodCreateParams = PaymentMethodCreateParamsFixtures.DEFAULT_CARD, + brand = CardBrand.Visa, + customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestReuse, + ), + shouldCompleteLinkInlineFlow = false + ) + + verify(intentConfirmationInterceptor).intercept( + initializationMode = any(), + paymentMethod = any(), + shippingValues = isNull(), + requiresSaveOnConfirmation = eq(true), + ) + } + + @Test + fun `On inline link payment with save not requested, should set with 'requireSaveOnConfirmation' set to 'false'`() = + runTest { + val linkConfiguration = createLinkConfiguration() + val linkConfigurationCoordinator = FakeLinkConfigurationCoordinator( + attachNewCardToAccountResult = Result.success(LINK_SAVED_PAYMENT_DETAILS), + accountStatus = AccountStatus.Verified, + ) + + val intentConfirmationInterceptor = spy(fakeIntentConfirmationInterceptor) + + val viewModel = createViewModel( + linkConfigurationCoordinator = linkConfigurationCoordinator, + intentConfirmationInterceptor = intentConfirmationInterceptor, + linkState = LinkState( + configuration = linkConfiguration, + loginState = LinkState.LoginState.LoggedOut + ) + ) + + viewModel.linkHandler.payWithLinkInline( + userInput = UserInput.SignIn( + email = "email@email.com", + ), + paymentSelection = PaymentSelection.New.Card( + paymentMethodCreateParams = PaymentMethodCreateParamsFixtures.DEFAULT_CARD, + brand = CardBrand.Visa, + customerRequestedSave = PaymentSelection.CustomerRequestedSave.NoRequest, + ), + shouldCompleteLinkInlineFlow = false + ) + + verify(intentConfirmationInterceptor).intercept( + initializationMode = any(), + paymentMethod = any(), + shippingValues = isNull(), + requiresSaveOnConfirmation = eq(false), + ) + } + @Test fun `On link payment through launcher, should process with wallets processing state`() = runTest { val linkConfiguration = LinkConfiguration( @@ -2506,6 +2577,8 @@ internal class PaymentSheetViewModelTest { args: PaymentSheetContractV2.Args = ARGS_CUSTOMER_WITH_GOOGLEPAY, stripeIntent: StripeIntent = PAYMENT_INTENT, customerPaymentMethods: List = PAYMENT_METHODS, + intentConfirmationInterceptor: IntentConfirmationInterceptor = fakeIntentConfirmationInterceptor, + linkConfigurationCoordinator: LinkConfigurationCoordinator = this.linkConfigurationCoordinator, customerRepository: CustomerRepository = FakeCustomerRepository(customerPaymentMethods), shouldFailLoad: Boolean = false, linkState: LinkState? = null, @@ -2548,7 +2621,7 @@ internal class PaymentSheetViewModelTest { savedStateHandle = thisSavedStateHandle, linkHandler = linkHandler, linkConfigurationCoordinator = linkInteractor, - intentConfirmationInterceptor = fakeIntentConfirmationInterceptor, + intentConfirmationInterceptor = intentConfirmationInterceptor, editInteractorFactory = fakeEditPaymentMethodInteractorFactory ) } @@ -2610,6 +2683,23 @@ internal class PaymentSheetViewModelTest { ) } + private fun createLinkConfiguration(): LinkConfiguration { + return LinkConfiguration( + stripeIntent = mock { + on { linkFundingSources } doReturn listOf( + PaymentMethod.Type.Card.code + ) + }, + signupMode = LinkSignupMode.InsteadOfSaveForFutureUse, + customerInfo = LinkConfiguration.CustomerInfo(null, null, null, null), + flags = mapOf(), + merchantName = "Test merchant inc.", + merchantCountryCode = "US", + passthroughModeEnabled = false, + shippingValues = mapOf(), + ) + } + private fun PaymentSheetViewModel.capturePaymentResultListener(): ActivityResultCallback { val mockActivityResultCaller = mock { on { @@ -2640,6 +2730,14 @@ internal class PaymentSheetViewModelTest { val PAYMENT_INTENT_WITH_PAYMENT_METHOD = PaymentIntentFixtures.PI_WITH_PAYMENT_METHOD val SETUP_INTENT = SetupIntentFixtures.SI_REQUIRES_PAYMENT_METHOD + private val LINK_SAVED_PAYMENT_DETAILS = LinkPaymentDetails.Saved( + paymentDetails = ConsumerPaymentDetails.Card( + id = "pm_123", + last4 = "4242", + ), + paymentMethodCreateParams = mock(), + ) + private const val BACS_ACCOUNT_NUMBER = "00012345" private const val BACS_SORT_CODE = "108800" private const val BACS_NAME = "John Doe" diff --git a/paymentsheet/src/test/java/com/stripe/android/utils/FakeLinkConfigurationCoordinator.kt b/paymentsheet/src/test/java/com/stripe/android/utils/FakeLinkConfigurationCoordinator.kt index f93401f009a..1c57adc747c 100644 --- a/paymentsheet/src/test/java/com/stripe/android/utils/FakeLinkConfigurationCoordinator.kt +++ b/paymentsheet/src/test/java/com/stripe/android/utils/FakeLinkConfigurationCoordinator.kt @@ -13,7 +13,19 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import org.mockito.kotlin.mock -class FakeLinkConfigurationCoordinator : LinkConfigurationCoordinator { +class FakeLinkConfigurationCoordinator( + private val attachNewCardToAccountResult: Result = Result.success( + LinkPaymentDetails.New( + paymentDetails = ConsumerPaymentDetails.Card( + id = "pm_123", + last4 = "4242", + ), + paymentMethodCreateParams = mock(), + originalParams = mock(), + ) + ), + private val accountStatus: AccountStatus = AccountStatus.SignedOut, +) : LinkConfigurationCoordinator { override val component: LinkComponent? get() = mock() @@ -22,7 +34,7 @@ class FakeLinkConfigurationCoordinator : LinkConfigurationCoordinator { get() = flowOf(null) override fun getAccountStatusFlow(configuration: LinkConfiguration): Flow { - return flowOf(AccountStatus.SignedOut) + return flowOf(accountStatus) } override suspend fun signInWithUserInput(configuration: LinkConfiguration, userInput: UserInput): Result { @@ -33,16 +45,7 @@ class FakeLinkConfigurationCoordinator : LinkConfigurationCoordinator { configuration: LinkConfiguration, paymentMethodCreateParams: PaymentMethodCreateParams ): Result { - return Result.success( - LinkPaymentDetails.New( - paymentDetails = ConsumerPaymentDetails.Card( - id = "pm_123", - last4 = "4242", - ), - paymentMethodCreateParams = mock(), - originalParams = mock(), - ) - ) + return attachNewCardToAccountResult } override suspend fun logOut(configuration: LinkConfiguration): Result { From 2545ae5c7243fd6dbee6ff6fbe189817892c7c68 Mon Sep 17 00:00:00 2001 From: Samer Alabi Date: Thu, 21 Mar 2024 11:46:40 -0400 Subject: [PATCH 5/6] Fix `Link` tests! --- .../resources/customer-get-success.json | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 paymentsheet/src/androidTest/resources/customer-get-success.json diff --git a/paymentsheet/src/androidTest/resources/customer-get-success.json b/paymentsheet/src/androidTest/resources/customer-get-success.json new file mode 100644 index 00000000000..329b6ce638e --- /dev/null +++ b/paymentsheet/src/androidTest/resources/customer-get-success.json @@ -0,0 +1,58 @@ +{ + "id": "cus_1", + "object": "customer", + "created": 1607372586, + "default_source": "src_1", + "description": null, + "email": "email@stripe.com", + "livemode": false, + "shipping": null, + "sources": { + "object": "list", + "data": [{ + "id": "src_1", + "object": "source", + "ach_credit_transfer": { + "account_number": "test_1", + "bank_name": "TEST BANK", + "fingerprint": "12345678", + "refund_account_holder_name": null, + "refund_account_holder_type": null, + "refund_routing_number": null, + "routing_number": "110000000", + "swift_code": "TSTEZ122" + }, + "amount": null, + "client_secret": "src_client_secret_1", + "created": 1643133663, + "currency": "usd", + "flow": "receiver", + "livemode": false, + "owner": { + "address": null, + "email": "amount_0@stripe.com", + "name": null, + "phone": null, + "verified_address": null, + "verified_email": null, + "verified_name": null, + "verified_phone": null + }, + "receiver": { + "address": "110000000-test_1", + "amount_charged": 0, + "amount_received": 0, + "amount_returned": 0, + "refund_attributes_method": "email", + "refund_attributes_status": "missing" + }, + "statement_descriptor": null, + "status": "pending", + "type": "ach_credit_transfer", + "usage": "reusable" + }], + "has_more": false, + "total_count": 1, + "url": "/v1/customers/cus_1/sources" + } +} From fc7a13946457574d5fcd0a5e51c37ec7f375fe98 Mon Sep 17 00:00:00 2001 From: Samer Alabi Date: Fri, 22 Mar 2024 18:51:39 -0400 Subject: [PATCH 6/6] Update & additional tests --- .../android/paymentsheet/LinkHandlerTest.kt | 129 +++++++++++------- .../PaymentOptionsViewModelTest.kt | 75 +++++++++- .../paymentsheet/PaymentSheetViewModelTest.kt | 104 +++++--------- .../paymentsheet/utils/LinkTestUtils.kt | 45 ++++++ 4 files changed, 236 insertions(+), 117 deletions(-) create mode 100644 paymentsheet/src/test/java/com/stripe/android/paymentsheet/utils/LinkTestUtils.kt diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt index 57740df030a..f359e41c2b1 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt @@ -263,86 +263,80 @@ class LinkHandlerTest { whenever(linkConfigurationCoordinator.signInWithUserInput(any(), any())) .thenReturn(Result.success(true)) testScope.launch { - handler.payWithLinkInline( - userInput, - cardSelection().copy(customerRequestedSave = PaymentSelection.CustomerRequestedSave.NoRequest), - shouldCompleteLinkFlow - ) + handler.payWithLinkInline(userInput, cardSelection(), shouldCompleteLinkFlow) } accountStatusFlow.emit(AccountStatus.SignedOut) assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.Started) assertThat(accountStatusTurbine.awaitItem()).isEqualTo(AccountStatus.SignedOut) accountStatusFlow.emit(AccountStatus.Verified) + assertThat(awaitItem()).isInstanceOf(LinkHandler.ProcessingState.PaymentDetailsCollected::class.java) + assertThat(accountStatusTurbine.awaitItem()).isEqualTo(AccountStatus.Verified) + verify(linkLauncher, never()).present(eq(configuration)) + verify(linkStore).markLinkAsUsed() + } - val processingState = awaitItem() - assertThat(processingState).isInstanceOf(LinkHandler.ProcessingState.PaymentDetailsCollected::class.java) + processingStateTurbine.cancelAndIgnoreRemainingEvents() // Validated above. + } - val selection = (processingState as LinkHandler.ProcessingState.PaymentDetailsCollected).paymentSelection - assertThat(selection).isInstanceOf(PaymentSelection.New.LinkInline::class.java) + @Test + fun `payWithLinkInline requests payment is saved if selection requested reuse`() = runLinkInlineTest( + accountStatusFlow = MutableSharedFlow(replay = 0), + shouldCompleteLinkFlowValues = listOf(false), + ) { + setupBasicLink() + + handler.processingState.test { + ensureAllEventsConsumed() + + payWithLinkInline( + customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestReuse + ) + + assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.Started) + accountStatusFlow.emit(AccountStatus.Verified) + + val linkInlineSelection = assertAndGetInlineLinkSelection(awaitItem()) - val linkInlineSelection = selection as PaymentSelection.New.LinkInline assertThat(linkInlineSelection.customerRequestedSave).isEqualTo( - PaymentSelection.CustomerRequestedSave.NoRequest + PaymentSelection.CustomerRequestedSave.RequestReuse ) - assertThat(accountStatusTurbine.awaitItem()).isEqualTo(AccountStatus.Verified) - verify(linkLauncher, never()).present(eq(configuration)) - verify(linkStore).markLinkAsUsed() + cancelAndConsumeRemainingEvents() } - processingStateTurbine.cancelAndIgnoreRemainingEvents() // Validated above. + accountStatusTurbine.cancelAndIgnoreRemainingEvents() + processingStateTurbine.cancelAndIgnoreRemainingEvents() } @Test - fun `payWithLinkInline collects payment details and requests payment is saved`() = runLinkInlineTest( + fun `payWithLinkInline requests payment is not saved if selection doesn't request it`() = runLinkInlineTest( accountStatusFlow = MutableSharedFlow(replay = 0), shouldCompleteLinkFlowValues = listOf(false), ) { - val userInput = UserInput.SignIn(email = "example@example.com") + setupBasicLink() - accountStatusTurbine.ensureAllEventsConsumed() - handler.setupLink( - state = LinkState( - loginState = LinkState.LoginState.LoggedOut, - configuration = configuration, + handler.processingState.test { + ensureAllEventsConsumed() + + payWithLinkInline( + customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestNoReuse ) - ) - handler.processingState.test { - ensureAllEventsConsumed() // Begin with no events. - whenever(linkConfigurationCoordinator.signInWithUserInput(any(), any())) - .thenReturn(Result.success(true)) - testScope.launch { - handler.payWithLinkInline( - userInput, - cardSelection().copy(customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestReuse), - shouldCompleteLinkFlow - ) - } - accountStatusFlow.emit(AccountStatus.SignedOut) assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.Started) - assertThat(accountStatusTurbine.awaitItem()).isEqualTo(AccountStatus.SignedOut) - accountStatusFlow.emit(AccountStatus.Verified) - val processingState = awaitItem() - assertThat(processingState).isInstanceOf(LinkHandler.ProcessingState.PaymentDetailsCollected::class.java) + val linkInlineSelection = assertAndGetInlineLinkSelection(awaitItem()) - val selection = (processingState as LinkHandler.ProcessingState.PaymentDetailsCollected).paymentSelection - assertThat(selection).isInstanceOf(PaymentSelection.New.LinkInline::class.java) - - val linkInlineSelection = selection as PaymentSelection.New.LinkInline assertThat(linkInlineSelection.customerRequestedSave).isEqualTo( - PaymentSelection.CustomerRequestedSave.RequestReuse + PaymentSelection.CustomerRequestedSave.RequestNoReuse ) - assertThat(accountStatusTurbine.awaitItem()).isEqualTo(AccountStatus.Verified) - verify(linkLauncher, never()).present(eq(configuration)) - verify(linkStore).markLinkAsUsed() + cancelAndConsumeRemainingEvents() } - processingStateTurbine.cancelAndIgnoreRemainingEvents() // Validated above. + accountStatusTurbine.cancelAndIgnoreRemainingEvents() + processingStateTurbine.cancelAndIgnoreRemainingEvents() } @Test @@ -563,6 +557,32 @@ class LinkHandlerTest { accountStatusTurbine.cancelAndIgnoreRemainingEvents() } + + private suspend fun LinkInlineTestData.setupBasicLink() { + handler.setupLink( + state = LinkState( + loginState = LinkState.LoginState.LoggedIn, + configuration = configuration, + ) + ) + + whenever(linkConfigurationCoordinator.signInWithUserInput(any(), any())) + .thenReturn(Result.success(true)) + } + + private suspend fun LinkInlineTestData.payWithLinkInline( + customerRequestedSave: PaymentSelection.CustomerRequestedSave + ) { + testScope.launch { + handler.payWithLinkInline( + UserInput.SignIn(email = "example@example.com"), + cardSelection().copy( + customerRequestedSave = customerRequestedSave + ), + shouldCompleteLinkFlow + ) + } + } } // Used to run through both complete flow, and custom flow for link inline tests. @@ -593,6 +613,19 @@ private fun runLinkInlineTest( } } +private fun assertAndGetInlineLinkSelection( + processingState: LinkHandler.ProcessingState +): PaymentSelection.New.LinkInline { + assertThat(processingState).isInstanceOf(LinkHandler.ProcessingState.PaymentDetailsCollected::class.java) + + val paymentDetailsCollectedState = processingState as LinkHandler.ProcessingState.PaymentDetailsCollected + val selection = paymentDetailsCollectedState.paymentSelection + + assertThat(selection).isInstanceOf(PaymentSelection.New.LinkInline::class.java) + + return selection as PaymentSelection.New.LinkInline +} + private fun runLinkTest( accountStatusFlow: MutableSharedFlow = MutableSharedFlow(replay = 1), linkConfiguration: LinkConfiguration = defaultLinkConfiguration(), diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentOptionsViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentOptionsViewModelTest.kt index f816e643660..24352a05ac1 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentOptionsViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentOptionsViewModelTest.kt @@ -9,6 +9,9 @@ import com.google.common.truth.Truth.assertThat import com.stripe.android.R import com.stripe.android.core.Logger import com.stripe.android.core.exception.APIConnectionException +import com.stripe.android.link.LinkConfigurationCoordinator +import com.stripe.android.link.model.AccountStatus +import com.stripe.android.link.ui.inline.UserInput import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFactory import com.stripe.android.model.CardBrand import com.stripe.android.model.PaymentIntentFixtures @@ -33,8 +36,10 @@ import com.stripe.android.paymentsheet.ui.DefaultEditPaymentMethodViewInteractor import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewAction import com.stripe.android.paymentsheet.ui.ModifiableEditPaymentMethodViewInteractor import com.stripe.android.paymentsheet.ui.PrimaryButton +import com.stripe.android.paymentsheet.utils.LinkTestUtils import com.stripe.android.testing.PaymentIntentFactory import com.stripe.android.testing.PaymentMethodFactory +import com.stripe.android.utils.FakeLinkConfigurationCoordinator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.resetMain @@ -693,11 +698,69 @@ internal class PaymentOptionsViewModelTest { verify(eventReporter, times(1)).onCannotProperlyReturnFromLinkAndOtherLPMs() } + @Test + fun `On link selection with save requested, selection should be updated with saveable link selection`() = + runTest { + val viewModel = createLinkViewModel() + + viewModel.linkHandler.payWithLinkInline( + userInput = UserInput.SignIn("email@email.com"), + paymentSelection = createCardPaymentSelection( + customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestReuse, + ), + shouldCompleteLinkInlineFlow = false + ) + + assertThat(viewModel.selection.value).isEqualTo( + PaymentSelection.New.LinkInline( + linkPaymentDetails = LinkTestUtils.LINK_NEW_PAYMENT_DETAILS, + customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestReuse, + ) + ) + } + + @Test + fun `On link selection with save not requested, selection should be updated with unsaveable link selection`() = + runTest { + val viewModel = createLinkViewModel() + + viewModel.linkHandler.payWithLinkInline( + userInput = UserInput.SignIn("email@email.com"), + paymentSelection = createCardPaymentSelection( + customerRequestedSave = PaymentSelection.CustomerRequestedSave.NoRequest, + ), + shouldCompleteLinkInlineFlow = false + ) + + assertThat(viewModel.selection.value).isEqualTo( + PaymentSelection.New.LinkInline( + linkPaymentDetails = LinkTestUtils.LINK_NEW_PAYMENT_DETAILS, + customerRequestedSave = PaymentSelection.CustomerRequestedSave.NoRequest + ) + ) + } + + private fun createLinkViewModel(): PaymentOptionsViewModel { + val linkConfigurationCoordinator = FakeLinkConfigurationCoordinator( + attachNewCardToAccountResult = Result.success(LinkTestUtils.LINK_NEW_PAYMENT_DETAILS), + accountStatus = AccountStatus.Verified, + ) + + return createViewModel( + linkState = LinkState( + configuration = LinkTestUtils.createLinkConfiguration(), + loginState = LinkState.LoginState.LoggedOut + ), + linkConfigurationCoordinator = linkConfigurationCoordinator, + ) + } + private fun createViewModel( args: PaymentOptionContract.Args = PAYMENT_OPTION_CONTRACT_ARGS, linkState: LinkState? = args.state.linkState, editInteractorFactory: ModifiableEditPaymentMethodViewInteractor.Factory = mock(), - ) = TestViewModelFactory.create { linkHandler, linkInteractor, savedStateHandle -> + linkConfigurationCoordinator: LinkConfigurationCoordinator = FakeLinkConfigurationCoordinator() + ) = TestViewModelFactory.create(linkConfigurationCoordinator) { linkHandler, linkInteractor, savedStateHandle -> PaymentOptionsViewModel( args = args.copy(state = args.state.copy(linkState = linkState)), prefsRepositoryFactory = { prefsRepository }, @@ -713,6 +776,16 @@ internal class PaymentOptionsViewModelTest { ) } + private fun createCardPaymentSelection( + customerRequestedSave: PaymentSelection.CustomerRequestedSave + ): PaymentSelection.New.Card { + return PaymentSelection.New.Card( + paymentMethodCreateParams = DEFAULT_CARD, + brand = CardBrand.Visa, + customerRequestedSave = customerRequestedSave, + ) + } + private companion object { private val PAYMENT_INTENT = PaymentIntentFactory.create() private val DEFERRED_PAYMENT_INTENT = PAYMENT_INTENT.copy(clientSecret = null) diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt index e04117a5133..84f0e465d25 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt @@ -24,7 +24,6 @@ import com.stripe.android.googlepaylauncher.injection.GooglePayPaymentMethodLaun import com.stripe.android.link.LinkActivityResult import com.stripe.android.link.LinkConfiguration import com.stripe.android.link.LinkConfigurationCoordinator -import com.stripe.android.link.LinkPaymentDetails import com.stripe.android.link.model.AccountStatus import com.stripe.android.link.ui.inline.InlineSignupViewState import com.stripe.android.link.ui.inline.LinkSignupMode @@ -36,7 +35,6 @@ import com.stripe.android.model.Address import com.stripe.android.model.CardBrand import com.stripe.android.model.ConfirmPaymentIntentParams import com.stripe.android.model.ConfirmSetupIntentParams -import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.model.MandateDataParams import com.stripe.android.model.PaymentIntent import com.stripe.android.model.PaymentIntentFixtures @@ -85,6 +83,7 @@ import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewAction import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewState import com.stripe.android.paymentsheet.ui.PrimaryButton import com.stripe.android.paymentsheet.utils.FakeEditPaymentMethodInteractorFactory +import com.stripe.android.paymentsheet.utils.LinkTestUtils import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel.Companion.SAVE_PROCESSING import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel.UserErrorMessage import com.stripe.android.testing.PaymentIntentFactory @@ -665,7 +664,7 @@ internal class PaymentSheetViewModelTest { @Test fun `On inline link payment, should process with primary button`() = runTest { - val linkConfiguration = createLinkConfiguration() + val linkConfiguration = LinkTestUtils.createLinkConfiguration() val viewModel = createViewModel( linkState = LinkState( @@ -725,30 +724,13 @@ internal class PaymentSheetViewModelTest { @Test fun `On inline link payment with save requested, should set with 'requireSaveOnConfirmation' set to 'true'`() = runTest { - val linkConfiguration = createLinkConfiguration() - val linkConfigurationCoordinator = FakeLinkConfigurationCoordinator( - attachNewCardToAccountResult = Result.success(LINK_SAVED_PAYMENT_DETAILS), - accountStatus = AccountStatus.Verified, - ) - val intentConfirmationInterceptor = spy(fakeIntentConfirmationInterceptor) - val viewModel = createViewModel( - linkConfigurationCoordinator = linkConfigurationCoordinator, - intentConfirmationInterceptor = intentConfirmationInterceptor, - linkState = LinkState( - configuration = linkConfiguration, - loginState = LinkState.LoginState.LoggedOut - ) - ) + val viewModel = createLinkViewModel(intentConfirmationInterceptor) viewModel.linkHandler.payWithLinkInline( - userInput = UserInput.SignIn( - email = "email@email.com", - ), - paymentSelection = PaymentSelection.New.Card( - paymentMethodCreateParams = PaymentMethodCreateParamsFixtures.DEFAULT_CARD, - brand = CardBrand.Visa, + userInput = UserInput.SignIn("email@email.com"), + paymentSelection = createCardPaymentSelection( customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestReuse, ), shouldCompleteLinkInlineFlow = false @@ -765,30 +747,13 @@ internal class PaymentSheetViewModelTest { @Test fun `On inline link payment with save not requested, should set with 'requireSaveOnConfirmation' set to 'false'`() = runTest { - val linkConfiguration = createLinkConfiguration() - val linkConfigurationCoordinator = FakeLinkConfigurationCoordinator( - attachNewCardToAccountResult = Result.success(LINK_SAVED_PAYMENT_DETAILS), - accountStatus = AccountStatus.Verified, - ) - val intentConfirmationInterceptor = spy(fakeIntentConfirmationInterceptor) - val viewModel = createViewModel( - linkConfigurationCoordinator = linkConfigurationCoordinator, - intentConfirmationInterceptor = intentConfirmationInterceptor, - linkState = LinkState( - configuration = linkConfiguration, - loginState = LinkState.LoginState.LoggedOut - ) - ) + val viewModel = createLinkViewModel(intentConfirmationInterceptor) viewModel.linkHandler.payWithLinkInline( - userInput = UserInput.SignIn( - email = "email@email.com", - ), - paymentSelection = PaymentSelection.New.Card( - paymentMethodCreateParams = PaymentMethodCreateParamsFixtures.DEFAULT_CARD, - brand = CardBrand.Visa, + userInput = UserInput.SignIn("email@email.com"), + paymentSelection = createCardPaymentSelection( customerRequestedSave = PaymentSelection.CustomerRequestedSave.NoRequest, ), shouldCompleteLinkInlineFlow = false @@ -2627,6 +2592,24 @@ internal class PaymentSheetViewModelTest { } } + private fun createLinkViewModel( + intentConfirmationInterceptor: IntentConfirmationInterceptor = fakeIntentConfirmationInterceptor + ): PaymentSheetViewModel { + val linkConfigurationCoordinator = FakeLinkConfigurationCoordinator( + attachNewCardToAccountResult = Result.success(LinkTestUtils.LINK_SAVED_PAYMENT_DETAILS), + accountStatus = AccountStatus.Verified, + ) + + return createViewModel( + linkConfigurationCoordinator = linkConfigurationCoordinator, + intentConfirmationInterceptor = intentConfirmationInterceptor, + linkState = LinkState( + configuration = LinkTestUtils.createLinkConfiguration(), + loginState = LinkState.LoginState.LoggedOut + ) + ) + } + private fun createViewModelForDeferredIntent( args: PaymentSheetContractV2.Args = ARGS_CUSTOMER_WITH_GOOGLEPAY, paymentIntent: PaymentIntent = PAYMENT_INTENT, @@ -2663,6 +2646,16 @@ internal class PaymentSheetViewModelTest { } } + private fun createCardPaymentSelection( + customerRequestedSave: PaymentSelection.CustomerRequestedSave + ): PaymentSelection { + return PaymentSelection.New.Card( + paymentMethodCreateParams = PaymentMethodCreateParamsFixtures.DEFAULT_CARD, + brand = CardBrand.Visa, + customerRequestedSave = customerRequestedSave, + ) + } + private fun createBacsPaymentSelection(): PaymentSelection { return PaymentSelection.New.GenericPaymentMethod( labelResource = "Test", @@ -2683,23 +2676,6 @@ internal class PaymentSheetViewModelTest { ) } - private fun createLinkConfiguration(): LinkConfiguration { - return LinkConfiguration( - stripeIntent = mock { - on { linkFundingSources } doReturn listOf( - PaymentMethod.Type.Card.code - ) - }, - signupMode = LinkSignupMode.InsteadOfSaveForFutureUse, - customerInfo = LinkConfiguration.CustomerInfo(null, null, null, null), - flags = mapOf(), - merchantName = "Test merchant inc.", - merchantCountryCode = "US", - passthroughModeEnabled = false, - shippingValues = mapOf(), - ) - } - private fun PaymentSheetViewModel.capturePaymentResultListener(): ActivityResultCallback { val mockActivityResultCaller = mock { on { @@ -2730,14 +2706,6 @@ internal class PaymentSheetViewModelTest { val PAYMENT_INTENT_WITH_PAYMENT_METHOD = PaymentIntentFixtures.PI_WITH_PAYMENT_METHOD val SETUP_INTENT = SetupIntentFixtures.SI_REQUIRES_PAYMENT_METHOD - private val LINK_SAVED_PAYMENT_DETAILS = LinkPaymentDetails.Saved( - paymentDetails = ConsumerPaymentDetails.Card( - id = "pm_123", - last4 = "4242", - ), - paymentMethodCreateParams = mock(), - ) - private const val BACS_ACCOUNT_NUMBER = "00012345" private const val BACS_SORT_CODE = "108800" private const val BACS_NAME = "John Doe" diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/utils/LinkTestUtils.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/utils/LinkTestUtils.kt new file mode 100644 index 00000000000..877269166e4 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/utils/LinkTestUtils.kt @@ -0,0 +1,45 @@ +package com.stripe.android.paymentsheet.utils + +import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.LinkPaymentDetails +import com.stripe.android.link.ui.inline.LinkSignupMode +import com.stripe.android.model.ConsumerPaymentDetails +import com.stripe.android.model.PaymentMethod +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock + +object LinkTestUtils { + val LINK_SAVED_PAYMENT_DETAILS = LinkPaymentDetails.Saved( + paymentDetails = ConsumerPaymentDetails.Card( + id = "pm_123", + last4 = "4242", + ), + paymentMethodCreateParams = mock(), + ) + + val LINK_NEW_PAYMENT_DETAILS = LinkPaymentDetails.New( + paymentDetails = ConsumerPaymentDetails.Card( + id = "pm_123", + last4 = "4242", + ), + paymentMethodCreateParams = mock(), + originalParams = mock() + ) + + fun createLinkConfiguration(): LinkConfiguration { + return LinkConfiguration( + stripeIntent = mock { + on { linkFundingSources } doReturn listOf( + PaymentMethod.Type.Card.code + ) + }, + signupMode = LinkSignupMode.InsteadOfSaveForFutureUse, + customerInfo = LinkConfiguration.CustomerInfo(null, null, null, null), + flags = mapOf(), + merchantName = "Test merchant inc.", + merchantCountryCode = "US", + passthroughModeEnabled = false, + shippingValues = mapOf(), + ) + } +}