diff --git a/paymentsheet/src/main/java/com/stripe/android/link/confirmation/DefaultLinkConfirmationHandler.kt b/paymentsheet/src/main/java/com/stripe/android/link/confirmation/DefaultLinkConfirmationHandler.kt index 15f69d961a3..2470056cf35 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/confirmation/DefaultLinkConfirmationHandler.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/confirmation/DefaultLinkConfirmationHandler.kt @@ -3,9 +3,14 @@ package com.stripe.android.link.confirmation import com.stripe.android.core.Logger import com.stripe.android.core.strings.resolvableString import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.LinkPaymentDetails import com.stripe.android.link.model.LinkAccount +import com.stripe.android.model.ConfirmPaymentIntentParams import com.stripe.android.model.ConsumerPaymentDetails +import com.stripe.android.model.PaymentMethod import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.model.PaymentMethodOptionsParams +import com.stripe.android.model.wallets.Wallet import com.stripe.android.paymentelement.confirmation.ConfirmationHandler import com.stripe.android.paymentelement.confirmation.PaymentMethodConfirmationOption import com.stripe.android.paymentsheet.PaymentSheet @@ -21,9 +26,35 @@ internal class DefaultLinkConfirmationHandler @Inject constructor( paymentDetails: ConsumerPaymentDetails.PaymentDetails, linkAccount: LinkAccount, cvc: String? + ): Result { + return confirm { + newConfirmationArgs( + paymentDetails = paymentDetails, + linkAccount = linkAccount, + cvc = cvc + ) + } + } + + override suspend fun confirm( + paymentDetails: LinkPaymentDetails, + linkAccount: LinkAccount, + cvc: String? + ): Result { + return confirm { + confirmationArgs( + paymentDetails = paymentDetails, + linkAccount = linkAccount, + cvc = cvc + ) + } + } + + private suspend fun confirm( + createArgs: () -> ConfirmationHandler.Args ): Result { return runCatching { - val args = confirmationArgs(paymentDetails, linkAccount, cvc) + val args = createArgs() confirmationHandler.start(args) val result = confirmationHandler.awaitResult() transformResult(result) @@ -55,6 +86,28 @@ internal class DefaultLinkConfirmationHandler @Inject constructor( } private fun confirmationArgs( + paymentDetails: LinkPaymentDetails, + linkAccount: LinkAccount, + cvc: String? + ): ConfirmationHandler.Args { + return when (paymentDetails) { + is LinkPaymentDetails.New -> { + newConfirmationArgs( + paymentDetails = paymentDetails.paymentDetails, + linkAccount = linkAccount, + cvc = cvc + ) + } + is LinkPaymentDetails.Saved -> { + savedConfirmationArgs( + paymentDetails = paymentDetails, + cvc = cvc + ) + } + } + } + + private fun newConfirmationArgs( paymentDetails: ConsumerPaymentDetails.PaymentDetails, linkAccount: LinkAccount, cvc: String? @@ -76,6 +129,37 @@ internal class DefaultLinkConfirmationHandler @Inject constructor( ) } + private fun savedConfirmationArgs( + paymentDetails: LinkPaymentDetails, + cvc: String? + ): ConfirmationHandler.Args { + return ConfirmationHandler.Args( + intent = configuration.stripeIntent, + confirmationOption = PaymentMethodConfirmationOption.Saved( + paymentMethod = PaymentMethod.Builder() + .setId(paymentDetails.paymentDetails.id) + .setCode(paymentDetails.paymentMethodCreateParams.typeCode) + .setCard( + PaymentMethod.Card( + last4 = paymentDetails.paymentDetails.last4, + wallet = Wallet.LinkWallet(paymentDetails.paymentDetails.last4), + ) + ) + .setType(PaymentMethod.Type.Card) + .build(), + optionsParams = PaymentMethodOptionsParams.Card( + setupFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.OffSession, + cvc = cvc?.takeIf { + configuration.passthroughModeEnabled.not() + } + ) + ), + appearance = PaymentSheet.Appearance(), + initializationMode = configuration.initializationMode, + shippingDetails = configuration.shippingDetails + ) + } + private fun createPaymentMethodCreateParams( selectedPaymentDetails: ConsumerPaymentDetails.PaymentDetails, linkAccount: LinkAccount, diff --git a/paymentsheet/src/main/java/com/stripe/android/link/confirmation/LinkConfirmationHandler.kt b/paymentsheet/src/main/java/com/stripe/android/link/confirmation/LinkConfirmationHandler.kt index d0848829edb..0e0a6fee1dd 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/confirmation/LinkConfirmationHandler.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/confirmation/LinkConfirmationHandler.kt @@ -1,6 +1,7 @@ package com.stripe.android.link.confirmation import com.stripe.android.core.strings.ResolvableString +import com.stripe.android.link.LinkPaymentDetails import com.stripe.android.link.model.LinkAccount import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.paymentelement.confirmation.ConfirmationHandler @@ -12,6 +13,12 @@ internal interface LinkConfirmationHandler { cvc: String? = null ): Result + suspend fun confirm( + paymentDetails: LinkPaymentDetails, + linkAccount: LinkAccount, + cvc: String? = null + ): Result + fun interface Factory { fun create(confirmationHandler: ConfirmationHandler): LinkConfirmationHandler } diff --git a/paymentsheet/src/main/java/com/stripe/android/link/ui/paymentmenthod/PaymentMethodViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/link/ui/paymentmenthod/PaymentMethodViewModel.kt index dddf5e9e4b1..181a93d0b05 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/ui/paymentmenthod/PaymentMethodViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/ui/paymentmenthod/PaymentMethodViewModel.kt @@ -9,6 +9,7 @@ import com.stripe.android.common.exception.stripeErrorMessage import com.stripe.android.core.Logger import com.stripe.android.link.LinkActivityResult import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.LinkPaymentDetails import com.stripe.android.link.account.LinkAccountManager import com.stripe.android.link.confirmation.LinkConfirmationHandler import com.stripe.android.link.confirmation.Result @@ -17,7 +18,6 @@ import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.ui.PrimaryButtonState import com.stripe.android.link.ui.completePaymentButtonLabel import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata -import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.model.PaymentMethod import com.stripe.android.paymentsheet.DefaultFormHelper import com.stripe.android.paymentsheet.FormHelper @@ -78,7 +78,7 @@ internal class PaymentMethodViewModel @Inject constructor( onSuccess = { linkPaymentDetails -> val cardMap = paymentMethodCreateParams.toParamMap()["card"] as? Map<*, *>? performConfirmation( - paymentDetails = linkPaymentDetails.paymentDetails, + paymentDetails = linkPaymentDetails, cvc = cardMap?.get("cvc") as? String? ) updateButtonState(PrimaryButtonState.Enabled) @@ -100,7 +100,7 @@ internal class PaymentMethodViewModel @Inject constructor( } private suspend fun performConfirmation( - paymentDetails: ConsumerPaymentDetails.PaymentDetails, + paymentDetails: LinkPaymentDetails, cvc: String? ) { val result = linkConfirmationHandler.confirm( diff --git a/paymentsheet/src/test/java/com/stripe/android/link/TestFactory.kt b/paymentsheet/src/test/java/com/stripe/android/link/TestFactory.kt index bd887237bb5..844901b5d6d 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/TestFactory.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/TestFactory.kt @@ -116,6 +116,11 @@ internal object TestFactory { originalParams = mock() ) + val LINK_SAVED_PAYMENT_DETAILS = LinkPaymentDetails.Saved( + paymentDetails = CONSUMER_PAYMENT_DETAILS_CARD, + paymentMethodCreateParams = PAYMENT_METHOD_CREATE_PARAMS, + ) + val LINK_ACCOUNT = LinkAccount(CONSUMER_SESSION) val CONSUMER_PAYMENT_DETAILS: ConsumerPaymentDetails = ConsumerPaymentDetails( diff --git a/paymentsheet/src/test/java/com/stripe/android/link/confirmation/DefaultLinkConfirmationHandlerTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/confirmation/DefaultLinkConfirmationHandlerTest.kt index d2c99aaa154..9a5c4c73a0f 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/confirmation/DefaultLinkConfirmationHandlerTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/confirmation/DefaultLinkConfirmationHandlerTest.kt @@ -4,10 +4,12 @@ import com.google.common.truth.Truth.assertThat import com.stripe.android.core.Logger import com.stripe.android.core.strings.resolvableString import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.LinkPaymentDetails import com.stripe.android.link.TestFactory import com.stripe.android.link.model.LinkAccount import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.model.PaymentMethodOptionsParams import com.stripe.android.model.SetupIntentFixtures import com.stripe.android.paymentelement.confirmation.ConfirmationHandler import com.stripe.android.paymentelement.confirmation.FakeConfirmationHandler @@ -169,6 +171,98 @@ internal class DefaultLinkConfirmationHandlerTest { .containsExactly("DefaultLinkConfirmationHandler: Payment confirmation returned null" to null) } + @Test + fun `confirm with New LinkPaymentDetails calls uses correct confirmation args`() = runTest(dispatcher) { + val configuration = TestFactory.LINK_CONFIGURATION + val confirmationHandler = FakeConfirmationHandler() + val handler = createHandler( + confirmationHandler = confirmationHandler, + configuration = configuration + ) + + confirmationHandler.awaitResultTurbine.add( + item = ConfirmationHandler.Result.Succeeded( + intent = configuration.stripeIntent, + deferredIntentConfirmationType = null + ) + ) + + val result = handler.confirm( + paymentDetails = TestFactory.LINK_NEW_PAYMENT_DETAILS, + linkAccount = TestFactory.LINK_ACCOUNT, + cvc = CVC + ) + + assertThat(result).isEqualTo(Result.Succeeded) + confirmationHandler.startTurbine.awaitItem().assertConfirmationArgs( + configuration = configuration, + linkAccount = TestFactory.LINK_ACCOUNT, + paymentDetails = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD, + cvc = CVC + ) + } + + @Test + fun `confirm with saved LinkPaymentDetails creates correct confirmation args`() = runTest(dispatcher) { + val configuration = TestFactory.LINK_CONFIGURATION + val confirmationHandler = FakeConfirmationHandler() + val handler = createHandler( + confirmationHandler = confirmationHandler, + configuration = configuration + ) + + confirmationHandler.awaitResultTurbine.add( + item = ConfirmationHandler.Result.Succeeded( + intent = configuration.stripeIntent, + deferredIntentConfirmationType = null + ) + ) + + val savedPaymentDetails = TestFactory.LINK_SAVED_PAYMENT_DETAILS + val result = handler.confirm( + paymentDetails = savedPaymentDetails, + linkAccount = TestFactory.LINK_ACCOUNT, + cvc = CVC + ) + + assertThat(result).isEqualTo(Result.Succeeded) + confirmationHandler.startTurbine.awaitItem().assertSavedConfirmationArgs( + configuration = configuration, + paymentDetails = TestFactory.LINK_SAVED_PAYMENT_DETAILS, + cvc = CVC + ) + } + + @Test + fun `confirm with saved LinkPaymentDetails in passthrough mode omits CVC`() = runTest(dispatcher) { + val configuration = TestFactory.LINK_CONFIGURATION.copy(passthroughModeEnabled = true) + val confirmationHandler = FakeConfirmationHandler() + val handler = createHandler( + confirmationHandler = confirmationHandler, + configuration = configuration + ) + + confirmationHandler.awaitResultTurbine.add( + item = ConfirmationHandler.Result.Succeeded( + intent = configuration.stripeIntent, + deferredIntentConfirmationType = null + ) + ) + + val result = handler.confirm( + paymentDetails = TestFactory.LINK_SAVED_PAYMENT_DETAILS, + linkAccount = TestFactory.LINK_ACCOUNT, + cvc = CVC + ) + + assertThat(result).isEqualTo(Result.Succeeded) + confirmationHandler.startTurbine.awaitItem().assertSavedConfirmationArgs( + configuration = configuration, + paymentDetails = TestFactory.LINK_SAVED_PAYMENT_DETAILS, + cvc = null + ) + } + private fun ConfirmationHandler.Args.assertConfirmationArgs( configuration: LinkConfiguration, paymentDetails: ConsumerPaymentDetails.PaymentDetails, @@ -188,6 +282,21 @@ internal class DefaultLinkConfirmationHandlerTest { assertThat(initializationMode).isEqualTo(configuration.initializationMode) } + private fun ConfirmationHandler.Args.assertSavedConfirmationArgs( + configuration: LinkConfiguration, + paymentDetails: LinkPaymentDetails.Saved, + cvc: String?, + ) { + assertThat(intent).isEqualTo(configuration.stripeIntent) + val option = confirmationOption as PaymentMethodConfirmationOption.Saved + assertThat(option.paymentMethod.id).isEqualTo(paymentDetails.paymentDetails.id) + + val optionsCard = option.optionsParams as? PaymentMethodOptionsParams.Card + assertThat(optionsCard?.cvc).isEqualTo(cvc) + assertThat(shippingDetails).isEqualTo(configuration.shippingDetails) + assertThat(initializationMode).isEqualTo(configuration.initializationMode) + } + private fun createHandler( configuration: LinkConfiguration = TestFactory.LINK_CONFIGURATION, logger: Logger = FakeLogger(), diff --git a/paymentsheet/src/test/java/com/stripe/android/link/confirmation/FakeLinkConfirmationHandler.kt b/paymentsheet/src/test/java/com/stripe/android/link/confirmation/FakeLinkConfirmationHandler.kt index 0cfb2f4dbbd..6f1a14b5684 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/confirmation/FakeLinkConfirmationHandler.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/confirmation/FakeLinkConfirmationHandler.kt @@ -1,11 +1,14 @@ package com.stripe.android.link.confirmation +import com.stripe.android.link.LinkPaymentDetails import com.stripe.android.link.model.LinkAccount import com.stripe.android.model.ConsumerPaymentDetails internal class FakeLinkConfirmationHandler : LinkConfirmationHandler { var confirmResult: Result = Result.Succeeded + var confirmWithLinkPaymentDetailsResult: Result = Result.Succeeded val calls = arrayListOf() + val confirmWithLinkPaymentDetailsCall = arrayListOf() override suspend fun confirm( paymentDetails: ConsumerPaymentDetails.PaymentDetails, @@ -22,9 +25,30 @@ internal class FakeLinkConfirmationHandler : LinkConfirmationHandler { return confirmResult } + override suspend fun confirm( + paymentDetails: LinkPaymentDetails, + linkAccount: LinkAccount, + cvc: String? + ): Result { + confirmWithLinkPaymentDetailsCall.add( + element = ConfirmWithLinkPaymentDetailsCall( + paymentDetails = paymentDetails, + linkAccount = linkAccount, + cvc = cvc + ) + ) + return confirmWithLinkPaymentDetailsResult + } + data class Call( val paymentDetails: ConsumerPaymentDetails.PaymentDetails, val linkAccount: LinkAccount, val cvc: String? ) + + data class ConfirmWithLinkPaymentDetailsCall( + val paymentDetails: LinkPaymentDetails, + val linkAccount: LinkAccount, + val cvc: String? + ) } diff --git a/paymentsheet/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodScreenTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodScreenTest.kt index 6ed9038f095..2e902f8b97c 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodScreenTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodScreenTest.kt @@ -134,7 +134,8 @@ internal class PaymentMethodScreenTest { fillCardDetails() - linkConfirmationHandler.confirmResult = LinkConfirmationResult.Failed("oops".resolvableString) + linkConfirmationHandler.confirmWithLinkPaymentDetailsResult = + LinkConfirmationResult.Failed("oops".resolvableString) onPayButton() .scrollToAndAssertDisplayed() diff --git a/paymentsheet/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt index 34f857f3325..b6dd09b11e2 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt @@ -112,9 +112,11 @@ class PaymentMethodViewModelTest { viewModel.onPayClicked() - assertThat(linkConfirmationHandler.calls.first().paymentDetails) - .isEqualTo(TestFactory.LINK_NEW_PAYMENT_DETAILS.paymentDetails) - assertThat(linkConfirmationHandler.calls.first().cvc).isEqualTo("111") + assertThat(linkConfirmationHandler.confirmWithLinkPaymentDetailsCall).hasSize(1) + val call = linkConfirmationHandler.confirmWithLinkPaymentDetailsCall.first() + assertThat(call.paymentDetails) + .isEqualTo(TestFactory.LINK_NEW_PAYMENT_DETAILS) + assertThat(call.cvc).isEqualTo("111") assertThat(result).isEqualTo(LinkActivityResult.Completed) assertThat(viewModel.state.value.primaryButtonState).isEqualTo(PrimaryButtonState.Enabled) } @@ -142,7 +144,7 @@ class PaymentMethodViewModelTest { viewModel.onPayClicked() - assertThat(linkConfirmationHandler.calls).isEmpty() + assertThat(linkConfirmationHandler.confirmWithLinkPaymentDetailsCall).isEmpty() assertThat(result).isEqualTo(null) assertThat(viewModel.state.value.primaryButtonState).isEqualTo(PrimaryButtonState.Enabled) @@ -156,7 +158,8 @@ class PaymentMethodViewModelTest { fun `onPayClicked handles confirmation failure`() = runTest { val linkConfirmationHandler = FakeLinkConfirmationHandler() - linkConfirmationHandler.confirmResult = LinkConfirmationResult.Failed("Payment failed".resolvableString) + linkConfirmationHandler.confirmWithLinkPaymentDetailsResult = + LinkConfirmationResult.Failed("Payment failed".resolvableString) val viewModel = createViewModel(linkConfirmationHandler = linkConfirmationHandler) @@ -166,7 +169,7 @@ class PaymentMethodViewModelTest { viewModel.onPayClicked() - assertThat(linkConfirmationHandler.calls).hasSize(1) + assertThat(linkConfirmationHandler.confirmWithLinkPaymentDetailsCall).hasSize(1) assertThat(viewModel.state.value.primaryButtonState).isEqualTo(PrimaryButtonState.Enabled) assertThat(viewModel.state.value.errorMessage).isEqualTo("Payment failed".resolvableString) } @@ -174,7 +177,7 @@ class PaymentMethodViewModelTest { @Test fun `onPayClicked handles cancellation`() = runTest { val linkConfirmationHandler = FakeLinkConfirmationHandler() - linkConfirmationHandler.confirmResult = LinkConfirmationResult.Canceled + linkConfirmationHandler.confirmWithLinkPaymentDetailsResult = LinkConfirmationResult.Canceled val viewModel = createViewModel(linkConfirmationHandler = linkConfirmationHandler) @@ -184,7 +187,7 @@ class PaymentMethodViewModelTest { viewModel.onPayClicked() - assertThat(linkConfirmationHandler.calls).hasSize(1) + assertThat(linkConfirmationHandler.confirmWithLinkPaymentDetailsCall).hasSize(1) assertThat(viewModel.state.value.primaryButtonState).isEqualTo(PrimaryButtonState.Enabled) assertThat(viewModel.state.value.errorMessage).isNull() } @@ -201,7 +204,7 @@ class PaymentMethodViewModelTest { viewModel.onPayClicked() - assertThat(linkConfirmationHandler.calls).isEmpty() + assertThat(linkConfirmationHandler.confirmWithLinkPaymentDetailsCall).isEmpty() assertThat(viewModel.state.value.primaryButtonState).isEqualTo(PrimaryButtonState.Disabled) assertThat(logger.errorLogs) .containsExactly("PaymentMethodViewModel: onPayClicked without paymentMethodCreateParams" to null)