From f2436374d57f04cc60be0a9da51dafb50f3acd42 Mon Sep 17 00:00:00 2001 From: toluo-stripe Date: Wed, 18 Dec 2024 15:59:57 -0500 Subject: [PATCH] Abstract confirmation --- .../DefaultLinkConfirmationHandler.kt | 94 +++++++++++ .../confirmation/LinkConfirmationHandler.kt | 23 +++ .../link/injection/NativeLinkComponent.kt | 2 + .../link/injection/NativeLinkModule.kt | 10 +- .../android/link/ui/wallet/WalletViewModel.kt | 67 +++----- .../android/link/LinkActivityResultTest.kt | 4 +- .../DefaultLinkConfirmationHandlerTest.kt | 149 ++++++++++++++++++ .../link/ui/wallet/WalletScreenTest.kt | 2 + .../link/ui/wallet/WalletViewModelTest.kt | 2 + 9 files changed, 302 insertions(+), 51 deletions(-) create mode 100644 paymentsheet/src/main/java/com/stripe/android/link/confirmation/DefaultLinkConfirmationHandler.kt create mode 100644 paymentsheet/src/main/java/com/stripe/android/link/confirmation/LinkConfirmationHandler.kt create mode 100644 paymentsheet/src/test/java/com/stripe/android/link/confirmation/DefaultLinkConfirmationHandlerTest.kt 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 new file mode 100644 index 00000000000..1c469462acd --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/link/confirmation/DefaultLinkConfirmationHandler.kt @@ -0,0 +1,94 @@ +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.model.LinkAccount +import com.stripe.android.model.ConsumerPaymentDetails +import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.paymentelement.confirmation.ConfirmationHandler +import com.stripe.android.paymentelement.confirmation.PaymentMethodConfirmationOption +import com.stripe.android.paymentsheet.PaymentSheet +import com.stripe.android.paymentsheet.R +import com.stripe.android.paymentsheet.state.PaymentElementLoader +import javax.inject.Inject + +internal class DefaultLinkConfirmationHandler @Inject constructor( + private val configuration: LinkConfiguration, + private val logger: Logger, + private val confirmationHandler: ConfirmationHandler +) : LinkConfirmationHandler { + override suspend fun confirm( + paymentDetails: ConsumerPaymentDetails.PaymentDetails, + linkAccount: LinkAccount + ): Result { + val args = confirmationArgs(paymentDetails, linkAccount) + confirmationHandler.start(args) + val result = confirmationHandler.awaitResult() + return transformResult(result) + } + + private fun transformResult(result: ConfirmationHandler.Result?): Result { + return when (result) { + is ConfirmationHandler.Result.Canceled -> Result.Canceled + is ConfirmationHandler.Result.Failed -> { + logger.error( + msg = "Failed to confirm payment", + t = result.cause + ) + Result.Failed(result.message) + } + is ConfirmationHandler.Result.Succeeded -> Result.Succeeded + null -> { + logger.error("Payment confirmation returned null") + Result.Failed(R.string.stripe_something_went_wrong.resolvableString) + } + } + } + + private fun confirmationArgs( + paymentDetails: ConsumerPaymentDetails.PaymentDetails, + linkAccount: LinkAccount + ): ConfirmationHandler.Args { + return ConfirmationHandler.Args( + intent = configuration.stripeIntent, + confirmationOption = PaymentMethodConfirmationOption.New( + createParams = createPaymentMethodCreateParams( + selectedPaymentDetails = paymentDetails, + linkAccount = linkAccount + ), + optionsParams = null, + shouldSave = false + ), + appearance = PaymentSheet.Appearance(), + initializationMode = PaymentElementLoader.InitializationMode.PaymentIntent( + clientSecret = configuration.stripeIntent.clientSecret ?: "" + ), + shippingDetails = null + ) + } + + private fun createPaymentMethodCreateParams( + selectedPaymentDetails: ConsumerPaymentDetails.PaymentDetails, + linkAccount: LinkAccount, + ): PaymentMethodCreateParams { + return PaymentMethodCreateParams.createLink( + paymentDetailsId = selectedPaymentDetails.id, + consumerSessionClientSecret = linkAccount.clientSecret, + extraParams = emptyMap(), + ) + } + + class Factory @Inject constructor( + private val configuration: LinkConfiguration, + private val logger: Logger, + ) : LinkConfirmationHandler.Factory { + override fun create(confirmationHandler: ConfirmationHandler): LinkConfirmationHandler { + return DefaultLinkConfirmationHandler( + confirmationHandler = confirmationHandler, + logger = logger, + configuration = configuration + ) + } + } +} 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 new file mode 100644 index 00000000000..b8bd6e35fc7 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/link/confirmation/LinkConfirmationHandler.kt @@ -0,0 +1,23 @@ +package com.stripe.android.link.confirmation + +import com.stripe.android.core.strings.ResolvableString +import com.stripe.android.link.model.LinkAccount +import com.stripe.android.model.ConsumerPaymentDetails +import com.stripe.android.paymentelement.confirmation.ConfirmationHandler + +internal interface LinkConfirmationHandler { + suspend fun confirm( + paymentDetails: ConsumerPaymentDetails.PaymentDetails, + linkAccount: LinkAccount + ): Result + + fun interface Factory { + fun create(confirmationHandler: ConfirmationHandler): LinkConfirmationHandler + } +} + +sealed interface Result { + data object Succeeded : Result + data object Canceled : Result + data class Failed(val message: ResolvableString) : Result +} diff --git a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkComponent.kt b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkComponent.kt index ba6b1948758..447b3e4f0fd 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkComponent.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkComponent.kt @@ -9,6 +9,7 @@ import com.stripe.android.link.LinkActivityViewModel import com.stripe.android.link.LinkConfiguration import com.stripe.android.link.account.LinkAccountManager import com.stripe.android.link.analytics.LinkEventsReporter +import com.stripe.android.link.confirmation.LinkConfirmationHandler import com.stripe.android.paymentelement.confirmation.injection.DefaultConfirmationModule import com.stripe.android.payments.core.injection.STATUS_BAR_COLOR import dagger.BindsInstance @@ -33,6 +34,7 @@ internal interface NativeLinkComponent { val configuration: LinkConfiguration val linkEventsReporter: LinkEventsReporter val logger: Logger + val linkConfirmationHandlerFactory: LinkConfirmationHandler.Factory val viewModel: LinkActivityViewModel @Component.Builder diff --git a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt index f8354b36ea5..71f6ea62f57 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt @@ -22,6 +22,8 @@ import com.stripe.android.link.account.DefaultLinkAccountManager import com.stripe.android.link.account.LinkAccountManager import com.stripe.android.link.analytics.DefaultLinkEventsReporter import com.stripe.android.link.analytics.LinkEventsReporter +import com.stripe.android.link.confirmation.DefaultLinkConfirmationHandler +import com.stripe.android.link.confirmation.LinkConfirmationHandler import com.stripe.android.link.repositories.LinkApiRepository import com.stripe.android.link.repositories.LinkRepository import com.stripe.android.networking.StripeApiRepository @@ -119,7 +121,7 @@ internal interface NativeLinkModule { @Provides @NativeLinkScope - internal fun providesAnalyticsRequestExecutor( + fun providesAnalyticsRequestExecutor( executor: DefaultAnalyticsRequestExecutor ): AnalyticsRequestExecutor = executor @@ -138,5 +140,11 @@ internal interface NativeLinkModule { @NativeLinkScope @Named(ALLOWS_MANUAL_CONFIRMATION) fun provideAllowsManualConfirmation() = true + + @Provides + @NativeLinkScope + fun provideLinkConfirmationHandlerFactory( + factory: DefaultLinkConfirmationHandler.Factory + ): LinkConfirmationHandler.Factory = factory } } diff --git a/paymentsheet/src/main/java/com/stripe/android/link/ui/wallet/WalletViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/link/ui/wallet/WalletViewModel.kt index b231e3ba1c9..cd972c93b10 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/ui/wallet/WalletViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/ui/wallet/WalletViewModel.kt @@ -11,18 +11,15 @@ import com.stripe.android.link.LinkActivityResult import com.stripe.android.link.LinkConfiguration import com.stripe.android.link.LinkScreen import com.stripe.android.link.account.LinkAccountManager +import com.stripe.android.link.confirmation.LinkConfirmationHandler +import com.stripe.android.link.confirmation.Result import com.stripe.android.link.injection.NativeLinkComponent import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.model.supportedPaymentMethodTypes import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.model.PaymentIntent -import com.stripe.android.model.PaymentMethodCreateParams import com.stripe.android.model.SetupIntent import com.stripe.android.model.StripeIntent -import com.stripe.android.paymentelement.confirmation.ConfirmationHandler -import com.stripe.android.paymentelement.confirmation.PaymentMethodConfirmationOption -import com.stripe.android.paymentsheet.PaymentSheet -import com.stripe.android.paymentsheet.state.PaymentElementLoader import com.stripe.android.ui.core.Amount import com.stripe.android.ui.core.R import kotlinx.coroutines.flow.MutableStateFlow @@ -36,7 +33,7 @@ internal class WalletViewModel @Inject constructor( private val linkAccount: LinkAccount, private val linkAccountManager: LinkAccountManager, private val logger: Logger, - private val confirmationHandler: ConfirmationHandler, + private val linkConfirmationHandler: LinkConfirmationHandler, private val navigateAndClearStack: (route: LinkScreen) -> Unit, private val dismissWithResult: (LinkActivityResult) -> Unit ) : ViewModel() { @@ -87,14 +84,6 @@ internal class WalletViewModel @Inject constructor( dismissWithResult(LinkActivityResult.Failed(fatalError)) } - private fun onError(error: Throwable) { - _uiState.update { - it.copy( - errorMessage = error.message?.resolvableString - ) - } - } - fun onItemSelected(item: ConsumerPaymentDetails.PaymentDetails) { if (item == uiState.value.selectedItem) return @@ -106,46 +95,26 @@ internal class WalletViewModel @Inject constructor( fun onPrimaryButtonClicked() { val selectedItem = uiState.value.selectedItem ?: return viewModelScope.launch { - confirmationHandler.start( - arguments = ConfirmationHandler.Args( - intent = configuration.stripeIntent, - confirmationOption = PaymentMethodConfirmationOption.New( - createParams = createPaymentMethodCreateParams( - selectedPaymentDetails = selectedItem, - linkAccount = linkAccount - ), - optionsParams = null, - shouldSave = false - ), - appearance = PaymentSheet.Appearance(), - initializationMode = PaymentElementLoader.InitializationMode.PaymentIntent( - clientSecret = configuration.stripeIntent.clientSecret ?: "" - ), - shippingDetails = null - ) + val result = linkConfirmationHandler.confirm( + paymentDetails = selectedItem, + linkAccount = linkAccount ) - when (val result = confirmationHandler.awaitResult()) { - is ConfirmationHandler.Result.Canceled -> { + when (result) { + Result.Canceled -> { dismissWithResult(LinkActivityResult.Canceled(LinkActivityResult.Canceled.Reason.BackPressed)) } - is ConfirmationHandler.Result.Failed -> onError(result.cause) - is ConfirmationHandler.Result.Succeeded -> Unit - null -> Unit + is Result.Failed -> { + _uiState.update { + it.copy( + errorMessage = result.message + ) + } + } + Result.Succeeded -> Unit } } } - private fun createPaymentMethodCreateParams( - selectedPaymentDetails: ConsumerPaymentDetails.PaymentDetails, - linkAccount: LinkAccount, - ): PaymentMethodCreateParams { - return PaymentMethodCreateParams.createLink( - paymentDetailsId = selectedPaymentDetails.id, - consumerSessionClientSecret = linkAccount.clientSecret, - extraParams = emptyMap(), - ) - } - fun onPayAnotherWayClicked() { dismissWithResult(LinkActivityResult.Canceled(LinkActivityResult.Canceled.Reason.PayAnotherWay)) } @@ -176,7 +145,9 @@ internal class WalletViewModel @Inject constructor( linkAccountManager = parentComponent.linkAccountManager, logger = parentComponent.logger, linkAccount = linkAccount, - confirmationHandler = parentComponent.viewModel.confirmationHandler, + linkConfirmationHandler = parentComponent.linkConfirmationHandlerFactory.create( + confirmationHandler = parentComponent.viewModel.confirmationHandler + ), navigateAndClearStack = navigateAndClearStack, dismissWithResult = dismissWithResult ) diff --git a/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityResultTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityResultTest.kt index 48f061d2d67..23844710ce4 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityResultTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityResultTest.kt @@ -98,12 +98,12 @@ class LinkActivityResultTest { @Test fun `complete with result from native link`() { val bundle = bundleOf( - LinkActivityContract.EXTRA_RESULT to LinkActivityResult.Completed(PaymentMethodFixtures.CARD_PAYMENT_METHOD) + LinkActivityContract.EXTRA_RESULT to LinkActivityResult.Completed ) val intent = Intent() intent.putExtras(bundle) val result = createLinkActivityResult(LinkActivity.RESULT_COMPLETE, intent) - assertThat(result).isEqualTo(LinkActivityResult.Completed(PaymentMethodFixtures.CARD_PAYMENT_METHOD)) + assertThat(result).isEqualTo(LinkActivityResult.Completed) } @Test 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 new file mode 100644 index 00000000000..2b3c4b06362 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/link/confirmation/DefaultLinkConfirmationHandlerTest.kt @@ -0,0 +1,149 @@ +package com.stripe.android.link.confirmation + +import com.google.common.truth.Truth +import com.stripe.android.core.Logger +import com.stripe.android.core.strings.resolvableString +import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.TestFactory +import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.paymentelement.confirmation.ConfirmationHandler +import com.stripe.android.paymentelement.confirmation.FakeConfirmationHandler +import com.stripe.android.paymentelement.confirmation.PaymentMethodConfirmationOption +import com.stripe.android.paymentsheet.PaymentSheet +import com.stripe.android.paymentsheet.R +import com.stripe.android.paymentsheet.state.PaymentElementLoader +import com.stripe.android.testing.FakeLogger +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test + +internal class DefaultLinkConfirmationHandlerTest { + private val dispatcher = UnconfinedTestDispatcher() + + @Test + fun `successful confirmation yields success result`() = runTest(dispatcher) { + val configuration = TestFactory.LINK_CONFIGURATION + val confirmationHandler = FakeConfirmationHandler() + val handler = createHandler( + confirmationHandler = confirmationHandler + ) + + confirmationHandler.awaitResultTurbine.add( + item = ConfirmationHandler.Result.Succeeded( + intent = configuration.stripeIntent, + deferredIntentConfirmationType = null + ) + ) + + val result = handler.confirm( + paymentDetails = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD, + linkAccount = TestFactory.LINK_ACCOUNT + ) + + Truth.assertThat(result).isEqualTo(Result.Succeeded) + Truth.assertThat(confirmationHandler.startTurbine.awaitItem()) + .isEqualTo( + ConfirmationHandler.Args( + intent = configuration.stripeIntent, + confirmationOption = PaymentMethodConfirmationOption.New( + createParams = PaymentMethodCreateParams.createLink( + paymentDetailsId = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD.id, + consumerSessionClientSecret = TestFactory.LINK_ACCOUNT.clientSecret, + extraParams = emptyMap(), + ), + optionsParams = null, + shouldSave = false + ), + appearance = PaymentSheet.Appearance(), + initializationMode = PaymentElementLoader.InitializationMode.PaymentIntent( + clientSecret = configuration.stripeIntent.clientSecret ?: "" + ), + shippingDetails = null + ) + ) + } + + @Test + fun `failed confirmation yields failed result`() = runTest(dispatcher) { + val error = Throwable("oops") + val errorMessage = "Something went wrong".resolvableString + + val confirmationHandler = FakeConfirmationHandler() + val logger = FakeLogger() + val handler = createHandler( + confirmationHandler = confirmationHandler, + logger = logger + ) + + confirmationHandler.awaitResultTurbine.add( + item = ConfirmationHandler.Result.Failed( + cause = error, + message = errorMessage, + type = ConfirmationHandler.Result.Failed.ErrorType.Payment + ) + ) + + val result = handler.confirm( + paymentDetails = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD, + linkAccount = TestFactory.LINK_ACCOUNT + ) + + Truth.assertThat(result).isEqualTo(Result.Failed(errorMessage)) + Truth.assertThat(logger.errorLogs) + .containsExactly("Failed to confirm payment" to error) + } + + @Test + fun `canceled confirmation yields canceled result`() = runTest(dispatcher) { + val confirmationHandler = FakeConfirmationHandler() + val handler = createHandler( + confirmationHandler = confirmationHandler + ) + + confirmationHandler.awaitResultTurbine.add( + item = ConfirmationHandler.Result.Canceled(ConfirmationHandler.Result.Canceled.Action.None) + ) + + val result = handler.confirm( + paymentDetails = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD, + linkAccount = TestFactory.LINK_ACCOUNT + ) + + Truth.assertThat(result).isEqualTo(Result.Canceled) + } + + @Test + fun `null confirmation yields canceled result`() = runTest(dispatcher) { + val confirmationHandler = FakeConfirmationHandler() + val logger = FakeLogger() + val handler = createHandler( + confirmationHandler = confirmationHandler, + logger = logger + ) + + confirmationHandler.awaitResultTurbine.add(null) + + val result = handler.confirm( + paymentDetails = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD, + linkAccount = TestFactory.LINK_ACCOUNT + ) + + Truth.assertThat(result).isEqualTo(Result.Failed(R.string.stripe_something_went_wrong.resolvableString)) + Truth.assertThat(logger.errorLogs) + .containsExactly("Payment confirmation returned null" to null) + } + + private fun createHandler( + configuration: LinkConfiguration = TestFactory.LINK_CONFIGURATION, + logger: Logger = FakeLogger(), + confirmationHandler: FakeConfirmationHandler = FakeConfirmationHandler() + ): DefaultLinkConfirmationHandler { + val handler = DefaultLinkConfirmationHandler( + confirmationHandler = confirmationHandler, + configuration = configuration, + logger = logger + ) + confirmationHandler.validate() + return handler + } +} diff --git a/paymentsheet/src/test/java/com/stripe/android/link/ui/wallet/WalletScreenTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/ui/wallet/WalletScreenTest.kt index 9342adf30ae..02f19103800 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/ui/wallet/WalletScreenTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/ui/wallet/WalletScreenTest.kt @@ -25,6 +25,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock @RunWith(AndroidJUnit4::class) internal class WalletScreenTest { @@ -174,6 +175,7 @@ internal class WalletScreenTest { linkAccount = TestFactory.LINK_ACCOUNT, linkAccountManager = linkAccountManager, logger = FakeLogger(), + confirmationHandler = mock(), navigateAndClearStack = {}, dismissWithResult = {} ) diff --git a/paymentsheet/src/test/java/com/stripe/android/link/ui/wallet/WalletViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/ui/wallet/WalletViewModelTest.kt index 5bc3eb578f6..c72cf5b84cc 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/ui/wallet/WalletViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/ui/wallet/WalletViewModelTest.kt @@ -18,6 +18,7 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -114,6 +115,7 @@ class WalletViewModelTest { linkAccount = TestFactory.LINK_ACCOUNT, linkAccountManager = linkAccountManager, logger = logger, + confirmationHandler = mock(), navigateAndClearStack = navigateAndClearStack, dismissWithResult = dismissWithResult )