diff --git a/paymentsheet/detekt-baseline.xml b/paymentsheet/detekt-baseline.xml index 29d6a472d79..d8ca452e7ba 100644 --- a/paymentsheet/detekt-baseline.xml +++ b/paymentsheet/detekt-baseline.xml @@ -3,6 +3,7 @@ ConstructorParameterNaming:BankFormScreenState.kt$BankFormScreenState$private val _isProcessing: Boolean = false + CyclomaticComplexMethod:ConfirmationOptionKtx.kt$internal fun PaymentSelection.toConfirmationOption( configuration: CommonConfiguration, linkConfiguration: LinkConfiguration?, ): ConfirmationHandler.Option? CyclomaticComplexMethod:CustomerSheetViewModel.kt$CustomerSheetViewModel$fun handleViewAction(viewAction: CustomerSheetViewAction) CyclomaticComplexMethod:PlaceholderHelper.kt$PlaceholderHelper$@VisibleForTesting internal fun specForPlaceholderField( field: PlaceholderField, placeholderOverrideList: List<IdentifierSpec>, requiresMandate: Boolean, configuration: PaymentSheet.BillingDetailsCollectionConfiguration, ) CyclomaticComplexMethod:TransformSpecToElements.kt$TransformSpecToElements$fun transform( specs: List<FormItemSpec>, placeholderOverrideList: List<IdentifierSpec> = emptyList(), ): List<FormElement> @@ -25,6 +26,7 @@ LargeClass:PaymentSheetViewModelTest.kt$PaymentSheetViewModelTest LargeClass:USBankAccountFormViewModelTest.kt$USBankAccountFormViewModelTest LongMethod:AutocompleteScreen.kt$@Composable internal fun AutocompleteScreenUI(viewModel: AutocompleteViewModel) + LongMethod:ConfirmationOptionKtx.kt$internal fun PaymentSelection.toConfirmationOption( configuration: CommonConfiguration, linkConfiguration: LinkConfiguration?, ): ConfirmationHandler.Option? LongMethod:CustomerSheetScreen.kt$@Composable internal fun SelectPaymentMethod( viewState: CustomerSheetViewState.SelectPaymentMethod, viewActionHandler: (CustomerSheetViewAction) -> Unit, paymentMethodNameProvider: (PaymentMethodCode?) -> ResolvableString, modifier: Modifier = Modifier, ) LongMethod:DefaultConfirmationHandlerTest.kt$DefaultConfirmationHandlerTest$private fun test( someDefinitionAction: ConfirmationDefinition.Action<SomeConfirmationDefinition.LauncherArgs> = ConfirmationDefinition.Action.Fail( cause = IllegalStateException("Failed!"), message = R.string.stripe_something_went_wrong.resolvableString, errorType = ConfirmationHandler.Result.Failed.ErrorType.Internal, ), someDefinitionResult: ConfirmationDefinition.Result = ConfirmationDefinition.Result.Failed( cause = IllegalStateException("Failed!"), message = R.string.stripe_something_went_wrong.resolvableString, type = ConfirmationHandler.Result.Failed.ErrorType.Internal, ), someDefinitionIsConfirmable: Boolean = true, someOtherDefinitionAction: ConfirmationDefinition.Action<SomeOtherConfirmationDefinition.LauncherArgs> = ConfirmationDefinition.Action.Fail( cause = IllegalStateException("Failed!"), message = R.string.stripe_something_went_wrong.resolvableString, errorType = ConfirmationHandler.Result.Failed.ErrorType.Internal, ), someOtherDefinitionResult: ConfirmationDefinition.Result = ConfirmationDefinition.Result.Failed( cause = IllegalStateException("Failed!"), message = R.string.stripe_something_went_wrong.resolvableString, type = ConfirmationHandler.Result.Failed.ErrorType.Internal, ), shouldRegister: Boolean = true, savedStateHandle: SavedStateHandle = SavedStateHandle(), dispatcher: CoroutineDispatcher = UnconfinedTestDispatcher(), scenarioTest: suspend Scenario.() -> Unit ) LongMethod:EmbeddedContentHelper.kt$DefaultEmbeddedContentHelper$private fun createInteractor( coroutineScope: CoroutineScope, paymentMethodMetadata: PaymentMethodMetadata, walletsState: StateFlow<WalletsState?>, ): PaymentMethodVerticalLayoutInteractor diff --git a/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/utils/ProductIntegrationType.kt b/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/utils/ProductIntegrationType.kt index 3a40178d6be..5cc6ad55a23 100644 --- a/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/utils/ProductIntegrationType.kt +++ b/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/utils/ProductIntegrationType.kt @@ -9,6 +9,6 @@ internal enum class ProductIntegrationType { internal object ProductIntegrationTypeProvider : TestParameterValuesProvider() { override fun provideValues(context: Context?): List { - return ProductIntegrationType.entries + return listOf(ProductIntegrationType.FlowController) } } diff --git a/paymentsheet/src/main/java/com/stripe/android/link/ui/inline/UserInput.kt b/paymentsheet/src/main/java/com/stripe/android/link/ui/inline/UserInput.kt index f3cb06dfa91..a7460e903c0 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/ui/inline/UserInput.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/ui/inline/UserInput.kt @@ -6,11 +6,11 @@ import kotlinx.parcelize.Parcelize /** * Valid user input into the inline sign up view. */ -@Parcelize internal sealed class UserInput : Parcelable { /** * Represents an input that is valid for signing in to a link account. */ + @Parcelize data class SignIn( val email: String ) : UserInput() @@ -18,6 +18,7 @@ internal sealed class UserInput : Parcelable { /** * Represents an input that is valid for signing up to a link account. */ + @Parcelize data class SignUp( val email: String, val phone: String, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/ConfirmationOptionKtx.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/ConfirmationOptionKtx.kt index 0353aeecba7..a27892185eb 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/ConfirmationOptionKtx.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/ConfirmationOptionKtx.kt @@ -8,6 +8,7 @@ import com.stripe.android.paymentelement.confirmation.bacs.BacsConfirmationOptio import com.stripe.android.paymentelement.confirmation.epms.ExternalPaymentMethodConfirmationOption import com.stripe.android.paymentelement.confirmation.gpay.GooglePayConfirmationOption import com.stripe.android.paymentelement.confirmation.link.LinkConfirmationOption +import com.stripe.android.paymentelement.confirmation.linkinline.LinkInlineSignupConfirmationOption import com.stripe.android.paymentsheet.model.PaymentSelection internal fun PaymentSelection.toConfirmationOption( @@ -39,6 +40,22 @@ internal fun PaymentSelection.toConfirmationOption( ) } } + is PaymentSelection.New.LinkInline -> linkConfiguration?.let { + LinkInlineSignupConfirmationOption( + createParams = paymentMethodCreateParams, + optionsParams = paymentMethodOptionsParams, + userInput = input, + linkConfiguration = linkConfiguration, + saveOption = when (customerRequestedSave) { + PaymentSelection.CustomerRequestedSave.RequestReuse -> + LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.RequestedReuse + PaymentSelection.CustomerRequestedSave.RequestNoReuse -> + LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.RequestedNoReuse + PaymentSelection.CustomerRequestedSave.NoRequest -> + LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.NoRequest + } + ) + } is PaymentSelection.New -> { if (paymentMethodCreateParams.typeCode == PaymentMethod.Type.BacsDebit.code) { BacsConfirmationOption( diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/injection/PaymentElementConfirmationModule.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/injection/PaymentElementConfirmationModule.kt index a4ef1ebff45..7a3a9f5d54f 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/injection/PaymentElementConfirmationModule.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/injection/PaymentElementConfirmationModule.kt @@ -4,6 +4,7 @@ import com.stripe.android.paymentelement.confirmation.bacs.BacsConfirmationModul import com.stripe.android.paymentelement.confirmation.epms.ExternalPaymentMethodConfirmationModule import com.stripe.android.paymentelement.confirmation.gpay.GooglePayConfirmationModule import com.stripe.android.paymentelement.confirmation.link.LinkConfirmationModule +import com.stripe.android.paymentelement.confirmation.linkinline.LinkInlineSignupConfirmationModule import dagger.Module @Module( @@ -13,6 +14,7 @@ import dagger.Module ExternalPaymentMethodConfirmationModule::class, GooglePayConfirmationModule::class, LinkConfirmationModule::class, + LinkInlineSignupConfirmationModule::class, ] ) internal interface PaymentElementConfirmationModule diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationDefinition.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationDefinition.kt new file mode 100644 index 00000000000..5a8173facaf --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationDefinition.kt @@ -0,0 +1,196 @@ +package com.stripe.android.paymentelement.confirmation.linkinline + +import android.os.Parcelable +import androidx.activity.result.ActivityResultCaller +import com.stripe.android.link.LinkConfigurationCoordinator +import com.stripe.android.link.LinkPaymentDetails +import com.stripe.android.link.account.LinkStore +import com.stripe.android.link.analytics.LinkAnalyticsHelper +import com.stripe.android.link.model.AccountStatus +import com.stripe.android.model.ConfirmPaymentIntentParams +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.ConfirmationDefinition +import com.stripe.android.paymentelement.confirmation.ConfirmationHandler +import com.stripe.android.paymentelement.confirmation.PaymentMethodConfirmationOption +import com.stripe.android.paymentelement.confirmation.intent.DeferredIntentConfirmationType +import kotlinx.coroutines.flow.first +import kotlinx.parcelize.Parcelize + +internal class LinkInlineSignupConfirmationDefinition( + private val linkConfigurationCoordinator: LinkConfigurationCoordinator, + private val linkAnalyticsHelper: LinkAnalyticsHelper, + private val linkStore: LinkStore, +) : ConfirmationDefinition< + LinkInlineSignupConfirmationOption, + LinkInlineSignupConfirmationDefinition.Launcher, + LinkInlineSignupConfirmationDefinition.LauncherArguments, + LinkInlineSignupConfirmationDefinition.Result, + > { + override val key: String = "LinkInlineSignup" + + override fun option(confirmationOption: ConfirmationHandler.Option): LinkInlineSignupConfirmationOption? { + return confirmationOption as? LinkInlineSignupConfirmationOption + } + + override suspend fun action( + confirmationOption: LinkInlineSignupConfirmationOption, + confirmationParameters: ConfirmationDefinition.Parameters + ): ConfirmationDefinition.Action { + val nextConfirmationOption = createPaymentMethodConfirmationOption(confirmationOption) + + return ConfirmationDefinition.Action.Launch( + launcherArguments = LauncherArguments(nextConfirmationOption), + receivesResultInProcess = true, + deferredIntentConfirmationType = null, + ) + } + + override fun createLauncher( + activityResultCaller: ActivityResultCaller, + onResult: (Result) -> Unit + ): Launcher { + return Launcher(onResult) + } + + override fun launch( + launcher: Launcher, + arguments: LauncherArguments, + confirmationOption: LinkInlineSignupConfirmationOption, + confirmationParameters: ConfirmationDefinition.Parameters, + ) { + launcher.onResult(Result(arguments.nextConfirmationOption)) + } + + override fun toResult( + confirmationOption: LinkInlineSignupConfirmationOption, + confirmationParameters: ConfirmationDefinition.Parameters, + deferredIntentConfirmationType: DeferredIntentConfirmationType?, + result: Result, + ): ConfirmationDefinition.Result { + return ConfirmationDefinition.Result.NextStep( + confirmationOption = result.nextConfirmationOption, + parameters = confirmationParameters, + ) + } + + private suspend fun createPaymentMethodConfirmationOption( + linkInlineSignupConfirmationOption: LinkInlineSignupConfirmationOption, + ): PaymentMethodConfirmationOption { + val configuration = linkInlineSignupConfirmationOption.linkConfiguration + val userInput = linkInlineSignupConfirmationOption.userInput + + return when (linkConfigurationCoordinator.getAccountStatusFlow(configuration).first()) { + AccountStatus.Verified -> createOptionAfterAttachingToLink(linkInlineSignupConfirmationOption) + AccountStatus.VerificationStarted, + AccountStatus.NeedsVerification -> { + linkAnalyticsHelper.onLinkPopupSkipped() + + linkInlineSignupConfirmationOption.toNewOption() + } + AccountStatus.SignedOut, + AccountStatus.Error -> { + linkConfigurationCoordinator.signInWithUserInput(configuration, userInput).fold( + onSuccess = { + // If successful, the account was fetched or created, so try again + createPaymentMethodConfirmationOption(linkInlineSignupConfirmationOption) + }, + onFailure = { + linkInlineSignupConfirmationOption.toNewOption() + } + ) + } + } + } + + private suspend fun createOptionAfterAttachingToLink( + linkInlineSignupConfirmationOption: LinkInlineSignupConfirmationOption, + ): PaymentMethodConfirmationOption { + val createParams = linkInlineSignupConfirmationOption.createParams + val saveOption = linkInlineSignupConfirmationOption.saveOption + + val linkPaymentDetails = linkConfigurationCoordinator.attachNewCardToAccount( + linkInlineSignupConfirmationOption.linkConfiguration, + createParams, + ).getOrNull() + + return when (linkPaymentDetails) { + is LinkPaymentDetails.New -> { + linkStore.markLinkAsUsed() + + linkPaymentDetails.toNewOption(saveOption) + } + is LinkPaymentDetails.Saved -> { + linkStore.markLinkAsUsed() + + linkPaymentDetails.toSavedOption(createParams, saveOption) + } + null -> linkInlineSignupConfirmationOption.toNewOption() + } + } + + private fun LinkPaymentDetails.Saved.toSavedOption( + createParams: PaymentMethodCreateParams, + saveOption: LinkInlineSignupConfirmationOption.PaymentMethodSaveOption, + ): PaymentMethodConfirmationOption.Saved { + val last4 = paymentDetails.last4 + + return PaymentMethodConfirmationOption.Saved( + paymentMethod = PaymentMethod.Builder() + .setId(paymentDetails.id) + .setCode(createParams.typeCode) + .setCard( + PaymentMethod.Card( + last4 = last4, + wallet = Wallet.LinkWallet(last4), + ) + ) + .setType(PaymentMethod.Type.Card) + .build(), + optionsParams = PaymentMethodOptionsParams.Card( + setupFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.OffSession?.takeIf { + saveOption.shouldSave() + } ?: ConfirmPaymentIntentParams.SetupFutureUsage.Blank + ), + ) + } + + private fun LinkPaymentDetails.New.toNewOption( + saveOption: LinkInlineSignupConfirmationOption.PaymentMethodSaveOption + ): PaymentMethodConfirmationOption.New { + return PaymentMethodConfirmationOption.New( + createParams = paymentMethodCreateParams, + optionsParams = PaymentMethodOptionsParams.Card( + setupFutureUsage = saveOption.setupFutureUsage, + ), + shouldSave = saveOption.shouldSave(), + ) + } + + private fun LinkInlineSignupConfirmationOption.toNewOption(): PaymentMethodConfirmationOption.New { + return PaymentMethodConfirmationOption.New( + createParams = createParams, + optionsParams = optionsParams, + shouldSave = saveOption.shouldSave(), + ) + } + + private fun LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.shouldSave(): Boolean { + return this == LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.RequestedReuse + } + + @Parcelize + data class Result( + val nextConfirmationOption: PaymentMethodConfirmationOption, + ) : Parcelable + + data class LauncherArguments( + val nextConfirmationOption: PaymentMethodConfirmationOption, + ) + + class Launcher( + val onResult: (Result) -> Unit, + ) +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationModule.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationModule.kt new file mode 100644 index 00000000000..6efe7cef711 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationModule.kt @@ -0,0 +1,31 @@ +package com.stripe.android.paymentelement.confirmation.linkinline + +import com.stripe.android.link.LinkConfigurationCoordinator +import com.stripe.android.link.account.LinkStore +import com.stripe.android.link.injection.LinkAnalyticsComponent +import com.stripe.android.paymentelement.confirmation.ConfirmationDefinition +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoSet + +@Module( + subcomponents = [ + LinkAnalyticsComponent::class, + ] +) +internal object LinkInlineSignupConfirmationModule { + @JvmSuppressWildcards + @Provides + @IntoSet + fun providesLinkConfirmationDefinition( + linkStore: LinkStore, + linkConfigurationCoordinator: LinkConfigurationCoordinator, + linkAnalyticsComponentBuilder: LinkAnalyticsComponent.Builder, + ): ConfirmationDefinition<*, *, *, *> { + return LinkInlineSignupConfirmationDefinition( + linkStore = linkStore, + linkConfigurationCoordinator = linkConfigurationCoordinator, + linkAnalyticsHelper = linkAnalyticsComponentBuilder.build().linkAnalyticsHelper, + ) + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationOption.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationOption.kt new file mode 100644 index 00000000000..15c4088b765 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationOption.kt @@ -0,0 +1,24 @@ +package com.stripe.android.paymentelement.confirmation.linkinline + +import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.ui.inline.UserInput +import com.stripe.android.model.ConfirmPaymentIntentParams +import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.model.PaymentMethodOptionsParams +import com.stripe.android.paymentelement.confirmation.ConfirmationHandler +import kotlinx.parcelize.Parcelize + +@Parcelize +internal data class LinkInlineSignupConfirmationOption( + val createParams: PaymentMethodCreateParams, + val optionsParams: PaymentMethodOptionsParams?, + val saveOption: PaymentMethodSaveOption, + val linkConfiguration: LinkConfiguration, + val userInput: UserInput, +) : ConfirmationHandler.Option { + enum class PaymentMethodSaveOption(val setupFutureUsage: ConfirmPaymentIntentParams.SetupFutureUsage?) { + RequestedReuse(ConfirmPaymentIntentParams.SetupFutureUsage.OffSession), + RequestedNoReuse(ConfirmPaymentIntentParams.SetupFutureUsage.Blank), + NoRequest(null) + } +} 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 03ed29f3fc2..7b8defc2840 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/LinkHandler.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/LinkHandler.kt @@ -1,64 +1,25 @@ package com.stripe.android.paymentsheet -import androidx.lifecycle.SavedStateHandle -import com.stripe.android.core.strings.resolvableString import com.stripe.android.link.LinkConfiguration import com.stripe.android.link.LinkConfigurationCoordinator -import com.stripe.android.link.LinkPaymentDetails -import com.stripe.android.link.account.LinkStore -import com.stripe.android.link.analytics.LinkAnalyticsHelper -import com.stripe.android.link.injection.LinkAnalyticsComponent -import com.stripe.android.link.model.AccountStatus -import com.stripe.android.link.ui.inline.UserInput -import com.stripe.android.model.ConfirmPaymentIntentParams -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.paymentsheet.model.PaymentSelection import com.stripe.android.paymentsheet.state.LinkState -import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel.Companion.SAVE_PROCESSING import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject internal class LinkHandler @Inject constructor( val linkConfigurationCoordinator: LinkConfigurationCoordinator, - private val savedStateHandle: SavedStateHandle, - private val linkStore: LinkStore, - linkAnalyticsComponentBuilder: LinkAnalyticsComponent.Builder, ) { - sealed class ProcessingState { - data object Ready : ProcessingState() - - data object Started : ProcessingState() - - data class PaymentDetailsCollected( - val paymentSelection: PaymentSelection - ) : ProcessingState() - } - - private val _processingState = - MutableSharedFlow(replay = 1, extraBufferCapacity = 5) - val processingState: Flow = _processingState - private val _isLinkEnabled = MutableStateFlow(null) val isLinkEnabled: StateFlow = _isLinkEnabled private val _linkConfiguration = MutableStateFlow(null) val linkConfiguration: StateFlow = _linkConfiguration.asStateFlow() - private val linkAnalyticsHelper: LinkAnalyticsHelper by lazy { - linkAnalyticsComponentBuilder.build().linkAnalyticsHelper - } - fun setupLink(state: LinkState?) { _isLinkEnabled.value = state != null @@ -67,148 +28,6 @@ internal class LinkHandler @Inject constructor( _linkConfiguration.value = state.configuration } - suspend fun payWithLinkInline( - paymentSelection: PaymentSelection.New.LinkInline, - shouldCompleteLinkInlineFlow: Boolean, - ) { - savedStateHandle[SAVE_PROCESSING] = true - _processingState.emit(ProcessingState.Started) - - val configuration = requireNotNull(_linkConfiguration.value) - - when (linkConfigurationCoordinator.getAccountStatusFlow(configuration).first()) { - AccountStatus.Verified -> { - completeLinkInlinePayment( - paymentSelection, - configuration, - paymentSelection.input is UserInput.SignIn && shouldCompleteLinkInlineFlow - ) - } - AccountStatus.VerificationStarted, - AccountStatus.NeedsVerification -> { - linkAnalyticsHelper.onLinkPopupSkipped() - _processingState.emit(ProcessingState.PaymentDetailsCollected(paymentSelection.toNewSelection())) - } - AccountStatus.SignedOut, - AccountStatus.Error -> { - linkConfigurationCoordinator.signInWithUserInput(configuration, paymentSelection.input).fold( - onSuccess = { - // If successful, the account was fetched or created, so try again - payWithLinkInline( - paymentSelection = paymentSelection, - shouldCompleteLinkInlineFlow = shouldCompleteLinkInlineFlow, - ) - }, - onFailure = { - _processingState.emit( - ProcessingState.PaymentDetailsCollected(paymentSelection.toNewSelection()) - ) - } - ) - } - } - } - - private suspend fun completeLinkInlinePayment( - paymentSelection: PaymentSelection.New.LinkInline, - configuration: LinkConfiguration, - shouldCompleteLinkInlineFlow: Boolean - ) { - val paymentMethodCreateParams = paymentSelection.paymentMethodCreateParams - val customerRequestedSave = paymentSelection.customerRequestedSave - - if (shouldCompleteLinkInlineFlow) { - linkAnalyticsHelper.onLinkPopupSkipped() - _processingState.emit(ProcessingState.PaymentDetailsCollected(paymentSelection.toNewSelection())) - } else { - val linkPaymentDetails = linkConfigurationCoordinator.attachNewCardToAccount( - configuration, - paymentMethodCreateParams - ).getOrNull() - - val nextSelection = when (linkPaymentDetails) { - is LinkPaymentDetails.New -> createGenericSelection( - linkPaymentDetails = linkPaymentDetails, - customerRequestedSave = customerRequestedSave, - ) - is LinkPaymentDetails.Saved -> createSavedSelection( - linkPaymentDetails = linkPaymentDetails, - paymentMethodCreateParams = paymentMethodCreateParams, - customerRequestedSave = customerRequestedSave, - ) - null -> null - } - - if (nextSelection != null) { - linkStore.markLinkAsUsed() - } - - _processingState.emit( - ProcessingState.PaymentDetailsCollected( - paymentSelection = nextSelection ?: paymentSelection.toNewSelection() - ) - ) - } - } - - private fun createGenericSelection( - linkPaymentDetails: LinkPaymentDetails.New, - customerRequestedSave: PaymentSelection.CustomerRequestedSave, - ): PaymentSelection.New.GenericPaymentMethod { - return PaymentSelection.New.GenericPaymentMethod( - paymentMethodCreateParams = linkPaymentDetails.paymentMethodCreateParams, - paymentMethodOptionsParams = PaymentMethodOptionsParams.Card( - setupFutureUsage = customerRequestedSave.setupFutureUsage - ), - paymentMethodExtraParams = null, - customerRequestedSave = customerRequestedSave, - label = resolvableString("···· ${linkPaymentDetails.paymentDetails.last4}"), - iconResource = R.drawable.stripe_ic_paymentsheet_link, - lightThemeIconUrl = null, - darkThemeIconUrl = null, - createdFromLink = true, - ) - } - - private fun createSavedSelection( - linkPaymentDetails: LinkPaymentDetails.Saved, - paymentMethodCreateParams: PaymentMethodCreateParams, - customerRequestedSave: PaymentSelection.CustomerRequestedSave, - ): PaymentSelection.Saved { - val last4 = linkPaymentDetails.paymentDetails.last4 - - return PaymentSelection.Saved( - paymentMethod = PaymentMethod.Builder() - .setId(linkPaymentDetails.paymentDetails.id) - .setCode(paymentMethodCreateParams.typeCode) - .setCard( - PaymentMethod.Card( - last4 = last4, - wallet = Wallet.LinkWallet(last4) - ) - ) - .setType(PaymentMethod.Type.Card) - .build(), - walletType = PaymentSelection.Saved.WalletType.Link, - paymentMethodOptionsParams = PaymentMethodOptionsParams.Card( - setupFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.OffSession?.takeIf { - customerRequestedSave == - PaymentSelection.CustomerRequestedSave.RequestReuse - } ?: ConfirmPaymentIntentParams.SetupFutureUsage.Blank - ) - ) - } - - private fun PaymentSelection.New.LinkInline.toNewSelection(): PaymentSelection.New.Card { - return PaymentSelection.New.Card( - paymentMethodCreateParams = paymentMethodCreateParams, - brand = brand, - customerRequestedSave = customerRequestedSave, - paymentMethodOptionsParams = paymentMethodOptionsParams, - paymentMethodExtraParams = paymentMethodExtraParams - ) - } - @OptIn(DelicateCoroutinesApi::class) fun logOut() { val configuration = linkConfiguration.value ?: return diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsViewModel.kt index ac5e3ccadcf..56a761e2d53 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsViewModel.kt @@ -27,7 +27,6 @@ import com.stripe.android.paymentsheet.state.WalletsProcessingState import com.stripe.android.paymentsheet.state.WalletsState import com.stripe.android.paymentsheet.ui.DefaultAddPaymentMethodInteractor import com.stripe.android.paymentsheet.ui.DefaultSelectSavedPaymentMethodsInteractor -import com.stripe.android.paymentsheet.ui.PrimaryButton import com.stripe.android.paymentsheet.verticalmode.VerticalModeInitialScreenFactory import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel import com.stripe.android.paymentsheet.viewmodels.PrimaryButtonUiStateMapper @@ -40,7 +39,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.coroutines.CoroutineContext @@ -130,12 +128,6 @@ internal class PaymentOptionsViewModel @Inject constructor( init { SessionSavedStateHandler.attachTo(this, savedStateHandle) - viewModelScope.launch { - linkHandler.processingState.collect { processingState -> - handleLinkProcessingState(processingState) - } - } - // This is bad, but I don't think there's a better option PaymentSheet.FlowController.linkHandler = linkHandler @@ -160,21 +152,6 @@ internal class PaymentOptionsViewModel @Inject constructor( ) } - private fun handleLinkProcessingState(processingState: LinkHandler.ProcessingState) { - when (processingState) { - is LinkHandler.ProcessingState.PaymentDetailsCollected -> { - updateSelection(processingState.paymentSelection) - onUserSelection() - } - LinkHandler.ProcessingState.Ready -> { - updatePrimaryButtonState(PrimaryButton.State.Ready) - } - LinkHandler.ProcessingState.Started -> { - updatePrimaryButtonState(PrimaryButton.State.StartProcessing) - } - } - } - override fun onUserCancel() { eventReporter.onDismiss() _paymentOptionResult.tryEmit( @@ -214,14 +191,6 @@ internal class PaymentOptionsViewModel @Inject constructor( eventReporter.onSelectPaymentOption(paymentSelection) when (paymentSelection) { - is PaymentSelection.New.LinkInline -> { - viewModelScope.launch(workContext) { - linkHandler.payWithLinkInline( - paymentSelection = paymentSelection, - shouldCompleteLinkInlineFlow = false, - ) - } - } is PaymentSelection.Saved, is PaymentSelection.GooglePay, is PaymentSelection.Link -> processExistingPaymentMethod(paymentSelection) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt index 3daf2325c62..fd0a8b97865 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt @@ -215,12 +215,6 @@ internal class PaymentSheetViewModel @Inject internal constructor( init { SessionSavedStateHandler.attachTo(this, savedStateHandle) - viewModelScope.launch { - linkHandler.processingState.collect { processingState -> - handleLinkProcessingState(processingState) - } - } - val isDeferred = args.initializationMode is PaymentElementLoader.InitializationMode.DeferredIntent eventReporter.onInit( @@ -233,23 +227,6 @@ internal class PaymentSheetViewModel @Inject internal constructor( } } - private fun handleLinkProcessingState(processingState: LinkHandler.ProcessingState) { - when (processingState) { - is LinkHandler.ProcessingState.PaymentDetailsCollected -> { - updateSelection(processingState.paymentSelection) - checkout(selection.value, CheckoutIdentifier.SheetBottomBuy) - } - LinkHandler.ProcessingState.Ready -> { - this.checkoutIdentifier = CheckoutIdentifier.SheetBottomBuy - viewState.value = PaymentSheetViewState.Reset() - } - LinkHandler.ProcessingState.Started -> { - this.checkoutIdentifier = CheckoutIdentifier.SheetBottomBuy - viewState.value = PaymentSheetViewState.StartProcessing - } - } - } - private suspend fun loadPaymentSheetState() { val result = withContext(workContext) { paymentElementLoader.load( @@ -372,16 +349,7 @@ internal class PaymentSheetViewModel @Inject internal constructor( ) { this.checkoutIdentifier = identifier - if (paymentSelection is PaymentSelection.New.LinkInline) { - viewModelScope.launch(workContext) { - linkHandler.payWithLinkInline( - paymentSelection = paymentSelection, - shouldCompleteLinkInlineFlow = false, - ) - } - } else { - confirmPaymentSelection(paymentSelection) - } + confirmPaymentSelection(paymentSelection) } override fun handlePaymentMethodSelected(selection: PaymentSelection?) { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEvent.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEvent.kt index 254ee44f4e8..9952626bd52 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEvent.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEvent.kt @@ -477,16 +477,7 @@ internal sealed class PaymentSheetEvent : AnalyticsEvent { is PaymentSelection.Link, is PaymentSelection.New.LinkInline -> "link" is PaymentSelection.ExternalPaymentMethod, - is PaymentSelection.New -> { - if ( - paymentSelection is PaymentSelection.New.GenericPaymentMethod && - paymentSelection.createdFromLink - ) { - "link" - } else { - "newpm" - } - } + is PaymentSelection.New -> "newpm" null -> "unknown" } 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 b12163fe741..212a8364edf 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 @@ -236,7 +236,6 @@ internal sealed class PaymentSelection : Parcelable { override val customerRequestedSave: CustomerRequestedSave, override val paymentMethodOptionsParams: PaymentMethodOptionsParams? = null, override val paymentMethodExtraParams: PaymentMethodExtraParams? = null, - val createdFromLink: Boolean = false, ) : New() } @@ -294,7 +293,6 @@ internal val PaymentSelection.isLink: Boolean is PaymentSelection.GooglePay -> false is PaymentSelection.Link -> true is PaymentSelection.New.LinkInline -> true - is PaymentSelection.New.GenericPaymentMethod -> createdFromLink is PaymentSelection.New -> false is PaymentSelection.Saved -> walletType == PaymentSelection.Saved.WalletType.Link is PaymentSelection.ExternalPaymentMethod -> false diff --git a/paymentsheet/src/test/java/com/stripe/android/customersheet/utils/CustomerSheetTestHelper.kt b/paymentsheet/src/test/java/com/stripe/android/customersheet/utils/CustomerSheetTestHelper.kt index e79ca54fd44..ff6b760fd60 100644 --- a/paymentsheet/src/test/java/com/stripe/android/customersheet/utils/CustomerSheetTestHelper.kt +++ b/paymentsheet/src/test/java/com/stripe/android/customersheet/utils/CustomerSheetTestHelper.kt @@ -44,6 +44,7 @@ import com.stripe.android.ui.core.cbc.CardBrandChoiceEligibility import com.stripe.android.utils.CompletableSingle import com.stripe.android.utils.DummyActivityResultCaller import com.stripe.android.utils.FakeIntentConfirmationInterceptor +import com.stripe.android.utils.FakeLinkConfigurationCoordinator import com.stripe.android.utils.RecordingLinkPaymentLauncher import kotlinx.coroutines.CoroutineScope import org.mockito.kotlin.mock @@ -141,6 +142,7 @@ internal object CustomerSheetTestHelper { savedStateHandle = SavedStateHandle(), errorReporter = FakeErrorReporter(), linkLauncher = RecordingLinkPaymentLauncher.noOp(), + linkConfigurationCoordinator = FakeLinkConfigurationCoordinator(), cvcRecollectionLauncherFactory = RecordingCvcRecollectionLauncherFactory.noOp(), ), eventReporter = eventReporter, diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ConfirmationHandlerOptionKtxTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ConfirmationHandlerOptionKtxTest.kt index bf241624a9a..0c170e89e84 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ConfirmationHandlerOptionKtxTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ConfirmationHandlerOptionKtxTest.kt @@ -3,7 +3,9 @@ package com.stripe.android.paymentelement.confirmation import com.google.common.truth.Truth.assertThat import com.stripe.android.common.model.asCommonConfiguration import com.stripe.android.core.strings.resolvableString -import com.stripe.android.link.TestFactory +import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.ui.inline.SignUpConsentAction +import com.stripe.android.link.ui.inline.UserInput import com.stripe.android.lpmfoundations.paymentmethod.PaymentSheetCardBrandFilter import com.stripe.android.model.Address import com.stripe.android.model.CardBrand @@ -17,11 +19,12 @@ import com.stripe.android.paymentelement.confirmation.bacs.BacsConfirmationOptio import com.stripe.android.paymentelement.confirmation.epms.ExternalPaymentMethodConfirmationOption import com.stripe.android.paymentelement.confirmation.gpay.GooglePayConfirmationOption import com.stripe.android.paymentelement.confirmation.link.LinkConfirmationOption +import com.stripe.android.paymentelement.confirmation.linkinline.LinkInlineSignupConfirmationOption import com.stripe.android.paymentsheet.PaymentSheet import com.stripe.android.paymentsheet.PaymentSheetFixtures import com.stripe.android.paymentsheet.R import com.stripe.android.paymentsheet.model.PaymentSelection -import com.stripe.android.paymentsheet.state.PaymentElementLoader +import com.stripe.android.testing.PaymentIntentFactory import com.stripe.android.testing.PaymentMethodFactory import com.stripe.android.utils.BankFormScreenStateFactory import org.junit.Test @@ -262,6 +265,50 @@ class ConfirmationHandlerOptionKtxTest { ) } + @Test + fun `On new Link inline selection without config, should return null`() { + assertThat( + PaymentSelection.New.LinkInline( + paymentMethodCreateParams = PaymentMethodCreateParamsFixtures.DEFAULT_CARD, + brand = CardBrand.Visa, + customerRequestedSave = PaymentSelection.CustomerRequestedSave.NoRequest, + paymentMethodOptionsParams = null, + paymentMethodExtraParams = null, + input = UserInput.SignUp( + email = "email@email.com", + phone = "1234567890", + name = "John Doe", + country = "CA", + consentAction = SignUpConsentAction.Checkbox, + ), + ).toConfirmationOption( + configuration = PaymentSheetFixtures.CONFIG_CUSTOMER.asCommonConfiguration(), + linkConfiguration = null, + ) + ).isNull() + } + + @Test + fun `On new Link inline selection with no reuse request, should return expected confirmation`() = + testLinkInlineSignupConfirmationOption( + customerRequestedSave = PaymentSelection.CustomerRequestedSave.NoRequest, + expectedSaveOption = LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.NoRequest + ) + + @Test + fun `On new Link inline selection with requested reuse, should return expected confirmation`() = + testLinkInlineSignupConfirmationOption( + customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestReuse, + expectedSaveOption = LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.RequestedReuse + ) + + @Test + fun `On new Link inline selection with requested no reuse, should return expected confirmation`() = + testLinkInlineSignupConfirmationOption( + customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestNoReuse, + expectedSaveOption = LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.RequestedNoReuse, + ) + @Test fun `Converts Instant Debits into a saved payment confirmation option`() { val paymentSelection = createNewBankAccountPaymentSelection(linkMode = LinkMode.LinkPaymentMethod) @@ -300,6 +347,42 @@ class ConfirmationHandlerOptionKtxTest { ) } + private fun testLinkInlineSignupConfirmationOption( + customerRequestedSave: PaymentSelection.CustomerRequestedSave, + expectedSaveOption: LinkInlineSignupConfirmationOption.PaymentMethodSaveOption, + ) { + val paymentMethodCreateParams = PaymentMethodCreateParamsFixtures.DEFAULT_CARD + val userInput = UserInput.SignUp( + email = "email@email.com", + phone = "1234567890", + name = "John Doe", + country = "CA", + consentAction = SignUpConsentAction.Checkbox, + ) + + assertThat( + PaymentSelection.New.LinkInline( + paymentMethodCreateParams = paymentMethodCreateParams, + brand = CardBrand.Visa, + customerRequestedSave = customerRequestedSave, + paymentMethodOptionsParams = null, + paymentMethodExtraParams = null, + input = userInput, + ).toConfirmationOption( + configuration = PaymentSheetFixtures.CONFIG_CUSTOMER.asCommonConfiguration(), + linkConfiguration = LINK_CONFIGURATION, + ) + ).isEqualTo( + LinkInlineSignupConfirmationOption( + createParams = paymentMethodCreateParams, + optionsParams = null, + linkConfiguration = LINK_CONFIGURATION, + saveOption = expectedSaveOption, + userInput = userInput, + ) + ) + } + private fun createNewPaymentSelection( createParams: PaymentMethodCreateParams = PaymentMethodCreateParamsFixtures.DEFAULT_CARD, optionsParams: PaymentMethodOptionsParams? = null, @@ -341,14 +424,21 @@ class ConfirmationHandlerOptionKtxTest { } private companion object { - val PI_INITIALIZATION_MODE = PaymentElementLoader.InitializationMode.PaymentIntent( - clientSecret = "pi_123" - ) - - val SI_INITIALIZATION_MODE = PaymentElementLoader.InitializationMode.SetupIntent( - clientSecret = "pi_123" + val LINK_CONFIGURATION = LinkConfiguration( + stripeIntent = PaymentIntentFactory.create(), + merchantName = "Merchant, Inc.", + merchantCountryCode = "CA", + customerInfo = LinkConfiguration.CustomerInfo( + name = "John Doe", + email = null, + phone = null, + billingCountryCode = "CA", + ), + shippingDetails = null, + passthroughModeEnabled = false, + cardBrandChoice = null, + flags = mapOf(), + useAttestationEndpointsForLink = false, ) - - val LINK_CONFIGURATION = TestFactory.LINK_CONFIGURATION } } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ConfirmationTestUtils.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ConfirmationTestUtils.kt index 19c02dc7728..698c9700475 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ConfirmationTestUtils.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ConfirmationTestUtils.kt @@ -110,6 +110,10 @@ internal fun ConfirmationHandler.Option.asSaved(): PaymentMethodConfirmationOpti return this as PaymentMethodConfirmationOption.Saved } +internal fun ConfirmationHandler.Option.asNew(): PaymentMethodConfirmationOption.New { + return this as PaymentMethodConfirmationOption.New +} + internal fun ConfirmationDefinition.Result?.asSucceeded(): ConfirmationDefinition.Result.Succeeded { return this as ConfirmationDefinition.Result.Succeeded } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ConfirmationUtils.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ConfirmationUtils.kt index f71b5a17918..54b2fdefaeb 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ConfirmationUtils.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ConfirmationUtils.kt @@ -3,7 +3,9 @@ package com.stripe.android.paymentelement.confirmation import androidx.lifecycle.SavedStateHandle import com.stripe.android.PaymentConfiguration import com.stripe.android.googlepaylauncher.injection.GooglePayPaymentMethodLauncherFactory +import com.stripe.android.link.LinkConfigurationCoordinator import com.stripe.android.link.LinkPaymentLauncher +import com.stripe.android.link.analytics.FakeLinkAnalyticsHelper import com.stripe.android.paymentelement.confirmation.bacs.BacsConfirmationDefinition import com.stripe.android.paymentelement.confirmation.cvc.CvcRecollectionConfirmationDefinition import com.stripe.android.paymentelement.confirmation.epms.ExternalPaymentMethodConfirmationDefinition @@ -11,6 +13,7 @@ import com.stripe.android.paymentelement.confirmation.gpay.GooglePayConfirmation import com.stripe.android.paymentelement.confirmation.intent.IntentConfirmationDefinition import com.stripe.android.paymentelement.confirmation.intent.IntentConfirmationInterceptor import com.stripe.android.paymentelement.confirmation.link.LinkConfirmationDefinition +import com.stripe.android.paymentelement.confirmation.linkinline.LinkInlineSignupConfirmationDefinition import com.stripe.android.payments.core.analytics.ErrorReporter import com.stripe.android.payments.paymentlauncher.StripePaymentLauncherAssistedFactory import com.stripe.android.paymentsheet.ExternalPaymentMethodInterceptor @@ -28,6 +31,7 @@ internal fun createTestConfirmationHandlerFactory( stripePaymentLauncherAssistedFactory: StripePaymentLauncherAssistedFactory, googlePayPaymentMethodLauncherFactory: GooglePayPaymentMethodLauncherFactory, cvcRecollectionLauncherFactory: CvcRecollectionLauncherFactory, + linkConfigurationCoordinator: LinkConfigurationCoordinator, linkLauncher: LinkPaymentLauncher, paymentConfiguration: PaymentConfiguration, statusBarColor: Int?, @@ -65,6 +69,11 @@ internal fun createTestConfirmationHandlerFactory( linkPaymentLauncher = linkLauncher, linkStore = RecordingLinkStore.noOp(), ), + LinkInlineSignupConfirmationDefinition( + linkConfigurationCoordinator = linkConfigurationCoordinator, + linkStore = RecordingLinkStore.noOp(), + linkAnalyticsHelper = FakeLinkAnalyticsHelper(), + ), CvcRecollectionConfirmationDefinition( factory = cvcRecollectionLauncherFactory, handler = CvcRecollectionHandlerImpl(), diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ExtendedPaymentElementConfirmationTestActivity.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ExtendedPaymentElementConfirmationTestActivity.kt index c8875c1bbe3..7c26611ac9d 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ExtendedPaymentElementConfirmationTestActivity.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/ExtendedPaymentElementConfirmationTestActivity.kt @@ -23,6 +23,7 @@ import com.stripe.android.core.utils.UserFacingLogger import com.stripe.android.core.utils.requireApplication import com.stripe.android.googlepaylauncher.GooglePayEnvironment import com.stripe.android.googlepaylauncher.GooglePayRepository +import com.stripe.android.link.LinkConfigurationCoordinator import com.stripe.android.networking.StripeApiRepository import com.stripe.android.networking.StripeRepository import com.stripe.android.paymentelement.confirmation.injection.ExtendedPaymentElementConfirmationModule @@ -34,6 +35,7 @@ import com.stripe.android.testing.FakeAnalyticsRequestExecutor import com.stripe.android.testing.FakeErrorReporter import com.stripe.android.testing.FakeLogger import com.stripe.android.utils.FakeDurationProvider +import com.stripe.android.utils.FakeLinkConfigurationCoordinator import dagger.Binds import dagger.BindsInstance import dagger.Component @@ -159,5 +161,9 @@ internal interface ExtendedPaymentElementConfirmationTestModule { @Provides @Named(STRIPE_ACCOUNT_ID) fun providesStripeAccountId(config: PaymentConfiguration): () -> String? = { config.stripeAccountId } + + @Provides + fun providesFakeLinkConfigurationCoordinator(): LinkConfigurationCoordinator = + FakeLinkConfigurationCoordinator() } } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/PaymentElementConfirmationTestActivity.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/PaymentElementConfirmationTestActivity.kt index ea13602b514..4e0e1121ccb 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/PaymentElementConfirmationTestActivity.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/PaymentElementConfirmationTestActivity.kt @@ -23,6 +23,7 @@ import com.stripe.android.core.utils.UserFacingLogger import com.stripe.android.core.utils.requireApplication import com.stripe.android.googlepaylauncher.GooglePayEnvironment import com.stripe.android.googlepaylauncher.GooglePayRepository +import com.stripe.android.link.LinkConfigurationCoordinator import com.stripe.android.networking.StripeApiRepository import com.stripe.android.networking.StripeRepository import com.stripe.android.paymentelement.confirmation.injection.PaymentElementConfirmationModule @@ -34,6 +35,7 @@ import com.stripe.android.testing.FakeAnalyticsRequestExecutor import com.stripe.android.testing.FakeErrorReporter import com.stripe.android.testing.FakeLogger import com.stripe.android.utils.FakeDurationProvider +import com.stripe.android.utils.FakeLinkConfigurationCoordinator import dagger.Binds import dagger.BindsInstance import dagger.Component @@ -159,5 +161,9 @@ internal interface PaymentElementConfirmationTestModule { @Provides @Named(STRIPE_ACCOUNT_ID) fun providesStripeAccountId(config: PaymentConfiguration): () -> String? = { config.stripeAccountId } + + @Provides + fun providesFakeLinkConfigurationCoordinator(): LinkConfigurationCoordinator = + FakeLinkConfigurationCoordinator() } } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationDefinitionTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationDefinitionTest.kt new file mode 100644 index 00000000000..3eada062be4 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationDefinitionTest.kt @@ -0,0 +1,821 @@ +package com.stripe.android.paymentelement.confirmation.linkinline + +import app.cash.turbine.ReceiveTurbine +import app.cash.turbine.Turbine +import com.google.common.truth.Truth.assertThat +import com.stripe.android.core.model.CountryCode +import com.stripe.android.isInstanceOf +import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.LinkConfigurationCoordinator +import com.stripe.android.link.LinkPaymentDetails +import com.stripe.android.link.account.LinkStore +import com.stripe.android.link.analytics.FakeLinkAnalyticsHelper +import com.stripe.android.link.analytics.LinkAnalyticsHelper +import com.stripe.android.link.injection.LinkComponent +import com.stripe.android.link.model.AccountStatus +import com.stripe.android.link.ui.inline.SignUpConsentAction +import com.stripe.android.link.ui.inline.UserInput +import com.stripe.android.model.CardBrand +import com.stripe.android.model.CardParams +import com.stripe.android.model.ConfirmPaymentIntentParams +import com.stripe.android.model.ConsumerPaymentDetails +import com.stripe.android.model.ConsumerSession +import com.stripe.android.model.CvcCheck +import com.stripe.android.model.PaymentMethod +import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.model.PaymentMethodCreateParamsFixtures +import com.stripe.android.model.PaymentMethodOptionsParams +import com.stripe.android.model.wallets.Wallet +import com.stripe.android.paymentelement.confirmation.ConfirmationDefinition +import com.stripe.android.paymentelement.confirmation.FakeConfirmationOption +import com.stripe.android.paymentelement.confirmation.PaymentMethodConfirmationOption +import com.stripe.android.paymentelement.confirmation.asLaunch +import com.stripe.android.paymentelement.confirmation.asNew +import com.stripe.android.paymentelement.confirmation.asNextStep +import com.stripe.android.paymentelement.confirmation.asSaved +import com.stripe.android.paymentsheet.PaymentSheet +import com.stripe.android.paymentsheet.addresselement.AddressDetails +import com.stripe.android.paymentsheet.state.PaymentElementLoader +import com.stripe.android.testing.PaymentIntentFactory +import com.stripe.android.testing.PaymentMethodFactory +import com.stripe.android.utils.DummyActivityResultCaller +import com.stripe.android.utils.FakeLinkConfigurationCoordinator +import com.stripe.android.utils.RecordingLinkStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.mockito.kotlin.mock + +internal class LinkInlineSignupConfirmationDefinitionTest { + @Test + fun `'key' should be 'LinkInlineSignup'`() { + val definition = createLinkInlineSignupConfirmationDefinition() + + assertThat(definition.key).isEqualTo("LinkInlineSignup") + } + + @Test + fun `'option' return casted 'LinkInlineSignupConfirmationOption'`() { + val definition = createLinkInlineSignupConfirmationDefinition() + + val option = createLinkInlineSignupConfirmationOption() + + assertThat(definition.option(option)).isEqualTo(option) + } + + @Test + fun `'option' return null for unknown option`() { + val definition = createLinkInlineSignupConfirmationDefinition() + + assertThat(definition.option(FakeConfirmationOption())).isNull() + } + + @Test + fun `'createLauncher' should create launcher properly`() = test { + val definition = createLinkInlineSignupConfirmationDefinition() + + val activityResultCaller = DummyActivityResultCaller.noOp() + val onResult: (LinkInlineSignupConfirmationDefinition.Result) -> Unit = {} + + val launcher = definition.createLauncher( + activityResultCaller = activityResultCaller, + onResult = onResult, + ) + + assertThat(launcher.onResult).isEqualTo(onResult) + } + + @Test + fun `'action' should skip signup if signup failed on 'SignedOut' account status`() = + testSkippedLinkSignupOnSignInError( + accountStatus = AccountStatus.SignedOut, + ) + + @Test + fun `'action' should skip signup if signup failed on 'Error' account status`() = + testSkippedLinkSignupOnSignInError( + accountStatus = AccountStatus.Error, + ) + + @Test + fun `'action' should skip signup and return 'Launch' on 'VerificationStarted' account status`() = + testSkippedLinkSignupOnAccountStatus( + accountStatus = AccountStatus.VerificationStarted, + ) + + @Test + fun `'action' should skip signup and return 'Launch' on 'NeedsVerification' account status`() = + testSkippedLinkSignupOnAccountStatus( + accountStatus = AccountStatus.NeedsVerification, + ) + + @Test + fun `'action' should return 'Launch' with new option with null SFU if no reuse request`() = + testSuccessfulSignupWithNewCard( + saveOption = LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.NoRequest, + expectedSetupForFutureUsage = null, + expectedShouldSave = false, + ) + + @Test + fun `'action' should return 'Launch' with new option with 'Blank' SFU if requested no reuse`() = + testSuccessfulSignupWithNewCard( + saveOption = LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.RequestedNoReuse, + expectedSetupForFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.Blank, + expectedShouldSave = false, + ) + + @Test + fun `'action' should return 'Launch' with new option with 'OffSession' SFU if requested reuse`() = + testSuccessfulSignupWithNewCard( + saveOption = LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.RequestedReuse, + expectedSetupForFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.OffSession, + expectedShouldSave = true, + ) + + @Test + fun `'action' should return 'Launch' with saved option with 'Blank' SFU if no reuse request`() = + testSuccessfulSignupWithSavedLinkCard( + saveOption = LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.NoRequest, + expectedSetupForFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.Blank, + ) + + @Test + fun `'action' should return 'Launch' with saved option with 'Blank' SFU if requested no reuse`() = + testSuccessfulSignupWithSavedLinkCard( + saveOption = LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.RequestedNoReuse, + expectedSetupForFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.Blank, + ) + + @Test + fun `'action' should return 'Launch' with saved option with 'OffSession' SFU if requested reuse`() = + testSuccessfulSignupWithSavedLinkCard( + saveOption = LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.RequestedReuse, + expectedSetupForFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.OffSession, + ) + + @Test + fun `'action' should skip & return 'Launch' if failed to attach card`() = test( + attachNewCardToAccountResult = Result.failure(IllegalStateException("Failed!")), + initialAccountStatus = AccountStatus.Verified, + ) { + val confirmationOption = createLinkInlineSignupConfirmationOption() + + val action = definition.action( + confirmationOption = confirmationOption, + confirmationParameters = CONFIRMATION_PARAMETERS, + ) + + val getAccountStatusFlowCall = coordinatorScenario.getAccountStatusFlowCalls.awaitItem() + + assertThat(getAccountStatusFlowCall.configuration).isEqualTo(confirmationOption.linkConfiguration) + + val attachNewCardToAccountCall = coordinatorScenario.attachNewCardToAccountCalls.awaitItem() + + assertThat(attachNewCardToAccountCall.configuration).isEqualTo(confirmationOption.linkConfiguration) + assertThat(attachNewCardToAccountCall.paymentMethodCreateParams) + .isEqualTo(confirmationOption.createParams) + + assertThat(action).isInstanceOf>() + + val launchAction = action.asLaunch() + + validateSkippedLaunchAction(confirmationOption, launchAction) + } + + @Test + fun `'action' should return 'Launch' after successful sign-in & attach`() = test( + attachNewCardToAccountResult = Result.success( + LinkPaymentDetails.Saved( + paymentDetails = ConsumerPaymentDetails.Card( + id = "pm_1", + last4 = "4242", + isDefault = false, + expiryYear = 2030, + expiryMonth = 4, + brand = CardBrand.Visa, + cvcCheck = CvcCheck.Pass, + billingAddress = null, + ), + paymentMethodCreateParams = PaymentMethodCreateParamsFixtures.DEFAULT_CARD, + ) + ), + signInResult = Result.success(true), + initialAccountStatus = AccountStatus.SignedOut, + accountStatusOnSignIn = AccountStatus.Verified, + ) { + val confirmationOption = createLinkInlineSignupConfirmationOption() + + val action = definition.action( + confirmationOption = confirmationOption, + confirmationParameters = CONFIRMATION_PARAMETERS, + ) + + val firstGetAccountStatusFlowCall = coordinatorScenario.getAccountStatusFlowCalls.awaitItem() + + assertThat(firstGetAccountStatusFlowCall.configuration).isEqualTo(confirmationOption.linkConfiguration) + + val signInCall = coordinatorScenario.signInCalls.awaitItem() + + assertThat(signInCall.configuration).isEqualTo(confirmationOption.linkConfiguration) + assertThat(signInCall.userInput).isEqualTo(confirmationOption.userInput) + + val secondGetAccountStatusFlowCall = coordinatorScenario.getAccountStatusFlowCalls.awaitItem() + + assertThat(secondGetAccountStatusFlowCall.configuration).isEqualTo(confirmationOption.linkConfiguration) + + val attachNewCardToAccountCall = coordinatorScenario.attachNewCardToAccountCalls.awaitItem() + + assertThat(attachNewCardToAccountCall.configuration).isEqualTo(confirmationOption.linkConfiguration) + assertThat(attachNewCardToAccountCall.paymentMethodCreateParams) + .isEqualTo(confirmationOption.createParams) + + assertThat(action).isInstanceOf>() + + val launchAction = action.asLaunch() + + val nextConfirmationOption = launchAction.launcherArguments.nextConfirmationOption + + assertThat(nextConfirmationOption).isInstanceOf() + + val savedConfirmationOption = nextConfirmationOption.asSaved() + + assertThat(savedConfirmationOption.optionsParams).isEqualTo( + PaymentMethodOptionsParams.Card( + setupFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.Blank, + ) + ) + + val paymentMethod = savedConfirmationOption.paymentMethod + + assertThat(paymentMethod.id).isEqualTo("pm_1") + assertThat(paymentMethod.type).isEqualTo(PaymentMethod.Type.Card) + assertThat(paymentMethod.card?.last4).isEqualTo("4242") + assertThat(paymentMethod.card?.wallet).isEqualTo(Wallet.LinkWallet(dynamicLast4 = "4242")) + + assertThat(launchAction.receivesResultInProcess).isTrue() + assertThat(launchAction.deferredIntentConfirmationType).isNull() + + assertThat(storeScenario.markAsUsedCalls.awaitItem()).isNotNull() + } + + @Test + fun `'launch' should immediately call 'onResult'`() = test { + val definition = createLinkInlineSignupConfirmationDefinition() + val launcher = LinkInlineSignupConfirmationDefinition.Launcher(onResultScenario.onResult) + + val nextOption = PaymentMethodConfirmationOption.Saved( + paymentMethod = PaymentMethodFactory.card(random = true), + optionsParams = PaymentMethodOptionsParams.Card( + setupFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.OnSession, + ), + ) + + definition.launch( + confirmationOption = createLinkInlineSignupConfirmationOption(), + confirmationParameters = CONFIRMATION_PARAMETERS, + launcher = launcher, + arguments = LinkInlineSignupConfirmationDefinition.LauncherArguments( + nextConfirmationOption = nextOption, + ), + ) + + val onResultCall = onResultScenario.onResultCalls.awaitItem() + + assertThat(onResultCall.result.nextConfirmationOption).isEqualTo(nextOption) + } + + @Test + fun `'toResult' should be 'NextStep' on result`() = test { + val definition = createLinkInlineSignupConfirmationDefinition(linkStore = storeScenario.linkStore) + + val nextOption = PaymentMethodConfirmationOption.Saved( + paymentMethod = PaymentMethodFactory.card(random = true), + optionsParams = PaymentMethodOptionsParams.Card( + setupFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.OnSession, + ), + ) + + val result = definition.toResult( + confirmationOption = createLinkInlineSignupConfirmationOption(), + confirmationParameters = CONFIRMATION_PARAMETERS, + result = LinkInlineSignupConfirmationDefinition.Result( + nextConfirmationOption = nextOption, + ), + deferredIntentConfirmationType = null, + ) + + assertThat(result).isInstanceOf() + + val nextStepResult = result.asNextStep() + + assertThat(nextStepResult.confirmationOption).isEqualTo(nextOption) + assertThat(nextStepResult.parameters).isEqualTo(CONFIRMATION_PARAMETERS) + } + + private fun testSkippedLinkSignupOnSignInError( + accountStatus: AccountStatus + ) = test { + val userInput = UserInput.SignIn(email = "email@email.com") + val confirmationOption = createLinkInlineSignupConfirmationOption( + userInput = userInput, + ) + + actionTest( + attachNewCardToAccountResult = Result.failure(IllegalStateException("Should not be used!")), + accountStatus = accountStatus, + signInResult = Result.failure(IllegalStateException("Something went wrong!")), + confirmationOption = confirmationOption, + ) { launchAction -> + val signInCall = coordinatorScenario.signInCalls.awaitItem() + + assertThat(signInCall.configuration).isEqualTo(confirmationOption.linkConfiguration) + assertThat(signInCall.userInput).isEqualTo(userInput) + + validateSkippedLaunchAction(confirmationOption, launchAction) + } + } + + private fun testSkippedLinkSignupOnAccountStatus( + accountStatus: AccountStatus + ) = test { + val confirmationOption = createLinkInlineSignupConfirmationOption() + + actionTest( + attachNewCardToAccountResult = Result.failure(IllegalStateException("Should not be used!")), + accountStatus = accountStatus, + signInResult = Result.success(true), + confirmationOption = confirmationOption, + ) { launchAction -> + validateSkippedLaunchAction(confirmationOption, launchAction) + + assertThat(analyticsScenario.onLinkPopupSkippedCalls.awaitItem()).isNotNull() + } + } + + private fun testSuccessfulSignupWithNewCard( + saveOption: LinkInlineSignupConfirmationOption.PaymentMethodSaveOption, + expectedSetupForFutureUsage: ConfirmPaymentIntentParams.SetupFutureUsage?, + expectedShouldSave: Boolean, + ) { + val expectedCreateParams = PaymentMethodCreateParams.createCard( + CardParams( + number = "4242424242424242", + expMonth = 7, + expYear = 2025, + ) + ) + + val confirmationOption = createLinkInlineSignupConfirmationOption( + saveOption = saveOption, + ) + + actionTest( + attachNewCardToAccountResult = Result.success( + LinkPaymentDetails.New( + paymentDetails = ConsumerPaymentDetails.Card( + id = "pm_1", + last4 = "4242", + isDefault = false, + expiryYear = 2030, + expiryMonth = 4, + brand = CardBrand.Visa, + cvcCheck = CvcCheck.Pass, + billingAddress = null, + ), + paymentMethodCreateParams = expectedCreateParams, + originalParams = PaymentMethodCreateParamsFixtures.DEFAULT_CARD, + ) + ), + accountStatus = AccountStatus.Verified, + signInResult = Result.success(true), + confirmationOption = confirmationOption, + ) { launchAction -> + val attachNewCardToAccountCall = coordinatorScenario.attachNewCardToAccountCalls.awaitItem() + + assertThat(attachNewCardToAccountCall.configuration).isEqualTo(confirmationOption.linkConfiguration) + assertThat(attachNewCardToAccountCall.paymentMethodCreateParams) + .isEqualTo(confirmationOption.createParams) + + val nextConfirmationOption = launchAction.launcherArguments.nextConfirmationOption + + assertThat(nextConfirmationOption).isInstanceOf() + + val newConfirmationOption = nextConfirmationOption.asNew() + + assertThat(newConfirmationOption.createParams).isEqualTo(expectedCreateParams) + assertThat(newConfirmationOption.optionsParams).isEqualTo( + PaymentMethodOptionsParams.Card( + setupFutureUsage = expectedSetupForFutureUsage, + ) + ) + assertThat(newConfirmationOption.shouldSave).isEqualTo(expectedShouldSave) + + assertThat(launchAction.receivesResultInProcess).isTrue() + assertThat(launchAction.deferredIntentConfirmationType).isNull() + + assertThat(storeScenario.markAsUsedCalls.awaitItem()).isNotNull() + } + } + + private fun testSuccessfulSignupWithSavedLinkCard( + saveOption: LinkInlineSignupConfirmationOption.PaymentMethodSaveOption, + expectedSetupForFutureUsage: ConfirmPaymentIntentParams.SetupFutureUsage, + ) { + val confirmationOption = createLinkInlineSignupConfirmationOption( + saveOption = saveOption, + ) + + actionTest( + attachNewCardToAccountResult = Result.success( + LinkPaymentDetails.Saved( + paymentDetails = ConsumerPaymentDetails.Card( + id = "pm_1", + last4 = "4242", + isDefault = false, + expiryYear = 2030, + expiryMonth = 4, + brand = CardBrand.Visa, + cvcCheck = CvcCheck.Pass, + billingAddress = null, + ), + paymentMethodCreateParams = PaymentMethodCreateParamsFixtures.DEFAULT_CARD, + ) + ), + signInResult = Result.success(true), + accountStatus = AccountStatus.Verified, + confirmationOption = confirmationOption, + ) { launchAction -> + val attachNewCardToAccountCall = coordinatorScenario.attachNewCardToAccountCalls.awaitItem() + + assertThat(attachNewCardToAccountCall.configuration).isEqualTo(confirmationOption.linkConfiguration) + assertThat(attachNewCardToAccountCall.paymentMethodCreateParams) + .isEqualTo(confirmationOption.createParams) + + val nextConfirmationOption = launchAction.launcherArguments.nextConfirmationOption + + assertThat(nextConfirmationOption).isInstanceOf() + + val savedConfirmationOption = nextConfirmationOption.asSaved() + + assertThat(savedConfirmationOption.optionsParams).isEqualTo( + PaymentMethodOptionsParams.Card( + setupFutureUsage = expectedSetupForFutureUsage, + ) + ) + + val paymentMethod = savedConfirmationOption.paymentMethod + + assertThat(paymentMethod.id).isEqualTo("pm_1") + assertThat(paymentMethod.type).isEqualTo(PaymentMethod.Type.Card) + assertThat(paymentMethod.card?.last4).isEqualTo("4242") + assertThat(paymentMethod.card?.wallet).isEqualTo(Wallet.LinkWallet(dynamicLast4 = "4242")) + + assertThat(launchAction.receivesResultInProcess).isTrue() + assertThat(launchAction.deferredIntentConfirmationType).isNull() + + assertThat(storeScenario.markAsUsedCalls.awaitItem()).isNotNull() + } + } + + private fun actionTest( + attachNewCardToAccountResult: Result, + signInResult: Result, + accountStatus: AccountStatus, + confirmationOption: LinkInlineSignupConfirmationOption = createLinkInlineSignupConfirmationOption(), + test: suspend Scenario.( + action: ConfirmationDefinition.Action.Launch + ) -> Unit + ) = test( + attachNewCardToAccountResult = attachNewCardToAccountResult, + signInResult = signInResult, + initialAccountStatus = accountStatus, + accountStatusOnSignIn = AccountStatus.Verified, + ) { + val action = definition.action( + confirmationOption = confirmationOption, + confirmationParameters = CONFIRMATION_PARAMETERS, + ) + + val getAccountStatusFlowCall = coordinatorScenario.getAccountStatusFlowCalls.awaitItem() + + assertThat(getAccountStatusFlowCall.configuration).isEqualTo(confirmationOption.linkConfiguration) + + assertThat(action).isInstanceOf>() + + test(action.asLaunch()) + } + + private fun validateSkippedLaunchAction( + confirmationOption: LinkInlineSignupConfirmationOption, + launchAction: ConfirmationDefinition.Action.Launch + ) { + val nextConfirmationOption = launchAction.launcherArguments.nextConfirmationOption + + assertThat(nextConfirmationOption).isInstanceOf() + + val nextNewConfirmationOption = nextConfirmationOption.asNew() + + assertThat(nextNewConfirmationOption.createParams).isEqualTo(confirmationOption.createParams) + assertThat(nextNewConfirmationOption.optionsParams).isEqualTo(confirmationOption.optionsParams) + + assertThat(launchAction.receivesResultInProcess).isTrue() + assertThat(launchAction.deferredIntentConfirmationType).isNull() + } + + private fun test( + attachNewCardToAccountResult: Result = Result.success( + LinkPaymentDetails.New( + paymentDetails = ConsumerPaymentDetails.Card( + id = "pm_123", + last4 = "4242", + expiryYear = 2024, + expiryMonth = 4, + brand = CardBrand.DinersClub, + cvcCheck = CvcCheck.Fail, + isDefault = false, + billingAddress = ConsumerPaymentDetails.BillingAddress( + countryCode = CountryCode.US, + postalCode = "42424" + ) + ), + paymentMethodCreateParams = mock(), + originalParams = mock(), + ) + ), + signInResult: Result = Result.success(true), + initialAccountStatus: AccountStatus = AccountStatus.Verified, + accountStatusOnSignIn: AccountStatus = AccountStatus.Verified, + hasUsedLink: Boolean = false, + test: suspend Scenario.() -> Unit + ) = runTest { + RecordingLinkConfigurationCoordinator.test( + attachNewCardToAccountResult = attachNewCardToAccountResult, + signInResult = signInResult, + initialAccountStatus = initialAccountStatus, + accountStatusOnSignIn = accountStatusOnSignIn, + ) { + val coordinatorScenario = this + + RecordingLinkAnalyticsHelper.test { + val analyticsScenario = this + + RecordingOnLinkInlineResult.test { + val onResultScenario = this + + RecordingLinkStore.test(hasUsedLink) { + val linkStoreScenario = this + + test( + Scenario( + definition = createLinkInlineSignupConfirmationDefinition( + linkConfigurationCoordinator = coordinatorScenario.coordinator, + linkAnalyticsHelper = analyticsScenario.helper, + linkStore = linkStoreScenario.linkStore, + ), + coordinatorScenario = coordinatorScenario, + storeScenario = linkStoreScenario, + analyticsScenario = analyticsScenario, + onResultScenario = onResultScenario, + ) + ) + } + } + } + } + } + + private fun createLinkInlineSignupConfirmationDefinition( + linkConfigurationCoordinator: LinkConfigurationCoordinator = FakeLinkConfigurationCoordinator(), + linkAnalyticsHelper: LinkAnalyticsHelper = FakeLinkAnalyticsHelper(), + linkStore: LinkStore = RecordingLinkStore.noOp(), + ): LinkInlineSignupConfirmationDefinition { + return LinkInlineSignupConfirmationDefinition( + linkConfigurationCoordinator = linkConfigurationCoordinator, + linkAnalyticsHelper = linkAnalyticsHelper, + linkStore = linkStore, + ) + } + + private fun createLinkInlineSignupConfirmationOption( + createParams: PaymentMethodCreateParams = PaymentMethodCreateParamsFixtures.DEFAULT_CARD, + saveOption: LinkInlineSignupConfirmationOption.PaymentMethodSaveOption = + LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.NoRequest, + userInput: UserInput = UserInput.SignUp( + email = "email@email.com", + phone = "1234567890", + country = "CA", + name = "John Doe", + consentAction = SignUpConsentAction.Checkbox, + ) + ): LinkInlineSignupConfirmationOption { + return LinkInlineSignupConfirmationOption( + createParams = createParams, + optionsParams = null, + saveOption = saveOption, + linkConfiguration = LinkConfiguration( + stripeIntent = PaymentIntentFactory.create(), + merchantName = "Merchant Inc.", + merchantCountryCode = "CA", + customerInfo = LinkConfiguration.CustomerInfo( + name = "Jphn Doe", + email = "johndoe@email.com", + phone = "+1123456789", + billingCountryCode = "CA" + ), + shippingDetails = null, + passthroughModeEnabled = false, + flags = mapOf(), + cardBrandChoice = null, + useAttestationEndpointsForLink = false, + ), + userInput = userInput, + ) + } + + private class Scenario( + val definition: LinkInlineSignupConfirmationDefinition, + val coordinatorScenario: RecordingLinkConfigurationCoordinator.Scenario, + val storeScenario: RecordingLinkStore.Scenario, + val analyticsScenario: RecordingLinkAnalyticsHelper.Scenario, + val onResultScenario: RecordingOnLinkInlineResult.Scenario, + ) + + private class RecordingOnLinkInlineResult private constructor() { + data class OnResultCall( + val result: LinkInlineSignupConfirmationDefinition.Result, + ) + + class Scenario( + val onResult: (LinkInlineSignupConfirmationDefinition.Result) -> Unit, + val onResultCalls: ReceiveTurbine, + ) + + companion object { + suspend fun test( + test: suspend Scenario.() -> Unit + ) { + val onResultCalls = Turbine() + + test( + Scenario( + onResult = { + onResultCalls.add(OnResultCall(it)) + }, + onResultCalls = onResultCalls, + ) + ) + + onResultCalls.ensureAllEventsConsumed() + } + } + } + + private class RecordingLinkAnalyticsHelper private constructor() : FakeLinkAnalyticsHelper() { + private val onLinkPopupSkippedCalls = Turbine() + + override fun onLinkPopupSkipped() { + onLinkPopupSkippedCalls.add(Unit) + } + + class Scenario( + val helper: LinkAnalyticsHelper, + val onLinkPopupSkippedCalls: ReceiveTurbine, + ) + + companion object { + suspend fun test( + test: suspend Scenario.() -> Unit + ) { + val helper = RecordingLinkAnalyticsHelper() + + test( + Scenario( + helper = helper, + onLinkPopupSkippedCalls = helper.onLinkPopupSkippedCalls, + ) + ) + + helper.onLinkPopupSkippedCalls.ensureAllEventsConsumed() + } + } + } + + private class RecordingLinkConfigurationCoordinator private constructor( + private val attachNewCardToAccountResult: Result, + private val signInResult: Result, + initialAccountStatus: AccountStatus, + private val accountStatusOnSignIn: AccountStatus, + ) : LinkConfigurationCoordinator { + private val getAccountStatusFlowCalls = Turbine() + private val signInCalls = Turbine() + private val attachNewCardToAccountCalls = Turbine() + + private val accountStatusFlow = MutableStateFlow(initialAccountStatus) + + override val emailFlow: StateFlow + get() { + throw NotImplementedError() + } + + override fun getComponent(configuration: LinkConfiguration): LinkComponent { + throw NotImplementedError() + } + + override fun getAccountStatusFlow(configuration: LinkConfiguration): Flow { + getAccountStatusFlowCalls.add(GetAccountStatusFlowCall(configuration)) + + return accountStatusFlow + } + + override suspend fun signInWithUserInput( + configuration: LinkConfiguration, + userInput: UserInput, + ): Result { + signInCalls.add(SignInCall(configuration, userInput)) + + accountStatusFlow.value = accountStatusOnSignIn + + return signInResult + } + + override suspend fun attachNewCardToAccount( + configuration: LinkConfiguration, + paymentMethodCreateParams: PaymentMethodCreateParams, + ): Result { + attachNewCardToAccountCalls.add(AttachNewCardToAccountCall(configuration, paymentMethodCreateParams)) + + return attachNewCardToAccountResult + } + + override suspend fun logOut(configuration: LinkConfiguration): Result { + throw NotImplementedError() + } + + data class GetAccountStatusFlowCall( + val configuration: LinkConfiguration + ) + + data class SignInCall( + val configuration: LinkConfiguration, + val userInput: UserInput, + ) + + data class AttachNewCardToAccountCall( + val configuration: LinkConfiguration, + val paymentMethodCreateParams: PaymentMethodCreateParams, + ) + + class Scenario( + val coordinator: LinkConfigurationCoordinator, + val getAccountStatusFlowCalls: ReceiveTurbine, + val signInCalls: ReceiveTurbine, + val attachNewCardToAccountCalls: ReceiveTurbine, + ) + + companion object { + suspend fun test( + attachNewCardToAccountResult: Result, + signInResult: Result, + initialAccountStatus: AccountStatus, + accountStatusOnSignIn: AccountStatus, + test: suspend Scenario.() -> Unit, + ) { + val coordinator = RecordingLinkConfigurationCoordinator( + attachNewCardToAccountResult = attachNewCardToAccountResult, + signInResult = signInResult, + initialAccountStatus = initialAccountStatus, + accountStatusOnSignIn = accountStatusOnSignIn, + ) + + test( + Scenario( + coordinator = coordinator, + getAccountStatusFlowCalls = coordinator.getAccountStatusFlowCalls, + signInCalls = coordinator.signInCalls, + attachNewCardToAccountCalls = coordinator.attachNewCardToAccountCalls + ) + ) + + coordinator.getAccountStatusFlowCalls.ensureAllEventsConsumed() + coordinator.signInCalls.ensureAllEventsConsumed() + coordinator.attachNewCardToAccountCalls.ensureAllEventsConsumed() + } + } + } + + private companion object { + private val PAYMENT_INTENT = PaymentIntentFactory.create() + + private val CONFIRMATION_PARAMETERS = ConfirmationDefinition.Parameters( + initializationMode = PaymentElementLoader.InitializationMode.PaymentIntent( + clientSecret = "pi_123_secret_123", + ), + intent = PAYMENT_INTENT, + appearance = PaymentSheet.Appearance(), + shippingDetails = AddressDetails(), + ) + } +} diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/FormHelperTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/FormHelperTest.kt index ab95c0f8497..14ce5887034 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/FormHelperTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/FormHelperTest.kt @@ -313,7 +313,6 @@ internal class FormHelperTest { iconResource = R.drawable.stripe_ic_paymentsheet_pm_bancontact, lightThemeIconUrl = null, darkThemeIconUrl = null, - createdFromLink = false, ) ) } 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 18abaf6268c..5bb531d9724 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/LinkHandlerTest.kt @@ -1,38 +1,22 @@ package com.stripe.android.paymentsheet import androidx.lifecycle.SavedStateHandle -import app.cash.turbine.ReceiveTurbine -import app.cash.turbine.test import app.cash.turbine.turbineScope import com.google.common.truth.Truth.assertThat -import com.stripe.android.core.model.CountryCode -import com.stripe.android.isInstanceOf import com.stripe.android.link.LinkConfiguration import com.stripe.android.link.LinkConfigurationCoordinator import com.stripe.android.link.LinkPaymentDetails import com.stripe.android.link.TestFactory import com.stripe.android.link.account.LinkStore import com.stripe.android.link.analytics.LinkAnalyticsHelper -import com.stripe.android.link.injection.LinkAnalyticsComponent import com.stripe.android.link.model.AccountStatus import com.stripe.android.link.ui.inline.LinkSignupMode -import com.stripe.android.link.ui.inline.SignUpConsentAction -import com.stripe.android.link.ui.inline.UserInput -import com.stripe.android.model.CardBrand -import com.stripe.android.model.ConfirmPaymentIntentParams -import com.stripe.android.model.ConsumerPaymentDetails -import com.stripe.android.model.CvcCheck -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.paymentsheet.model.PaymentSelection import com.stripe.android.paymentsheet.state.LinkState import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel.Companion.SAVE_SELECTION import com.stripe.android.testing.PaymentIntentFactory import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -40,9 +24,6 @@ import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.stub -import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner @@ -73,401 +54,6 @@ class LinkHandlerTest { assertThat(handler.isLinkEnabled.first()).isTrue() assertThat(savedStateHandle.get(SAVE_SELECTION)).isNull() } - - @Test - fun `payWithLinkInline completes successfully for existing verified user in complete flow`() = runLinkInlineTest( - shouldCompleteLinkFlowValues = listOf(true), - ) { - val userInput = UserInput.SignIn("example@example.com") - - handler.setupLink( - state = createLinkState( - loginState = LinkState.LoginState.LoggedIn, - ) - ) - - handler.processingState.test { - accountStatusFlow.emit(AccountStatus.Verified) - ensureAllEventsConsumed() // Begin with no events. - testScope.launch { - handler.payWithLinkInline(linkInlineSelection(userInput), shouldCompleteLinkFlow) - } - assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.Started) - assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.PaymentDetailsCollected(cardSelection())) - verify(linkAnalyticsHelper).onLinkPopupSkipped() - verify(linkStore, never()).markLinkAsUsed() - } - - processingStateTurbine.cancelAndIgnoreRemainingEvents() // Validated above. - } - - @Test - fun `payWithLinkInline completes successfully for existing verified user in custom flow`() = runLinkInlineTest( - shouldCompleteLinkFlowValues = listOf(false), - ) { - val userInput = UserInput.SignIn("example@example.com") - - handler.setupLink( - state = createLinkState( - loginState = LinkState.LoginState.NeedsVerification, - ) - ) - - handler.processingState.test { - accountStatusFlow.emit(AccountStatus.Verified) - ensureAllEventsConsumed() // Begin with no events. - testScope.launch { - handler.payWithLinkInline(linkInlineSelection(userInput), shouldCompleteLinkFlow) - } - assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.Started) - assertThat(awaitItem()).isInstanceOf() - verify(linkStore).markLinkAsUsed() - } - - processingStateTurbine.cancelAndIgnoreRemainingEvents() // Validated above. - } - - @Test - fun `payWithLinkInline completes successfully for existing user in custom flow`() = runLinkInlineTest( - shouldCompleteLinkFlowValues = listOf(false), - ) { - val userInput = UserInput.SignIn("example@example.com") - - handler.setupLink( - state = createLinkState( - loginState = LinkState.LoginState.LoggedOut, - ) - ) - - handler.processingState.test { - accountStatusFlow.emit(AccountStatus.NeedsVerification) - ensureAllEventsConsumed() // Begin with no events. - testScope.launch { - handler.payWithLinkInline(linkInlineSelection(userInput), shouldCompleteLinkFlow) - } - assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.Started) - assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.PaymentDetailsCollected(cardSelection())) - verify(linkAnalyticsHelper).onLinkPopupSkipped() - verify(linkStore, never()).markLinkAsUsed() - } - - processingStateTurbine.cancelAndIgnoreRemainingEvents() // Validated above. - } - - @Test - fun `payWithLinkInline completes successfully for signedOut user in complete flow`() = runLinkInlineTest( - MutableSharedFlow(replay = 0), - shouldCompleteLinkFlowValues = listOf(true), - ) { - val userInput = UserInput.SignIn(email = "example@example.com") - - handler.setupLink( - state = createLinkState( - loginState = LinkState.LoginState.LoggedOut, - ) - ) - - handler.processingState.test { - ensureAllEventsConsumed() // Begin with no events. - whenever(linkConfigurationCoordinator.signInWithUserInput(any(), any())) - .thenReturn(Result.success(true)) - testScope.launch { - handler.payWithLinkInline(linkInlineSelection(userInput), shouldCompleteLinkFlow) - } - accountStatusFlow.emit(AccountStatus.SignedOut) - assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.Started) - accountStatusFlow.emit(AccountStatus.Verified) - assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.PaymentDetailsCollected(cardSelection())) - verify(linkAnalyticsHelper).onLinkPopupSkipped() - verify(linkStore, never()).markLinkAsUsed() - } - - processingStateTurbine.cancelAndIgnoreRemainingEvents() // Validated above. - } - - @Test - fun `payWithLinkInline collects payment details`() = runLinkInlineTest( - accountStatusFlow = MutableSharedFlow(replay = 0), - shouldCompleteLinkFlowValues = listOf(false), - ) { - val userInput = UserInput.SignIn(email = "example@example.com") - - handler.setupLink( - state = createLinkState( - loginState = LinkState.LoginState.LoggedOut, - ) - ) - - handler.processingState.test { - ensureAllEventsConsumed() // Begin with no events. - whenever(linkConfigurationCoordinator.signInWithUserInput(any(), any())) - .thenReturn(Result.success(true)) - testScope.launch { - handler.payWithLinkInline(linkInlineSelection(userInput), shouldCompleteLinkFlow) - } - accountStatusFlow.emit(AccountStatus.SignedOut) - assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.Started) - - accountStatusFlow.emit(AccountStatus.Verified) - assertThat(awaitItem()).isInstanceOf() - verify(linkStore).markLinkAsUsed() - } - - processingStateTurbine.cancelAndIgnoreRemainingEvents() // Validated above. - } - - @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 genericSelection = assertAndGetGenericSelection(awaitItem()) - - assertThat(genericSelection.customerRequestedSave).isEqualTo( - PaymentSelection.CustomerRequestedSave.RequestReuse - ) - - assertThat(genericSelection.createdFromLink).isTrue() - - cancelAndConsumeRemainingEvents() - } - - processingStateTurbine.cancelAndIgnoreRemainingEvents() - } - - @Test - fun `payWithLinkInline requests payment is not saved if selection doesn't request it`() = runLinkInlineTest( - accountStatusFlow = MutableSharedFlow(replay = 0), - shouldCompleteLinkFlowValues = listOf(false), - ) { - setupBasicLink() - - handler.processingState.test { - ensureAllEventsConsumed() - - payWithLinkInline( - customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestNoReuse - ) - - assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.Started) - accountStatusFlow.emit(AccountStatus.Verified) - - val genericSelection = assertAndGetGenericSelection(awaitItem()) - - assertThat(genericSelection.customerRequestedSave).isEqualTo( - PaymentSelection.CustomerRequestedSave.RequestNoReuse - ) - - assertThat(genericSelection.createdFromLink).isTrue() - - cancelAndConsumeRemainingEvents() - } - - processingStateTurbine.cancelAndIgnoreRemainingEvents() - } - - @Test - fun `payWithLinkInline collects payment details in passthrough mode`() = runLinkInlineTest( - accountStatusFlow = MutableSharedFlow(replay = 0), - shouldCompleteLinkFlowValues = listOf(false), - linkConfiguration = defaultLinkConfiguration().copy(passthroughModeEnabled = true), - attachNewCardToAccountResult = Result.success( - LinkPaymentDetails.Saved( - paymentDetails = ConsumerPaymentDetails.Passthrough( - id = "pm_123", - last4 = "4242" - ), - paymentMethodCreateParams = mock(), - ) - ), - ) { - val userInput = UserInput.SignIn(email = "example@example.com") - - handler.setupLink( - state = createLinkState( - loginState = LinkState.LoginState.LoggedOut, - ) - ) - - handler.processingState.test { - ensureAllEventsConsumed() // Begin with no events. - whenever(linkConfigurationCoordinator.signInWithUserInput(any(), any())) - .thenReturn(Result.success(true)) - testScope.launch { - handler.payWithLinkInline(linkInlineSelection(userInput), shouldCompleteLinkFlow) - } - accountStatusFlow.emit(AccountStatus.SignedOut) - assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.Started) - - accountStatusFlow.emit(AccountStatus.Verified) - assertThat(awaitItem()).isEqualTo( - LinkHandler.ProcessingState.PaymentDetailsCollected( - paymentSelection = PaymentSelection.Saved( - paymentMethod = PaymentMethod.Builder() - .setId("pm_123") - .setCode("card") - .setCard( - PaymentMethod.Card( - last4 = "4242", - wallet = Wallet.LinkWallet("4242") - ) - ) - .setType(PaymentMethod.Type.Card) - .build(), - walletType = PaymentSelection.Saved.WalletType.Link, - paymentMethodOptionsParams = PaymentMethodOptionsParams.Card( - setupFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.OffSession - ), - ), - ) - ) - } - - processingStateTurbine.cancelAndIgnoreRemainingEvents() // Validated above. - } - - @Test - fun `if lookup fails, payWithLinkInline emits new selection with details from link`() = runLinkInlineTest { - val userInput = UserInput.SignIn(email = "example@example.com") - - handler.setupLink( - state = createLinkState( - loginState = LinkState.LoginState.LoggedOut, - ) - ) - - handler.processingState.test { - accountStatusFlow.emit(AccountStatus.SignedOut) - ensureAllEventsConsumed() // Begin with no events. - whenever(linkConfigurationCoordinator.signInWithUserInput(any(), any())) - .thenReturn(Result.failure(IllegalStateException("Whoops"))) - testScope.launch { - handler.payWithLinkInline(linkInlineSelection(userInput), shouldCompleteLinkFlow) - } - assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.Started) - assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.PaymentDetailsCollected(cardSelection())) - } - - processingStateTurbine.cancelAndIgnoreRemainingEvents() // Validated above. - } - - @Test - fun `if sign up fails, payWithLinkInline emits event to pay without Link`() = runLinkInlineTest { - val userInput = UserInput.SignUp( - name = "John Doe", - email = "example@example.com", - phone = "+11234567890", - country = "US", - consentAction = SignUpConsentAction.Checkbox, - ) - - handler.setupLink( - state = createLinkState( - loginState = LinkState.LoginState.LoggedOut, - ) - ) - - handler.processingState.test { - accountStatusFlow.emit(AccountStatus.SignedOut) - ensureAllEventsConsumed() // Begin with no events. - whenever(linkConfigurationCoordinator.signInWithUserInput(any(), any())) - .thenReturn(Result.failure(IllegalStateException("Whoops"))) - testScope.launch { - handler.payWithLinkInline(linkInlineSelection(userInput), shouldCompleteLinkFlow) - } - assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.Started) - assertThat(awaitItem()).isEqualTo(LinkHandler.ProcessingState.PaymentDetailsCollected(cardSelection())) - } - - processingStateTurbine.cancelAndIgnoreRemainingEvents() // Validated above. - } - - private suspend fun LinkInlineTestData.setupBasicLink() { - handler.setupLink( - state = createLinkState( - loginState = LinkState.LoginState.LoggedIn, - ) - ) - - whenever(linkConfigurationCoordinator.signInWithUserInput(any(), any())) - .thenReturn(Result.success(true)) - } - - private suspend fun LinkInlineTestData.payWithLinkInline( - customerRequestedSave: PaymentSelection.CustomerRequestedSave - ) { - testScope.launch { - handler.payWithLinkInline( - linkInlineSelection( - input = UserInput.SignIn(email = "example@example.com"), - customerRequestedSave = customerRequestedSave - ), - shouldCompleteLinkFlow - ) - } - } -} - -// Used to run through both complete flow, and custom flow for link inline tests. -private fun runLinkInlineTest( - accountStatusFlow: MutableSharedFlow = MutableSharedFlow(replay = 1), - shouldCompleteLinkFlowValues: List = listOf(true, false), - linkConfiguration: LinkConfiguration = defaultLinkConfiguration( - linkFundingSources = listOf("card"), - ), - attachNewCardToAccountResult: Result = Result.success( - LinkPaymentDetails.New( - paymentDetails = ConsumerPaymentDetails.Card( - id = "pm_123", - last4 = "4242", - expiryYear = 2024, - expiryMonth = 4, - brand = CardBrand.DinersClub, - cvcCheck = CvcCheck.Fail, - isDefault = false, - billingAddress = ConsumerPaymentDetails.BillingAddress( - countryCode = CountryCode.US, - postalCode = "42424" - ) - ), - paymentMethodCreateParams = mock(), - originalParams = mock(), - ) - ), - testBlock: suspend LinkInlineTestData.() -> Unit, -) { - for (shouldCompleteLinkFlowValue in shouldCompleteLinkFlowValues) { - runLinkTest(accountStatusFlow, linkConfiguration, attachNewCardToAccountResult) { - with(LinkInlineTestData(shouldCompleteLinkFlowValue, this)) { - testBlock() - } - } - } -} - -private fun assertAndGetGenericSelection( - processingState: LinkHandler.ProcessingState -): PaymentSelection.New.GenericPaymentMethod { - assertThat(processingState).isInstanceOf() - - val paymentDetailsCollectedState = processingState as LinkHandler.ProcessingState.PaymentDetailsCollected - val selection = paymentDetailsCollectedState.paymentSelection - - assertThat(selection).isInstanceOf() - - return selection as PaymentSelection.New.GenericPaymentMethod } private fun runLinkTest( @@ -482,20 +68,10 @@ private fun runLinkTest( val linkStore = mock() val handler = LinkHandler( linkConfigurationCoordinator = linkConfigurationCoordinator, - savedStateHandle = savedStateHandle, - linkStore = linkStore, - linkAnalyticsComponentBuilder = mock().stub { - val component = object : LinkAnalyticsComponent { - override val linkAnalyticsHelper: LinkAnalyticsHelper = linkAnalyticsHelper - } - whenever(it.build()).thenReturn(component) - }, ) val testScope = this turbineScope { - val processingStateTurbine = handler.processingState.testIn(backgroundScope) - whenever(linkConfigurationCoordinator.getAccountStatusFlow(eq(linkConfiguration))).thenReturn(accountStatusFlow) whenever(linkConfigurationCoordinator.attachNewCardToAccount(eq(linkConfiguration), any())).thenReturn( attachNewCardToAccountResult @@ -510,12 +86,10 @@ private fun runLinkTest( savedStateHandle = savedStateHandle, configuration = linkConfiguration, accountStatusFlow = accountStatusFlow, - processingStateTurbine = processingStateTurbine, linkAnalyticsHelper = linkAnalyticsHelper, ) ) { testBlock() - processingStateTurbine.ensureAllEventsConsumed() } } } @@ -541,40 +115,6 @@ private fun defaultLinkConfiguration( ) } -private fun linkInlineSelection( - input: UserInput, - customerRequestedSave: PaymentSelection.CustomerRequestedSave = - PaymentSelection.CustomerRequestedSave.RequestReuse, -): PaymentSelection.New.LinkInline { - return PaymentSelection.New.LinkInline( - paymentMethodCreateParams = defaultCardParams(), - brand = CardBrand.Visa, - customerRequestedSave = customerRequestedSave, - input = input, - ) -} - -private fun cardSelection( - customerRequestedSave: PaymentSelection.CustomerRequestedSave = - PaymentSelection.CustomerRequestedSave.RequestReuse, -): PaymentSelection.New.Card { - return PaymentSelection.New.Card( - paymentMethodCreateParams = defaultCardParams(), - brand = CardBrand.Visa, - customerRequestedSave = customerRequestedSave, - ) -} - -private fun defaultCardParams(): PaymentMethodCreateParams { - return PaymentMethodCreateParams.create( - PaymentMethodCreateParams.Card( - number = "4242424242424242", - expiryMonth = 1, - expiryYear = 34, - ) - ) -} - private class LinkTestDataImpl( override val testScope: TestScope, override val handler: LinkHandler, @@ -583,7 +123,6 @@ private class LinkTestDataImpl( override val savedStateHandle: SavedStateHandle, override val configuration: LinkConfiguration, override val accountStatusFlow: MutableSharedFlow, - override val processingStateTurbine: ReceiveTurbine, override val linkAnalyticsHelper: LinkAnalyticsHelper, ) : LinkTestData @@ -595,11 +134,5 @@ private interface LinkTestData { val savedStateHandle: SavedStateHandle val configuration: LinkConfiguration val accountStatusFlow: MutableSharedFlow - val processingStateTurbine: ReceiveTurbine val linkAnalyticsHelper: LinkAnalyticsHelper } - -private class LinkInlineTestData( - val shouldCompleteLinkFlow: Boolean, - linkTestData: LinkTestData, -) : LinkTestData by linkTestData 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 77100b32bea..e832ae7c8e3 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentOptionsViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentOptionsViewModelTest.kt @@ -10,10 +10,10 @@ import com.stripe.android.core.strings.resolvableString import com.stripe.android.isInstanceOf import com.stripe.android.link.LinkConfigurationCoordinator import com.stripe.android.link.model.AccountStatus +import com.stripe.android.link.ui.inline.SignUpConsentAction 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.ConfirmPaymentIntentParams import com.stripe.android.model.PaymentIntentFixtures import com.stripe.android.model.PaymentMethod import com.stripe.android.model.PaymentMethodCreateParams @@ -21,7 +21,6 @@ import com.stripe.android.model.PaymentMethodCreateParamsFixtures import com.stripe.android.model.PaymentMethodCreateParamsFixtures.DEFAULT_CARD import com.stripe.android.model.PaymentMethodFixtures import com.stripe.android.model.PaymentMethodFixtures.toDisplayableSavedPaymentMethod -import com.stripe.android.model.PaymentMethodOptionsParams import com.stripe.android.paymentsheet.PaymentSheetFixtures.updateState import com.stripe.android.paymentsheet.analytics.EventReporter import com.stripe.android.paymentsheet.model.PaymentSelection @@ -56,7 +55,6 @@ import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import kotlin.test.Test import com.stripe.android.R as PaymentsCoreR -import com.stripe.android.paymentsheet.R as PaymentSheetR @RunWith(RobolectricTestRunner::class) internal class PaymentOptionsViewModelTest { @@ -725,27 +723,20 @@ internal class PaymentOptionsViewModelTest { runTest { val viewModel = createLinkViewModel() - viewModel.linkHandler.payWithLinkInline( - paymentSelection = createLinkInlinePaymentSelection( - customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestReuse, - input = UserInput.SignIn("email@email.com"), - ), - shouldCompleteLinkInlineFlow = false - ) - assertThat(viewModel.selection.value).isEqualTo( - PaymentSelection.New.GenericPaymentMethod( - paymentMethodCreateParams = LinkTestUtils.LINK_NEW_PAYMENT_DETAILS.paymentMethodCreateParams, - paymentMethodOptionsParams = PaymentMethodOptionsParams.Card( - setupFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.OffSession, - ), + PaymentSelection.New.LinkInline( + paymentMethodCreateParams = DEFAULT_CARD, + paymentMethodOptionsParams = null, paymentMethodExtraParams = null, + brand = CardBrand.Visa, customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestReuse, - iconResource = PaymentSheetR.drawable.stripe_ic_paymentsheet_link, - label = "···· 4242".resolvableString, - lightThemeIconUrl = null, - darkThemeIconUrl = null, - createdFromLink = true, + input = UserInput.SignUp( + name = "John Doe", + email = "email@email.com", + phone = "1234567890", + country = "CA", + consentAction = SignUpConsentAction.Checkbox, + ), ) ) } @@ -755,26 +746,21 @@ internal class PaymentOptionsViewModelTest { runTest { val viewModel = createLinkViewModel() - viewModel.linkHandler.payWithLinkInline( - paymentSelection = createLinkInlinePaymentSelection( - customerRequestedSave = PaymentSelection.CustomerRequestedSave.NoRequest, - input = UserInput.SignIn("email@email.com"), - ), - shouldCompleteLinkInlineFlow = false - ) - assertThat(viewModel.selection.value).isEqualTo( - PaymentSelection.New.GenericPaymentMethod( - paymentMethodCreateParams = LinkTestUtils.LINK_NEW_PAYMENT_DETAILS.paymentMethodCreateParams, - paymentMethodOptionsParams = PaymentMethodOptionsParams.Card(), + PaymentSelection.New.LinkInline( + paymentMethodCreateParams = DEFAULT_CARD, + paymentMethodOptionsParams = null, paymentMethodExtraParams = null, - customerRequestedSave = PaymentSelection.CustomerRequestedSave.NoRequest, - iconResource = PaymentSheetR.drawable.stripe_ic_paymentsheet_link, - label = "···· 4242".resolvableString, - lightThemeIconUrl = null, - darkThemeIconUrl = null, - createdFromLink = true, - ) + brand = CardBrand.Visa, + customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestReuse, + input = UserInput.SignUp( + name = "John Doe", + email = "email@email.com", + phone = "1234567890", + country = "CA", + consentAction = SignUpConsentAction.Checkbox, + ), + ), ) } @@ -816,18 +802,6 @@ internal class PaymentOptionsViewModelTest { ) } - private fun createLinkInlinePaymentSelection( - customerRequestedSave: PaymentSelection.CustomerRequestedSave, - input: UserInput, - ): PaymentSelection.New.LinkInline { - return PaymentSelection.New.LinkInline( - paymentMethodCreateParams = DEFAULT_CARD, - brand = CardBrand.Visa, - customerRequestedSave = customerRequestedSave, - input = input, - ) - } - 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/PaymentSheetActivityTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetActivityTest.kt index e34ba11405c..10abeb92ac6 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetActivityTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetActivityTest.kt @@ -1137,11 +1137,13 @@ internal class PaymentSheetActivityTest { args: PaymentSheetContractV2.Args = PaymentSheetFixtures.ARGS_CUSTOMER_WITH_GOOGLEPAY, cbcEligibility: CardBrandChoiceEligibility = CardBrandChoiceEligibility.Ineligible, ): PaymentSheetViewModel = runBlocking { + val coordinator = mock().stub { + onBlocking { getAccountStatusFlow(any()) }.thenReturn(flowOf(AccountStatus.SignedOut)) + on { emailFlow } doReturn stateFlowOf("email@email.com") + } + TestViewModelFactory.create( - linkConfigurationCoordinator = mock().stub { - onBlocking { getAccountStatusFlow(any()) }.thenReturn(flowOf(AccountStatus.SignedOut)) - on { emailFlow } doReturn stateFlowOf("email@email.com") - }, + linkConfigurationCoordinator = coordinator, ) { linkHandler, savedStateHandle -> PaymentSheetViewModel( args = args, @@ -1175,6 +1177,7 @@ internal class PaymentSheetActivityTest { statusBarColor = args.statusBarColor, linkLauncher = linkPaymentLauncher, errorReporter = FakeErrorReporter(), + linkConfigurationCoordinator = coordinator, cvcRecollectionLauncherFactory = RecordingCvcRecollectionLauncherFactory.noOp(), ), cardAccountRangeRepositoryFactory = NullCardAccountRangeRepositoryFactory, 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 e03f539579f..8f5ab6a3b48 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt @@ -897,14 +897,15 @@ internal class PaymentSheetViewModelTest { val viewModel = createLinkViewModel(intentConfirmationInterceptor) - viewModel.linkHandler.payWithLinkInline( - paymentSelection = createLinkInlinePaymentSelection( + viewModel.updateSelection( + createLinkInlinePaymentSelection( customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestReuse, input = UserInput.SignIn("email@email.com"), - ), - shouldCompleteLinkInlineFlow = false + ) ) + viewModel.checkout() + verify(intentConfirmationInterceptor).intercept( initializationMode = any(), paymentMethod = any(), @@ -924,14 +925,15 @@ internal class PaymentSheetViewModelTest { val viewModel = createLinkViewModel(intentConfirmationInterceptor) - viewModel.linkHandler.payWithLinkInline( - paymentSelection = createLinkInlinePaymentSelection( + viewModel.updateSelection( + createLinkInlinePaymentSelection( customerRequestedSave = PaymentSelection.CustomerRequestedSave.NoRequest, input = UserInput.SignIn("email@email.com"), - ), - shouldCompleteLinkInlineFlow = false + ) ) + viewModel.checkout() + verify(intentConfirmationInterceptor).intercept( initializationMode = any(), paymentMethod = any(), @@ -3157,6 +3159,7 @@ internal class PaymentSheetViewModelTest { statusBarColor = args.statusBarColor, errorReporter = FakeErrorReporter(), linkLauncher = linkPaymentLauncher, + linkConfigurationCoordinator = linkConfigurationCoordinator, cvcRecollectionLauncherFactory = RecordingCvcRecollectionLauncherFactory.noOp(), ), cardAccountRangeRepositoryFactory = NullCardAccountRangeRepositoryFactory, diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/TestViewModelFactory.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/TestViewModelFactory.kt index 28030015e94..fdf7bd2aecc 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/TestViewModelFactory.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/TestViewModelFactory.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.SavedStateHandle import com.stripe.android.link.LinkConfigurationCoordinator import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel import com.stripe.android.utils.FakeLinkConfigurationCoordinator -import org.mockito.kotlin.mock internal object TestViewModelFactory { fun create( @@ -17,9 +16,6 @@ internal object TestViewModelFactory { ): T { val linkHandler = LinkHandler( linkConfigurationCoordinator = linkConfigurationCoordinator, - savedStateHandle = savedStateHandle, - linkAnalyticsComponentBuilder = mock(), - linkStore = mock(), ) return viewModelFactory(linkHandler, savedStateHandle) } 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 8cd8a9e209d..fa9c6a2f3d4 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 @@ -13,7 +13,6 @@ import com.stripe.android.model.PaymentMethodFixtures import com.stripe.android.paymentsheet.ExperimentalCustomerSessionApi import com.stripe.android.paymentsheet.PaymentSheet import com.stripe.android.paymentsheet.PaymentSheetFixtures -import com.stripe.android.paymentsheet.R import com.stripe.android.paymentsheet.model.PaymentSelection import com.stripe.android.paymentsheet.state.PaymentElementLoader import org.junit.runner.RunWith @@ -692,48 +691,6 @@ class PaymentSheetEventTest { ) } - @Test - fun `Generic payment method event created from Link should return expected event`() { - val inlineLinkEvent = PaymentSheetEvent.Payment( - mode = EventReporter.Mode.Complete, - paymentSelection = PaymentSelection.New.GenericPaymentMethod( - paymentMethodCreateParams = PaymentMethodCreateParamsFixtures.DEFAULT_CARD, - paymentMethodOptionsParams = null, - paymentMethodExtraParams = null, - customerRequestedSave = PaymentSelection.CustomerRequestedSave.NoRequest, - label = resolvableString("**** 4444"), - iconResource = R.drawable.stripe_ic_paymentsheet_card_visa, - darkThemeIconUrl = null, - lightThemeIconUrl = null, - createdFromLink = true, - ), - duration = 1.milliseconds, - result = PaymentSheetEvent.Payment.Result.Success, - currency = "usd", - isDeferred = false, - linkEnabled = false, - googlePaySupported = false, - deferredIntentConfirmationType = null, - ) - assertThat( - inlineLinkEvent.eventName - ).isEqualTo( - "mc_complete_payment_link_success" - ) - assertThat( - inlineLinkEvent.params - ).isEqualTo( - mapOf( - "currency" to "usd", - "duration" to 0.001F, - "selected_lpm" to "card", - "is_decoupled" to false, - "link_enabled" to false, - "google_pay_enabled" to false, - ) - ) - } - @Test fun `External payment method event should return expected event`() { val newPMEvent = PaymentSheetEvent.Payment( 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 e7a89cd0ea8..ac469b46a46 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 @@ -91,6 +91,7 @@ import com.stripe.android.testing.CoroutineTestRule import com.stripe.android.testing.FakeErrorReporter import com.stripe.android.uicore.image.StripeImageLoader import com.stripe.android.utils.FakeIntentConfirmationInterceptor +import com.stripe.android.utils.FakeLinkConfigurationCoordinator import com.stripe.android.utils.FakePaymentElementLoader import com.stripe.android.utils.IntentConfirmationInterceptorTestRule import com.stripe.android.utils.RelayingPaymentElementLoader @@ -2343,6 +2344,7 @@ internal class DefaultFlowControllerTest { stripePaymentLauncherAssistedFactory = paymentLauncherAssistedFactory, cvcRecollectionLauncherFactory = cvcRecollectionLauncherFactory, paymentConfiguration = PaymentConfiguration.getInstance(context), + linkConfigurationCoordinator = FakeLinkConfigurationCoordinator(), linkLauncher = linkPaymentLauncher, errorReporter = errorReporter, savedStateHandle = viewModel.handle, diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/viewmodels/FakeBaseSheetViewModel.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/viewmodels/FakeBaseSheetViewModel.kt index 85cac5692e1..f9083c40911 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/viewmodels/FakeBaseSheetViewModel.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/viewmodels/FakeBaseSheetViewModel.kt @@ -22,14 +22,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import org.mockito.Mockito.mock -private fun linkHandler(savedStateHandle: SavedStateHandle): LinkHandler { +private fun linkHandler(): LinkHandler { return LinkHandler( linkConfigurationCoordinator = FakeLinkConfigurationCoordinator(), - savedStateHandle = savedStateHandle, - linkStore = mock(), - linkAnalyticsComponentBuilder = mock(), ) } @@ -53,7 +49,7 @@ internal class FakeBaseSheetViewModel private constructor( initialScreen: PaymentSheetScreen, ): FakeBaseSheetViewModel { val savedStateHandle = SavedStateHandle() - val linkHandler = linkHandler(savedStateHandle) + val linkHandler = linkHandler() return FakeBaseSheetViewModel(savedStateHandle, linkHandler, paymentMethodMetadata).apply { navigationHandler.transitionTo( initialScreen diff --git a/paymentsheet/src/test/java/com/stripe/android/utils/RecordingLinkStore.kt b/paymentsheet/src/test/java/com/stripe/android/utils/RecordingLinkStore.kt index 54543a1f558..5104c7613ae 100644 --- a/paymentsheet/src/test/java/com/stripe/android/utils/RecordingLinkStore.kt +++ b/paymentsheet/src/test/java/com/stripe/android/utils/RecordingLinkStore.kt @@ -11,13 +11,23 @@ internal object RecordingLinkStore { return mock() } - suspend fun test(test: suspend Scenario.() -> Unit) { + suspend fun test( + hasUsedLink: Boolean = false, + test: suspend Scenario.() -> Unit + ) { val markAsUsedCalls = Turbine() + val hasUsedLinkCalls = Turbine() val linkStore = mock { on { markLinkAsUsed() } doAnswer { markAsUsedCalls.add(Unit) } + + on { hasUsedLink() } doAnswer { + hasUsedLinkCalls.add(Unit) + + hasUsedLink + } } test(