diff --git a/paymentsheet/src/main/java/com/stripe/android/link/LinkActivity.kt b/paymentsheet/src/main/java/com/stripe/android/link/LinkActivity.kt index 9da2d910086..164349038fd 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/LinkActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/LinkActivity.kt @@ -48,6 +48,11 @@ internal class LinkActivity : ComponentActivity() { } val vm = viewModel ?: return + vm.registerFromActivity( + activityResultCaller = this, + lifecycleOwner = this, + ) + setContent { var bottomSheetContent by remember { mutableStateOf(null) } val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) diff --git a/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityViewModel.kt index 04f11b8f12a..5e9c51e7e1a 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityViewModel.kt @@ -1,6 +1,7 @@ package com.stripe.android.link import android.app.Application +import androidx.activity.result.ActivityResultCaller import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.SavedStateHandle @@ -23,6 +24,7 @@ import com.stripe.android.paymentelement.confirmation.ConfirmationHandler import com.stripe.android.paymentsheet.R import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @@ -49,6 +51,27 @@ internal class LinkActivityViewModel @Inject constructor( var navController: NavHostController? = null var dismissWithResult: ((LinkActivityResult) -> Unit)? = null + init { + viewModelScope.launch { + listenToConfirmationState() + } + } + + private suspend fun listenToConfirmationState() { +// confirmationHandler.state +// .filterIsInstance() +// .collect { +// dismissWithResult?.invoke(LinkActivityResult.Completed) +// } + } + + fun registerFromActivity( + activityResultCaller: ActivityResultCaller, + lifecycleOwner: LifecycleOwner, + ) { + confirmationHandler.register(activityResultCaller, lifecycleOwner) + } + fun handleViewAction(action: LinkAction) { when (action) { LinkAction.BackPressed -> handleBackPressed() @@ -117,6 +140,7 @@ internal class LinkActivityViewModel @Inject constructor( .stripeAccountIdProvider { args.stripeAccountId } .savedStateHandle(handle) .context(app) + .savedStateHandle(handle) .build() .viewModel } diff --git a/paymentsheet/src/main/java/com/stripe/android/link/injection/LinkViewModelModule.kt b/paymentsheet/src/main/java/com/stripe/android/link/injection/LinkViewModelModule.kt new file mode 100644 index 00000000000..bf68169f66a --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/link/injection/LinkViewModelModule.kt @@ -0,0 +1,24 @@ +package com.stripe.android.link.injection + +import com.stripe.android.link.LinkActivityViewModel +import com.stripe.android.link.account.LinkAccountManager +import com.stripe.android.paymentelement.confirmation.DefaultConfirmationHandler +import dagger.Module +import dagger.Provides + +@Module +internal object LinkViewModelModule { + @Provides + @NativeLinkScope + fun provideLinkActivityViewModel( + component: NativeLinkComponent, + defaultConfirmationHandlerFactory: DefaultConfirmationHandler.Factory, + linkAccountManager: LinkAccountManager + ): LinkActivityViewModel { + return LinkActivityViewModel( + component, + defaultConfirmationHandlerFactory, + linkAccountManager + ) + } +} 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 f4cef613b46..182a4071ce3 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 @@ -27,6 +27,9 @@ internal annotation class NativeLinkScope modules = [ NativeLinkModule::class, DefaultConfirmationModule::class, +// ViewModelModule::class, + LinkViewModelModule::class, + DefaultConfirmationModule::class, ] ) internal interface NativeLinkComponent { 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 094e37edd4c..d658eee3dda 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 @@ -161,6 +161,8 @@ internal interface NativeLinkModule { factory: DefaultLinkConfirmationHandler.Factory ): LinkConfirmationHandler.Factory = factory + @Provides + @NativeLinkScope fun provideEventReporterMode(): EventReporter.Mode = EventReporter.Mode.Custom } } diff --git a/paymentsheet/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt b/paymentsheet/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt index 2c770dd4d11..36b5f208650 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt @@ -41,6 +41,7 @@ import com.stripe.android.link.theme.HorizontalPadding import com.stripe.android.link.theme.linkColors import com.stripe.android.link.theme.linkShapes import com.stripe.android.link.ui.BottomSheetContent +import com.stripe.android.link.ui.ErrorText import com.stripe.android.link.ui.PrimaryButton import com.stripe.android.link.ui.SecondaryButton import com.stripe.android.model.ConsumerPaymentDetails @@ -91,17 +92,11 @@ internal fun WalletBody( hideBottomSheetContent: () -> Unit ) { if (state.paymentDetailsList.isEmpty()) { - Box( - modifier = Modifier - .fillMaxSize() - .testTag(WALLET_LOADER_TAG), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } + Loader() return } + val context = LocalContext.current val focusManager = LocalFocusManager.current LaunchedEffect(state.isProcessing) { @@ -133,6 +128,15 @@ internal fun WalletBody( BankAccountTerms() } + AnimatedVisibility(visible = state.errorMessage != null) { + ErrorText( + text = state.errorMessage?.resolve(context).orEmpty(), + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) + } + Spacer(modifier = Modifier.height(16.dp)) PrimaryButton( @@ -154,6 +158,18 @@ internal fun WalletBody( } } +@Composable +private fun Loader() { + Box( + modifier = Modifier + .fillMaxSize() + .testTag(WALLET_LOADER_TAG), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } +} + @Composable private fun PaymentMethodSection( state: WalletUiState, 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 5bf4768d861..7075484cd31 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 @@ -5,8 +5,8 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory -import com.stripe.android.common.exception.stripeErrorMessage import com.stripe.android.core.Logger +import com.stripe.android.core.strings.resolvableString import com.stripe.android.link.LinkActivityResult import com.stripe.android.link.LinkConfiguration import com.stripe.android.link.LinkScreen @@ -21,6 +21,10 @@ import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams import com.stripe.android.model.PaymentMethod 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.state.PaymentElementLoader import com.stripe.android.ui.core.FieldValuesToParamsMapConverter import com.stripe.android.ui.core.elements.CardDetailsUtil.createExpiryDateFormFieldValues import com.stripe.android.ui.core.elements.CvcController @@ -34,11 +38,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.Result -import kotlin.String -import kotlin.Throwable -import kotlin.Unit import kotlin.fold -import kotlin.takeIf import com.stripe.android.link.confirmation.Result as LinkConfirmationResult internal class WalletViewModel @Inject constructor( @@ -48,6 +48,7 @@ internal class WalletViewModel @Inject constructor( private val linkConfirmationHandler: LinkConfirmationHandler, private val logger: Logger, private val navigate: (route: LinkScreen) -> Unit, + private val confirmationHandler: ConfirmationHandler, private val navigateAndClearStack: (route: LinkScreen) -> Unit, private val dismissWithResult: (LinkActivityResult) -> Unit ) : ViewModel() { @@ -59,7 +60,8 @@ internal class WalletViewModel @Inject constructor( selectedItem = null, isProcessing = false, hasCompleted = false, - primaryButtonLabel = completePaymentButtonLabel(configuration.stripeIntent) + primaryButtonLabel = completePaymentButtonLabel(configuration.stripeIntent), + errorMessage = null ) ) @@ -151,55 +153,65 @@ internal class WalletViewModel @Inject constructor( val card = selectedPaymentDetails as? ConsumerPaymentDetails.Card val isExpired = card != null && card.isExpired - if (isExpired) { - performPaymentDetailsUpdate(selectedPaymentDetails).fold( - onSuccess = { result -> - val updatedPaymentDetails = result.paymentDetails.single { - it.id == selectedPaymentDetails.id - } - performPaymentConfirmation(updatedPaymentDetails) - }, - onFailure = { error -> + performPaymentConfirmationWithCvc( + selectedPaymentDetails = selectedPaymentDetails, + cvc = cvcController.formFieldValue.value.takeIf { it.isComplete }?.value + ) + } + + private suspend fun performPaymentConfirmationWithCvc( + selectedPaymentDetails: ConsumerPaymentDetails.PaymentDetails, + cvc: String? + ) { + viewModelScope.launch { + confirmationHandler.start( + arguments = ConfirmationHandler.Args( + intent = configuration.stripeIntent, + confirmationOption = PaymentMethodConfirmationOption.New( + createParams = createPaymentMethodCreateParams( + selectedPaymentDetails = selectedPaymentDetails, + linkAccount = linkAccount + ), + optionsParams = null, + shouldSave = false + ), + appearance = PaymentSheet.Appearance(), + initializationMode = PaymentElementLoader.InitializationMode.PaymentIntent( + clientSecret = configuration.stripeIntent.clientSecret ?: "" + ), + shippingDetails = null + ) + ) + val result = confirmationHandler.awaitResult() + when (result) { + is ConfirmationHandler.Result.Succeeded -> { + dismissWithResult(LinkActivityResult.Completed) + } + is ConfirmationHandler.Result.Canceled -> { + dismissWithResult(LinkActivityResult.Canceled(LinkActivityResult.Canceled.Reason.BackPressed)) + } + is ConfirmationHandler.Result.Failed -> { _uiState.update { it.copy( - alertMessage = error.stripeErrorMessage(), + errorMessage = result.message, isProcessing = false ) } } - ) - } else { - // Confirm payment with LinkConfirmationHandler - performPaymentConfirmationWithCvc( - selectedPaymentDetails = selectedPaymentDetails, - cvc = cvcController.formFieldValue.value.takeIf { it.isComplete }?.value - ) + null -> Unit + } } } - private suspend fun performPaymentConfirmationWithCvc( + private fun createPaymentMethodCreateParams( selectedPaymentDetails: ConsumerPaymentDetails.PaymentDetails, - cvc: String? - ) { - val result = linkConfirmationHandler.confirm( - paymentDetails = selectedPaymentDetails, - linkAccount = linkAccount, - cvc = cvc + linkAccount: LinkAccount, + ): PaymentMethodCreateParams { + return PaymentMethodCreateParams.createLink( + paymentDetailsId = selectedPaymentDetails.id, + consumerSessionClientSecret = linkAccount.clientSecret, + extraParams = emptyMap(), ) - when (result) { - LinkConfirmationResult.Canceled -> Unit - is LinkConfirmationResult.Failed -> { - _uiState.update { - it.copy( - errorMessage = result.message, - isProcessing = false - ) - } - } - LinkConfirmationResult.Succeeded -> { - dismissWithResult(LinkActivityResult.Completed) - } - } } private suspend fun performPaymentDetailsUpdate( @@ -248,6 +260,7 @@ internal class WalletViewModel @Inject constructor( logger = parentComponent.logger, linkAccount = linkAccount, navigate = navigate, + confirmationHandler = parentComponent.viewModel.confirmationHandler, navigateAndClearStack = navigateAndClearStack, dismissWithResult = dismissWithResult ) 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 c99d2678412..68de538d83d 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/TestFactory.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/TestFactory.kt @@ -17,6 +17,7 @@ import com.stripe.android.model.PaymentMethod import com.stripe.android.model.PaymentMethodCreateParams import com.stripe.android.paymentsheet.PaymentSheet import com.stripe.android.paymentsheet.paymentdatacollection.FormArguments +import com.stripe.android.paymentsheet.addresselement.AddressDetails import com.stripe.android.ui.core.Amount import com.stripe.android.ui.core.cbc.CardBrandChoiceEligibility import org.mockito.kotlin.mock 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 db7cdaa49ea..7a63272552d 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 @@ -1,18 +1,16 @@ package com.stripe.android.link.confirmation -import com.google.common.truth.Truth.assertThat +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.link.model.LinkAccount -import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.model.PaymentIntentFixtures import com.stripe.android.model.PaymentMethodCreateParams -import com.stripe.android.model.SetupIntentFixtures 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 @@ -28,44 +26,7 @@ internal class DefaultLinkConfirmationHandlerTest { 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.CONSUMER_PAYMENT_DETAILS_CARD, - 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, - initMode = PaymentElementLoader.InitializationMode.PaymentIntent( - clientSecret = configuration.stripeIntent.clientSecret.orEmpty() - ) - ) - } - - @Test - fun `successful confirmation yields success result with setup intent`() = runTest(dispatcher) { - val configuration = TestFactory.LINK_CONFIGURATION.copy( - stripeIntent = SetupIntentFixtures.SI_SUCCEEDED - ) - val confirmationHandler = FakeConfirmationHandler() - val handler = createHandler( - confirmationHandler = confirmationHandler, - configuration = configuration + confirmationHandler = confirmationHandler ) confirmationHandler.awaitResultTurbine.add( @@ -80,16 +41,27 @@ internal class DefaultLinkConfirmationHandlerTest { linkAccount = TestFactory.LINK_ACCOUNT ) - assertThat(result).isEqualTo(Result.Succeeded) - confirmationHandler.startTurbine.awaitItem().assertConfirmationArgs( - configuration = configuration, - linkAccount = TestFactory.LINK_ACCOUNT, - paymentDetails = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD, - cvc = null, - initMode = PaymentElementLoader.InitializationMode.SetupIntent( - clientSecret = configuration.stripeIntent.clientSecret.orEmpty() + 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 = configuration.shippingDetails + ) ) - ) } @Test @@ -117,8 +89,8 @@ internal class DefaultLinkConfirmationHandlerTest { linkAccount = TestFactory.LINK_ACCOUNT ) - assertThat(result).isEqualTo(Result.Failed(errorMessage)) - assertThat(logger.errorLogs) + Truth.assertThat(result).isEqualTo(Result.Failed(errorMessage)) + Truth.assertThat(logger.errorLogs) .containsExactly("DefaultLinkConfirmationHandler: Failed to confirm payment" to error) } @@ -138,11 +110,11 @@ internal class DefaultLinkConfirmationHandlerTest { linkAccount = TestFactory.LINK_ACCOUNT ) - assertThat(result).isEqualTo(Result.Canceled) + Truth.assertThat(result).isEqualTo(Result.Canceled) } @Test - fun `null confirmation yields failed result`() = runTest(dispatcher) { + fun `null confirmation yields canceled result`() = runTest(dispatcher) { val confirmationHandler = FakeConfirmationHandler() val logger = FakeLogger() val handler = createHandler( @@ -157,8 +129,8 @@ internal class DefaultLinkConfirmationHandlerTest { linkAccount = TestFactory.LINK_ACCOUNT ) - assertThat(result).isEqualTo(Result.Failed(R.string.stripe_something_went_wrong.resolvableString)) - assertThat(logger.errorLogs) + Truth.assertThat(result).isEqualTo(Result.Failed(R.string.stripe_something_went_wrong.resolvableString)) + Truth.assertThat(logger.errorLogs) .containsExactly("DefaultLinkConfirmationHandler: Payment confirmation returned null" to null) } @@ -183,32 +155,10 @@ internal class DefaultLinkConfirmationHandlerTest { linkAccount = TestFactory.LINK_ACCOUNT ) - assertThat(result).isEqualTo(Result.Failed(R.string.stripe_something_went_wrong.resolvableString)) - assertThat(logger.errorLogs) - .containsExactly( - "DefaultLinkConfirmationHandler: Failed to confirm payment" - to DefaultLinkConfirmationHandler.NO_CLIENT_SECRET_FOUND - ) - } - - private fun ConfirmationHandler.Args.assertConfirmationArgs( - configuration: LinkConfiguration, - paymentDetails: ConsumerPaymentDetails.PaymentDetails, - linkAccount: LinkAccount, - cvc: String?, - initMode: PaymentElementLoader.InitializationMode - ) { - assertThat(intent).isEqualTo(configuration.stripeIntent) - val option = confirmationOption as PaymentMethodConfirmationOption.New - assertThat(option.createParams).isEqualTo( - PaymentMethodCreateParams.createLink( - paymentDetailsId = paymentDetails.id, - consumerSessionClientSecret = linkAccount.clientSecret, - extraParams = cvc?.let { mapOf("card" to mapOf("cvc" to cvc)) }, - ) - ) - assertThat(shippingDetails).isEqualTo(configuration.shippingDetails) - assertThat(initializationMode).isEqualTo(initMode) + Truth.assertThat(result).isEqualTo(Result.Failed(R.string.stripe_something_went_wrong.resolvableString)) + Truth.assertThat(logger.errorLogs) + .containsExactly("DefaultLinkConfirmationHandler: Failed to confirm payment" + to DefaultLinkConfirmationHandler.NO_CLIENT_SECRET_FOUND) } private fun createHandler( @@ -224,8 +174,4 @@ internal class DefaultLinkConfirmationHandlerTest { confirmationHandler.validate() return handler } - - companion object { - private const val CVC = "333" - } } diff --git a/paymentsheet/src/test/java/com/stripe/android/link/ui/wallet/WalletScreenScreenshotTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/ui/wallet/WalletScreenScreenshotTest.kt index 6c78fa2c0ad..2694a68aec9 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/ui/wallet/WalletScreenScreenshotTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/ui/wallet/WalletScreenScreenshotTest.kt @@ -24,7 +24,8 @@ internal class WalletScreenScreenshotTest { selectedItem = null, isProcessing = false, hasCompleted = false, - primaryButtonLabel = primaryButtonLabel + primaryButtonLabel = primaryButtonLabel, + errorMessage = null ) ) } @@ -37,7 +38,8 @@ internal class WalletScreenScreenshotTest { selectedItem = TestFactory.CONSUMER_PAYMENT_DETAILS.paymentDetails.firstOrNull(), isProcessing = false, hasCompleted = false, - primaryButtonLabel = primaryButtonLabel + primaryButtonLabel = primaryButtonLabel, + errorMessage = null ) ) } @@ -50,7 +52,8 @@ internal class WalletScreenScreenshotTest { selectedItem = TestFactory.CONSUMER_PAYMENT_DETAILS.paymentDetails.firstOrNull(), isProcessing = false, hasCompleted = false, - primaryButtonLabel = primaryButtonLabel + primaryButtonLabel = primaryButtonLabel, + errorMessage = null ), isExpanded = true ) @@ -64,7 +67,8 @@ internal class WalletScreenScreenshotTest { selectedItem = TestFactory.CONSUMER_PAYMENT_DETAILS.paymentDetails.firstOrNull(), isProcessing = false, hasCompleted = false, - primaryButtonLabel = primaryButtonLabel + primaryButtonLabel = primaryButtonLabel, + errorMessage = null ), isExpanded = true ) @@ -78,7 +82,8 @@ internal class WalletScreenScreenshotTest { selectedItem = TestFactory.CONSUMER_PAYMENT_DETAILS.paymentDetails.firstOrNull(), isProcessing = true, hasCompleted = false, - primaryButtonLabel = primaryButtonLabel + primaryButtonLabel = primaryButtonLabel, + errorMessage = null ), isExpanded = true ) @@ -99,7 +104,8 @@ internal class WalletScreenScreenshotTest { selectedItem = paymentDetailsList.firstOrNull(), isProcessing = false, hasCompleted = false, - primaryButtonLabel = primaryButtonLabel + primaryButtonLabel = primaryButtonLabel, + errorMessage = null ), isExpanded = true ) @@ -120,7 +126,8 @@ internal class WalletScreenScreenshotTest { selectedItem = paymentDetailsList.firstOrNull(), isProcessing = false, hasCompleted = false, - primaryButtonLabel = primaryButtonLabel + primaryButtonLabel = primaryButtonLabel, + errorMessage = null ), isExpanded = true ) @@ -136,7 +143,8 @@ internal class WalletScreenScreenshotTest { }, isProcessing = false, hasCompleted = false, - primaryButtonLabel = primaryButtonLabel + primaryButtonLabel = primaryButtonLabel, + errorMessage = null ), isExpanded = true ) 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 eff7f976530..d36e8cb86c0 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 @@ -1,14 +1,6 @@ package com.stripe.android.link.ui.wallet import androidx.activity.ComponentActivity -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed @@ -16,24 +8,21 @@ import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onLast import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertThat -import com.stripe.android.core.strings.resolvableString import com.stripe.android.link.TestFactory import com.stripe.android.link.account.FakeLinkAccountManager import com.stripe.android.link.account.LinkAccountManager import com.stripe.android.link.confirmation.FakeLinkConfirmationHandler -import com.stripe.android.link.confirmation.LinkConfirmationHandler -import com.stripe.android.link.ui.BottomSheetContent import com.stripe.android.link.ui.PrimaryButtonTag import com.stripe.android.model.ConsumerPaymentDetails -import com.stripe.android.testing.CoroutineTestRule import com.stripe.android.testing.FakeLogger +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -45,8 +34,10 @@ internal class WalletScreenTest { @get:Rule val composeTestRule = createAndroidComposeRule() - @get:Rule - val coroutineTestRule = CoroutineTestRule(dispatcher) + @Before + fun setup() { + Dispatchers.setMain(dispatcher) + } @Test fun `wallet list is collapsed on start`() = runTest(dispatcher) { @@ -61,11 +52,7 @@ internal class WalletScreenTest { ) val viewModel = createViewModel(linkAccountManager) composeTestRule.setContent { - WalletScreen( - viewModel = viewModel, - showBottomSheetContent = {}, - hideBottomSheetContent = {} - ) + WalletScreen(viewModel) } composeTestRule.waitForIdle() @@ -92,11 +79,7 @@ internal class WalletScreenTest { ) val viewModel = createViewModel(linkAccountManager) composeTestRule.setContent { - WalletScreen( - viewModel = viewModel, - showBottomSheetContent = {}, - hideBottomSheetContent = {} - ) + WalletScreen(viewModel) } composeTestRule.waitForIdle() @@ -121,11 +104,7 @@ internal class WalletScreenTest { ) val viewModel = createViewModel(linkAccountManager) composeTestRule.setContent { - WalletScreen( - viewModel = viewModel, - showBottomSheetContent = {}, - hideBottomSheetContent = {} - ) + WalletScreen(viewModel) } composeTestRule.waitForIdle() @@ -156,11 +135,7 @@ internal class WalletScreenTest { ) val viewModel = createViewModel(linkAccountManager) composeTestRule.setContent { - WalletScreen( - viewModel = viewModel, - showBottomSheetContent = {}, - hideBottomSheetContent = {} - ) + WalletScreen(viewModel) } composeTestRule.waitForIdle() @@ -183,11 +158,7 @@ internal class WalletScreenTest { val viewModel = createViewModel(linkAccountManager) composeTestRule.setContent { - WalletScreen( - viewModel = viewModel, - showBottomSheetContent = {}, - hideBottomSheetContent = {} - ) + WalletScreen(viewModel) } composeTestRule.waitForIdle() @@ -196,171 +167,15 @@ internal class WalletScreenTest { onPaymentMethodList().assertCountEquals(0) } - @Test - fun `wallet menu is displayed on payment method menu clicked`() = runTest(dispatcher) { - val viewModel = createViewModel() - composeTestRule.setContent { - var sheetContent by remember { mutableStateOf(null) } - Box { - WalletScreen( - viewModel = viewModel, - showBottomSheetContent = { - sheetContent = it - }, - hideBottomSheetContent = { - sheetContent = null - } - ) - - sheetContent?.let { - Column { it() } - } - } - } - - composeTestRule.waitForIdle() - - onCollapsedWalletRow().performClick() - - composeTestRule.waitForIdle() - - onWalletPaymentMethodMenu().assertDoesNotExist() - onWalletPaymentMethodRowMenuButton().onLast().performClick() - - composeTestRule.waitForIdle() - - onWalletPaymentMethodMenu().assertIsDisplayed() - } - - @Test - fun `wallet menu is dismissed on cancel clicked`() = runTest(dispatcher) { - testMenu( - nodeTag = onWalletPaymentMethodMenuCancelTag() - ) - } - - @Test - fun `wallet menu is dismissed on remove clicked`() = runTest(dispatcher) { - testMenu( - nodeTag = onWalletPaymentMethodMenuRemoveTag(), - expectedRemovedCounter = 1 - ) - } - - @Test - fun `wallet menu is dismissed on edit clicked`() = runTest(dispatcher) { - testMenu( - nodeTag = onWalletPaymentMethodMenuUpdateTag(), - expectedEditPaymentMethodCounter = 1 - ) - } - - @Test - fun `wallet menu is dismissed on setAsDefault clicked`() = runTest(dispatcher) { - testMenu( - nodeTag = onWalletPaymentMethodMenuSetAsDefaultTag(), - expectedSetAsDefaultCounter = 1 - ) - } - - private fun testMenu( - nodeTag: SemanticsNodeInteraction, - expectedRemovedCounter: Int = 0, - expectedSetAsDefaultCounter: Int = 0, - expectedEditPaymentMethodCounter: Int = 0 - ) { - var onSetDefaultCounter = 0 - var onRemoveClickedCounter = 0 - var onEditPaymentMethodClickedCounter = 0 - composeTestRule.setContent { - var sheetContent by remember { mutableStateOf(null) } - Box { - TestWalletBody( - onSetDefaultClicked = { - onSetDefaultCounter += 1 - }, - onRemoveClicked = { - onRemoveClickedCounter += 1 - }, - onEditPaymentMethodClicked = { - onEditPaymentMethodClickedCounter += 1 - }, - showBottomSheetContent = { - sheetContent = it - }, - hideBottomSheetContent = { - sheetContent = null - } - ) - - sheetContent?.let { - Column { it() } - } - } - } - - composeTestRule.waitForIdle() - - onWalletPaymentMethodRowMenuButton().onLast().performClick() - - composeTestRule.waitForIdle() - - onWalletPaymentMethodMenu().assertIsDisplayed() - - nodeTag.performClick() - - composeTestRule.waitForIdle() - - onWalletPaymentMethodMenu().assertDoesNotExist() - assertThat(onSetDefaultCounter).isEqualTo(expectedSetAsDefaultCounter) - assertThat(onRemoveClickedCounter).isEqualTo(expectedRemovedCounter) - assertThat(onEditPaymentMethodClickedCounter).isEqualTo(expectedEditPaymentMethodCounter) - } - - @Composable - private fun TestWalletBody( - onRemoveClicked: (ConsumerPaymentDetails.PaymentDetails) -> Unit = {}, - onSetDefaultClicked: (ConsumerPaymentDetails.PaymentDetails) -> Unit = {}, - onEditPaymentMethodClicked: (ConsumerPaymentDetails.PaymentDetails) -> Unit = {}, - showBottomSheetContent: (BottomSheetContent?) -> Unit, - hideBottomSheetContent: () -> Unit - ) { - val paymentDetails = TestFactory.CONSUMER_PAYMENT_DETAILS.paymentDetails - .filterIsInstance() - .map { it.copy(isDefault = false) } - WalletBody( - state = WalletUiState( - paymentDetailsList = paymentDetails, - selectedItem = paymentDetails.firstOrNull(), - isProcessing = false, - hasCompleted = false, - primaryButtonLabel = "Buy".resolvableString - ), - isExpanded = true, - onItemSelected = {}, - onExpandedChanged = {}, - onPrimaryButtonClick = {}, - onPayAnotherWayClicked = {}, - onRemoveClicked = onRemoveClicked, - onSetDefaultClicked = onSetDefaultClicked, - onEditPaymentMethodClicked = onEditPaymentMethodClicked, - showBottomSheetContent = showBottomSheetContent, - hideBottomSheetContent = hideBottomSheetContent, - onAddNewPaymentMethodClicked = {} - ) - } - private fun createViewModel( - linkAccountManager: LinkAccountManager = FakeLinkAccountManager(), - linkConfirmationHandler: LinkConfirmationHandler = FakeLinkConfirmationHandler() + linkAccountManager: LinkAccountManager = FakeLinkAccountManager() ): WalletViewModel { return WalletViewModel( configuration = TestFactory.LINK_CONFIGURATION, linkAccount = TestFactory.LINK_ACCOUNT, linkAccountManager = linkAccountManager, - linkConfirmationHandler = linkConfirmationHandler, logger = FakeLogger(), - navigate = {}, + linkConfirmationHandler = FakeLinkConfirmationHandler(), navigateAndClearStack = {}, dismissWithResult = {} ) @@ -392,22 +207,4 @@ internal class WalletScreenTest { composeTestRule.onNodeWithTag(WALLET_SCREEN_PAY_ANOTHER_WAY_BUTTON, useUnmergedTree = true) private fun onLoader() = composeTestRule.onNodeWithTag(WALLET_LOADER_TAG) - - private fun onWalletPaymentMethodRowMenuButton() = - composeTestRule.onAllNodes(hasTestTag(WALLET_PAYMENT_DETAIL_ITEM_MENU_BUTTON), useUnmergedTree = true) - - private fun onWalletPaymentMethodMenu() = - composeTestRule.onNodeWithTag(WALLET_SCREEN_MENU_SHEET_TAG, useUnmergedTree = true) - - private fun onWalletPaymentMethodMenuCancelTag() = - composeTestRule.onNodeWithTag(WALLET_MENU_CANCEL_TAG, useUnmergedTree = true) - - private fun onWalletPaymentMethodMenuRemoveTag() = - composeTestRule.onNodeWithTag(WALLET_MENU_REMOVE_ITEM_TAG, useUnmergedTree = true) - - private fun onWalletPaymentMethodMenuUpdateTag() = - composeTestRule.onNodeWithTag(WALLET_MENU_EDIT_CARD_TAG, useUnmergedTree = true) - - private fun onWalletPaymentMethodMenuSetAsDefaultTag() = - composeTestRule.onNodeWithTag(WALLET_MENU_SET_AS_DEFAULT_TAG, useUnmergedTree = true) } 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 91679ccc549..177239aada9 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 @@ -1,47 +1,67 @@ package com.stripe.android.link.ui.wallet import com.google.common.truth.Truth.assertThat -import com.stripe.android.common.exception.stripeErrorMessage import com.stripe.android.core.Logger import com.stripe.android.core.strings.resolvableString import com.stripe.android.link.LinkActivityResult import com.stripe.android.link.LinkScreen import com.stripe.android.link.TestFactory import com.stripe.android.link.account.FakeLinkAccountManager +import com.stripe.android.link.account.LinkAccountManager import com.stripe.android.link.confirmation.FakeLinkConfirmationHandler import com.stripe.android.link.confirmation.LinkConfirmationHandler +import com.stripe.android.link.model.LinkAccount import com.stripe.android.model.ConsumerPaymentDetails -import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams -import com.stripe.android.testing.CoroutineTestRule import com.stripe.android.testing.FakeLogger -import com.stripe.android.uicore.forms.FormFieldEntry +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest -import org.junit.Rule +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import kotlin.Result -import com.stripe.android.link.confirmation.Result as LinkConfirmationResult +import kotlin.String +import kotlin.Throwable +import kotlin.Unit +import kotlin.time.Duration.Companion.seconds +import kotlin.to +import com.stripe.android.link.confirmation.Result as ConfirmationResult @RunWith(RobolectricTestRunner::class) class WalletViewModelTest { private val dispatcher = UnconfinedTestDispatcher() - @get:Rule - val coroutineTestRule = CoroutineTestRule(dispatcher) + @Before + fun setUp() { + Dispatchers.setMain(dispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } @Test fun `viewmodel should load payment methods on init`() = runTest(dispatcher) { - val linkAccountManager = WalletLinkAccountManager() + val linkAccountManager = object : FakeLinkAccountManager() { + var paymentMethodTypes: Set? = null + override suspend fun listPaymentDetails(paymentMethodTypes: Set): Result { + this.paymentMethodTypes = paymentMethodTypes + return super.listPaymentDetails(paymentMethodTypes) + } + } val viewModel = createViewModel( linkAccountManager = linkAccountManager ) - assertThat(linkAccountManager.listPaymentDetailsCalls) - .containsExactly(TestFactory.LINK_CONFIGURATION.stripeIntent.paymentMethodTypes.toSet()) + assertThat(linkAccountManager.paymentMethodTypes) + .containsExactlyElementsIn(TestFactory.LINK_CONFIGURATION.stripeIntent.paymentMethodTypes) assertThat(viewModel.uiState.value).isEqualTo( WalletUiState( @@ -50,8 +70,7 @@ class WalletViewModelTest { isProcessing = false, hasCompleted = false, primaryButtonLabel = TestFactory.LINK_WALLET_PRIMARY_BUTTON_LABEL, - expiryDateInput = FormFieldEntry(""), - cvcInput = FormFieldEntry("") + errorMessage = null ) ) } @@ -59,7 +78,7 @@ class WalletViewModelTest { @Test fun `viewmodel should dismiss with failure on load payment method failure`() = runTest(dispatcher) { val error = Throwable("oops") - val linkAccountManager = WalletLinkAccountManager() + val linkAccountManager = FakeLinkAccountManager() linkAccountManager.listPaymentDetailsResult = Result.failure(error) var linkActivityResult: LinkActivityResult? = null @@ -81,7 +100,7 @@ class WalletViewModelTest { @Test fun `viewmodel should open payment method screen when none is available`() = runTest(dispatcher) { - val linkAccountManager = WalletLinkAccountManager() + val linkAccountManager = FakeLinkAccountManager() linkAccountManager.listPaymentDetailsResult = Result.success(ConsumerPaymentDetails(emptyList())) var navScreen: LinkScreen? = null @@ -98,272 +117,69 @@ class WalletViewModelTest { } @Test - fun `viewmodel should open card edit screen when onEditPaymentMethodClicked`() = runTest(dispatcher) { - var navScreen: LinkScreen? = null - fun navigate(screen: LinkScreen) { - navScreen = screen + fun `viewmodel should dismiss link after successful payment`() = runTest(dispatcher) { + val linkConfirmationHandler = object : FakeLinkConfirmationHandler() { + override suspend fun confirm( + paymentDetails: ConsumerPaymentDetails.PaymentDetails, + linkAccount: LinkAccount + ): com.stripe.android.link.confirmation.Result { + delay(1.seconds) + return super.confirm(paymentDetails, linkAccount) + } } + linkConfirmationHandler.result = ConfirmationResult.Succeeded - val vm = createViewModel(navigate = ::navigate) - - vm.onEditPaymentMethodClicked(TestFactory.CONSUMER_PAYMENT_DETAILS_CARD) - - assertThat(navScreen).isEqualTo(LinkScreen.CardEdit) - } - - @Test - fun `viewmodel should open payment method screen when onAddNewPaymentMethodClicked`() = runTest(dispatcher) { - var navScreen: LinkScreen? = null - fun navigate(screen: LinkScreen) { - navScreen = screen + var linkActivityResult: LinkActivityResult? = null + fun dismissWithResult(result: LinkActivityResult) { + linkActivityResult = result } - val vm = createViewModel(navigate = ::navigate) - - vm.onAddNewPaymentMethodClicked() - - assertThat(navScreen).isEqualTo(LinkScreen.PaymentMethod) - } - - fun `expiryDateController updates uiState when input changes`() = runTest(dispatcher) { - val viewModel = createViewModel() - - viewModel.expiryDateController.onRawValueChange("12") - advanceUntilIdle() - assertThat(viewModel.uiState.value.expiryDateInput).isEqualTo(FormFieldEntry("12", isComplete = false)) - - viewModel.expiryDateController.onRawValueChange("12/25") - assertThat(viewModel.uiState.value.expiryDateInput).isEqualTo(FormFieldEntry("1225", isComplete = true)) - } - - @Test - fun `cvcController updates uiState when input changes`() = runTest(dispatcher) { - val viewModel = createViewModel() - - viewModel.cvcController.onRawValueChange("12") - advanceUntilIdle() - assertThat(viewModel.uiState.value.cvcInput).isEqualTo(FormFieldEntry("12", isComplete = false)) - - viewModel.cvcController.onRawValueChange("123") - advanceUntilIdle() - assertThat(viewModel.uiState.value.cvcInput).isEqualTo(FormFieldEntry("123", isComplete = true)) - } - - @Test - fun `expiryDateController and cvcController reset when new item is selected`() = runTest(dispatcher) { - val viewModel = createViewModel() - - viewModel.expiryDateController.onRawValueChange("12/25") - viewModel.cvcController.onRawValueChange("123") - - assertThat(viewModel.uiState.value.expiryDateInput).isEqualTo(FormFieldEntry("1225", isComplete = true)) - assertThat(viewModel.uiState.value.cvcInput).isEqualTo(FormFieldEntry("123", isComplete = true)) - - val newCard = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD.copy(id = "new_card_id") - viewModel.onItemSelected(newCard) - - assertThat(viewModel.uiState.value.expiryDateInput).isEqualTo(FormFieldEntry("", isComplete = false)) - assertThat(viewModel.uiState.value.cvcInput).isEqualTo(FormFieldEntry("", isComplete = false)) - } - - @Test - fun `performPaymentConfirmation updates expired card successfully`() = runTest(dispatcher) { - val expiredCard = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD.copy(expiryYear = 1999) - val updatedCard = expiredCard.copy(expiryYear = 2099) - val linkAccountManager = WalletLinkAccountManager() - linkAccountManager.updatePaymentDetailsResult = Result.success( - ConsumerPaymentDetails(paymentDetails = listOf(updatedCard)) - ) - linkAccountManager.listPaymentDetailsResult = Result.success( - ConsumerPaymentDetails(paymentDetails = listOf(expiredCard)) - ) - val linkConfirmationHandler = FakeLinkConfirmationHandler() - - val viewModel = createViewModel( - linkAccountManager = linkAccountManager, - linkConfirmationHandler = linkConfirmationHandler - ) - viewModel.onItemSelected(expiredCard) - viewModel.expiryDateController.onRawValueChange("1299") - viewModel.cvcController.onRawValueChange("123") - - viewModel.onPrimaryButtonClicked() - - assertThat(viewModel.uiState.value.isProcessing).isTrue() - assertThat(viewModel.uiState.value.alertMessage).isNull() - - val updateParamsUsed = linkAccountManager.updatePaymentDetailsCalls.firstOrNull() - val card = updateParamsUsed?.cardPaymentMethodCreateParamsMap - ?.get("card") as? Map<*, *> - - assertThat(updateParamsUsed?.id).isEqualTo(expiredCard.id) - assertThat(card).isEqualTo( - mapOf( - "exp_month" to updatedCard.expiryMonth.toString(), - "exp_year" to updatedCard.expiryYear.toString() - ) - ) - - assertThat(linkConfirmationHandler.calls).containsExactly( - FakeLinkConfirmationHandler.Call( - paymentDetails = updatedCard, - linkAccount = TestFactory.LINK_ACCOUNT, - cvc = "123" - ) - ) - } - - @Test - fun `performPaymentConfirmation handles update failure`() = runTest(dispatcher) { - val error = RuntimeException("Update failed") - val expiredCard = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD.copy(expiryYear = 1999) - val linkAccountManager = WalletLinkAccountManager() - linkAccountManager.listPaymentDetailsResult = Result.success( - ConsumerPaymentDetails(paymentDetails = listOf(expiredCard)) - ) - linkAccountManager.updatePaymentDetailsResult = Result.failure(error) - - val linkConfirmationHandler = FakeLinkConfirmationHandler() - - val viewModel = createViewModel( - linkAccountManager = linkAccountManager, - linkConfirmationHandler = linkConfirmationHandler - ) - viewModel.onItemSelected(expiredCard) - viewModel.expiryDateController.onRawValueChange("1225") - viewModel.cvcController.onRawValueChange("123") - - viewModel.onPrimaryButtonClicked() - - assertThat(viewModel.uiState.value.isProcessing).isFalse() - assertThat(viewModel.uiState.value.alertMessage).isEqualTo(error.stripeErrorMessage()) - - assertThat(linkConfirmationHandler.calls).isEmpty() - } - - @Test - fun `performPaymentConfirmation skips update for non-expired card`() = runTest(dispatcher) { - val validCard = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD.copy(expiryYear = 2099) - val linkAccountManager = WalletLinkAccountManager() - - val linkConfirmationHandler = FakeLinkConfirmationHandler() - - val viewModel = createViewModel( - linkAccountManager = linkAccountManager, - linkConfirmationHandler = linkConfirmationHandler - ) - viewModel.onItemSelected(validCard) - - viewModel.onPrimaryButtonClicked() - - assertThat(linkAccountManager.updatePaymentDetailsCalls).isEmpty() - - assertThat(linkConfirmationHandler.calls).containsExactly( - FakeLinkConfirmationHandler.Call( - paymentDetails = validCard, - linkAccount = TestFactory.LINK_ACCOUNT, - cvc = null - ) - ) - } - - @Test - fun `performPaymentConfirmation dismisses with Completed result on success`() = runTest(dispatcher) { - val validCard = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD.copy(expiryYear = 2099) - val linkAccountManager = WalletLinkAccountManager() - linkAccountManager.listPaymentDetailsResult = Result.success( - value = ConsumerPaymentDetails(paymentDetails = listOf(validCard)) - ) - - val linkConfirmationHandler = FakeLinkConfirmationHandler() - - var result: LinkActivityResult? = null - val viewModel = createViewModel( - linkAccountManager = linkAccountManager, + val vm = createViewModel( linkConfirmationHandler = linkConfirmationHandler, - dismissWithResult = { - result = it - } + dismissWithResult = ::dismissWithResult ) - viewModel.onItemSelected(validCard) - viewModel.onPrimaryButtonClicked() + vm.onPrimaryButtonClicked() - assertThat(linkAccountManager.updatePaymentDetailsCalls).isEmpty() + assertThat(vm.uiState.value.isProcessing).isTrue() - assertThat(linkConfirmationHandler.calls).containsExactly( - FakeLinkConfirmationHandler.Call( - paymentDetails = validCard, - cvc = null, - linkAccount = TestFactory.LINK_ACCOUNT - ) - ) + dispatcher.scheduler.advanceTimeBy(1.5.seconds) - assertThat(result).isEqualTo(LinkActivityResult.Completed) + assertThat(linkActivityResult).isEqualTo(LinkActivityResult.Completed) + assertThat(vm.uiState.value.errorMessage).isEqualTo(null) } @Test - fun `performPaymentConfirmation displays error on failure result`() = runTest(dispatcher) { - val confirmationResult = LinkConfirmationResult.Failed("oops".resolvableString) - val validCard = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD.copy(expiryYear = 2099) - val linkAccountManager = WalletLinkAccountManager() - linkAccountManager.listPaymentDetailsResult = Result.success( - value = ConsumerPaymentDetails(paymentDetails = listOf(validCard)) - ) - - val linkConfirmationHandler = FakeLinkConfirmationHandler() - linkConfirmationHandler.confirmResult = confirmationResult - - var result: LinkActivityResult? = null - val viewModel = createViewModel( - linkAccountManager = linkAccountManager, - linkConfirmationHandler = linkConfirmationHandler, - dismissWithResult = { - result = it + fun `viewmodel should display error after failed payment`() = runTest(dispatcher) { + val errorMessage = "Something's up".resolvableString + val linkConfirmationHandler = object : FakeLinkConfirmationHandler() { + override suspend fun confirm( + paymentDetails: ConsumerPaymentDetails.PaymentDetails, + linkAccount: LinkAccount + ): com.stripe.android.link.confirmation.Result { + delay(1.seconds) + return ConfirmationResult.Failed(errorMessage) } - ) - viewModel.onItemSelected(validCard) - - viewModel.onPrimaryButtonClicked() - - assertThat(viewModel.uiState.value.errorMessage).isEqualTo(confirmationResult.message) - assertThat(viewModel.uiState.value.isProcessing).isFalse() - assertThat(result).isNull() - } + } - @Test - fun `performPaymentConfirmation does nothing on canceled result`() = runTest(dispatcher) { - val confirmationResult = LinkConfirmationResult.Canceled - val validCard = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD.copy(expiryYear = 2099) - val linkAccountManager = WalletLinkAccountManager() - linkAccountManager.listPaymentDetailsResult = Result.success( - value = ConsumerPaymentDetails(paymentDetails = listOf(validCard)) + val vm = createViewModel( + linkConfirmationHandler = linkConfirmationHandler ) - val linkConfirmationHandler = FakeLinkConfirmationHandler() - linkConfirmationHandler.confirmResult = confirmationResult + vm.onPrimaryButtonClicked() - var result: LinkActivityResult? = null - val viewModel = createViewModel( - linkAccountManager = linkAccountManager, - linkConfirmationHandler = linkConfirmationHandler, - dismissWithResult = { - result = it - } - ) - viewModel.onItemSelected(validCard) + assertThat(vm.uiState.value.isProcessing).isTrue() - viewModel.onPrimaryButtonClicked() + dispatcher.scheduler.advanceTimeBy(1.5.seconds) - assertThat(viewModel.uiState.value.errorMessage).isNull() - assertThat(result).isNull() + assertThat(vm.uiState.value.errorMessage).isEqualTo(errorMessage) + assertThat(vm.uiState.value.isProcessing).isFalse() } private fun createViewModel( - linkAccountManager: WalletLinkAccountManager = WalletLinkAccountManager(), + linkAccountManager: LinkAccountManager = FakeLinkAccountManager(), logger: Logger = FakeLogger(), linkConfirmationHandler: LinkConfirmationHandler = FakeLinkConfirmationHandler(), - navigate: (route: LinkScreen) -> Unit = {}, navigateAndClearStack: (route: LinkScreen) -> Unit = {}, dismissWithResult: (LinkActivityResult) -> Unit = {} ): WalletViewModel { @@ -371,27 +187,10 @@ class WalletViewModelTest { configuration = TestFactory.LINK_CONFIGURATION, linkAccount = TestFactory.LINK_ACCOUNT, linkAccountManager = linkAccountManager, - linkConfirmationHandler = linkConfirmationHandler, logger = logger, - navigate = navigate, + linkConfirmationHandler = linkConfirmationHandler, navigateAndClearStack = navigateAndClearStack, dismissWithResult = dismissWithResult ) } } - -private class WalletLinkAccountManager : FakeLinkAccountManager() { - val listPaymentDetailsCalls = arrayListOf>() - val updatePaymentDetailsCalls = arrayListOf() - override suspend fun listPaymentDetails(paymentMethodTypes: Set): Result { - listPaymentDetailsCalls.add(paymentMethodTypes) - return super.listPaymentDetails(paymentMethodTypes) - } - - override suspend fun updatePaymentDetails( - updateParams: ConsumerPaymentDetailsUpdateParams - ): Result { - updatePaymentDetailsCalls.add(updateParams) - return super.updatePaymentDetails(updateParams) - } -}