diff --git a/paymentsheet/api/paymentsheet.api b/paymentsheet/api/paymentsheet.api index 3514ae79805..8778b9ed187 100644 --- a/paymentsheet/api/paymentsheet.api +++ b/paymentsheet/api/paymentsheet.api @@ -661,6 +661,22 @@ public final class com/stripe/android/paymentelement/confirmation/link/LinkConfi public synthetic fun newArray (I)[Ljava/lang/Object; } +public final class com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationDefinition$Result$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationDefinition$Result; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationDefinition$Result; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationOption$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationOption; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationOption; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + public final class com/stripe/android/paymentelement/embedded/DefaultEmbeddedConfigurationHandler$Arguments$Creator : android/os/Parcelable$Creator { public fun ()V public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/paymentelement/embedded/DefaultEmbeddedConfigurationHandler$Arguments; diff --git a/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/LinkTest.kt b/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/LinkTest.kt index 6ee42a94230..e1bd5fd1559 100644 --- a/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/LinkTest.kt +++ b/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/LinkTest.kt @@ -665,6 +665,13 @@ internal class LinkTest { response.testBodyFromFile("payment-intent-confirm.json") } + networkRule.enqueue( + method("POST"), + path("/v1/consumers/sessions/log_out"), + ) { response -> + response.testBodyFromFile("consumer-session-logout-success.json") + } + page.clickPrimaryButton() } @@ -721,6 +728,13 @@ internal class LinkTest { response.testBodyFromFile("payment-intent-confirm.json") } + networkRule.enqueue( + method("POST"), + path("/v1/consumers/sessions/log_out"), + ) { response -> + response.testBodyFromFile("consumer-session-logout-success.json") + } + page.clickPrimaryButton() } @@ -784,6 +798,13 @@ internal class LinkTest { response.testBodyFromFile("payment-intent-confirm.json") } + networkRule.enqueue( + method("POST"), + path("/v1/consumers/sessions/log_out"), + ) { response -> + response.testBodyFromFile("consumer-session-logout-success.json") + } + page.clickPrimaryButton() } @@ -832,6 +853,13 @@ internal class LinkTest { response.testBodyFromFile("payment-intent-confirm.json") } + networkRule.enqueue( + method("POST"), + path("/v1/consumers/sessions/log_out"), + ) { response -> + response.testBodyFromFile("consumer-session-logout-success.json") + } + page.clickPrimaryButton() } diff --git a/paymentsheet/src/main/java/com/stripe/android/link/account/DefaultLinkAccountManager.kt b/paymentsheet/src/main/java/com/stripe/android/link/account/DefaultLinkAccountManager.kt index a8779fe887a..d1d395f9ce6 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/account/DefaultLinkAccountManager.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/account/DefaultLinkAccountManager.kt @@ -81,16 +81,21 @@ internal class DefaultLinkAccountManager @Inject constructor( return runCatching { requireNotNull(linkAccount.value) }.mapCatching { account -> - linkRepository.logOut( - consumerSessionClientSecret = account.clientSecret, - consumerAccountPublishableKey = consumerPublishableKey, - ).getOrThrow() - }.onSuccess { - errorReporter.report(ErrorReporter.SuccessEvent.LINK_LOG_OUT_SUCCESS) - Logger.getInstance(BuildConfig.DEBUG).debug("Logged out of Link successfully") - }.onFailure { error -> - errorReporter.report(ErrorReporter.ExpectedErrorEvent.LINK_LOG_OUT_FAILURE, StripeException.create(error)) - Logger.getInstance(BuildConfig.DEBUG).warning("Failed to log out of Link: $error") + runCatching { + linkRepository.logOut( + consumerSessionClientSecret = account.clientSecret, + consumerAccountPublishableKey = consumerPublishableKey, + ).getOrThrow() + }.onSuccess { + errorReporter.report(ErrorReporter.SuccessEvent.LINK_LOG_OUT_SUCCESS) + Logger.getInstance(BuildConfig.DEBUG).debug("Logged out of Link successfully") + }.onFailure { error -> + errorReporter.report( + ErrorReporter.ExpectedErrorEvent.LINK_LOG_OUT_FAILURE, + StripeException.create(error) + ) + Logger.getInstance(BuildConfig.DEBUG).warning("Failed to log out of Link: $error") + }.getOrThrow() } } 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..4345e20da10 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( @@ -15,60 +16,106 @@ internal fun PaymentSelection.toConfirmationOption( linkConfiguration: LinkConfiguration?, ): ConfirmationHandler.Option? { return when (this) { - is PaymentSelection.Saved -> PaymentMethodConfirmationOption.Saved( - paymentMethod = paymentMethod, + is PaymentSelection.Saved -> toConfirmationOption() + is PaymentSelection.ExternalPaymentMethod -> toConfirmationOption() + is PaymentSelection.New.USBankAccount -> toConfirmationOption() + is PaymentSelection.New.LinkInline -> toConfirmationOption(linkConfiguration) + is PaymentSelection.New -> toConfirmationOption() + is PaymentSelection.GooglePay -> toConfirmationOption(configuration) + is PaymentSelection.Link -> toConfirmationOption(linkConfiguration) + } +} + +private fun PaymentSelection.Saved.toConfirmationOption(): PaymentMethodConfirmationOption.Saved { + return PaymentMethodConfirmationOption.Saved( + paymentMethod = paymentMethod, + optionsParams = paymentMethodOptionsParams, + ) +} + +private fun PaymentSelection.ExternalPaymentMethod.toConfirmationOption(): ExternalPaymentMethodConfirmationOption { + return ExternalPaymentMethodConfirmationOption( + type = type, + billingDetails = billingDetails, + ) +} + +private fun PaymentSelection.New.USBankAccount.toConfirmationOption(): PaymentMethodConfirmationOption { + return if (instantDebits != null) { + // For Instant Debits, we create the PaymentMethod inside the bank auth flow. Therefore, + // we can just use the already created object here. + PaymentMethodConfirmationOption.Saved( + paymentMethod = instantDebits.paymentMethod, optionsParams = paymentMethodOptionsParams, ) - is PaymentSelection.ExternalPaymentMethod -> ExternalPaymentMethodConfirmationOption( - type = type, - billingDetails = billingDetails, + } else { + PaymentMethodConfirmationOption.New( + createParams = paymentMethodCreateParams, + optionsParams = paymentMethodOptionsParams, + shouldSave = customerRequestedSave == PaymentSelection.CustomerRequestedSave.RequestReuse, ) - is PaymentSelection.New.USBankAccount -> { - if (instantDebits != null) { - // For Instant Debits, we create the PaymentMethod inside the bank auth flow. Therefore, - // we can just use the already created object here. - PaymentMethodConfirmationOption.Saved( - paymentMethod = instantDebits.paymentMethod, - optionsParams = paymentMethodOptionsParams, - ) - } else { - PaymentMethodConfirmationOption.New( - createParams = paymentMethodCreateParams, - optionsParams = paymentMethodOptionsParams, - shouldSave = customerRequestedSave == PaymentSelection.CustomerRequestedSave.RequestReuse, - ) - } - } - is PaymentSelection.New -> { - if (paymentMethodCreateParams.typeCode == PaymentMethod.Type.BacsDebit.code) { - BacsConfirmationOption( - createParams = paymentMethodCreateParams, - optionsParams = paymentMethodOptionsParams, - ) - } else { - PaymentMethodConfirmationOption.New( - createParams = paymentMethodCreateParams, - optionsParams = paymentMethodOptionsParams, - shouldSave = customerRequestedSave == PaymentSelection.CustomerRequestedSave.RequestReuse, - ) + } +} + +private fun PaymentSelection.New.LinkInline.toConfirmationOption( + linkConfiguration: LinkConfiguration? +): LinkInlineSignupConfirmationOption? { + return 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.GooglePay -> configuration.googlePay?.let { googlePay -> - GooglePayConfirmationOption( - config = GooglePayConfirmationOption.Config( - environment = googlePay.environment, - merchantName = configuration.merchantDisplayName, - merchantCountryCode = googlePay.countryCode, - merchantCurrencyCode = googlePay.currencyCode, - customAmount = googlePay.amount, - customLabel = googlePay.label, - billingDetailsCollectionConfiguration = configuration.billingDetailsCollectionConfiguration, - cardBrandFilter = PaymentSheetCardBrandFilter(configuration.cardBrandAcceptance) - ) + ) + } +} + +private fun PaymentSelection.New.toConfirmationOption(): ConfirmationHandler.Option { + return if (paymentMethodCreateParams.typeCode == PaymentMethod.Type.BacsDebit.code) { + BacsConfirmationOption( + createParams = paymentMethodCreateParams, + optionsParams = paymentMethodOptionsParams, + ) + } else { + PaymentMethodConfirmationOption.New( + createParams = paymentMethodCreateParams, + optionsParams = paymentMethodOptionsParams, + shouldSave = customerRequestedSave == PaymentSelection.CustomerRequestedSave.RequestReuse, + ) + } +} + +private fun PaymentSelection.GooglePay.toConfirmationOption( + configuration: CommonConfiguration, +): GooglePayConfirmationOption? { + return configuration.googlePay?.let { googlePay -> + GooglePayConfirmationOption( + config = GooglePayConfirmationOption.Config( + environment = googlePay.environment, + merchantName = configuration.merchantDisplayName, + merchantCountryCode = googlePay.countryCode, + merchantCurrencyCode = googlePay.currencyCode, + customAmount = googlePay.amount, + customLabel = googlePay.label, + billingDetailsCollectionConfiguration = configuration.billingDetailsCollectionConfiguration, + cardBrandFilter = PaymentSheetCardBrandFilter(configuration.cardBrandAcceptance) ) - } - is PaymentSelection.Link -> linkConfiguration?.let { - LinkConfirmationOption(configuration = linkConfiguration) - } + ) + } +} + +private fun PaymentSelection.Link.toConfirmationOption( + linkConfiguration: LinkConfiguration? +): LinkConfirmationOption? { + return linkConfiguration?.let { + LinkConfirmationOption(configuration = linkConfiguration) } } 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..c7c51c05449 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationDefinition.kt @@ -0,0 +1,204 @@ +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.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.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, userInput) + 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, + userInput: UserInput, + ): PaymentMethodConfirmationOption { + if (userInput is UserInput.SignIn) { + linkAnalyticsHelper.onLinkPopupSkipped() + + return linkInlineSignupConfirmationOption.toNewOption() + } + + 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..8dc7f703d53 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,27 @@ 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 +import javax.inject.Singleton +@Singleton 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 +30,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..6ca86068228 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,15 +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 - linkHandler.setupLink(args.state.paymentMethodMetadata.linkState) // After recovering from don't keep activities the paymentMethodMetadata will be saved, @@ -160,21 +149,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 +188,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/PaymentSheet.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheet.kt index 5ea61584439..24f9e840fb2 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheet.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheet.kt @@ -2159,9 +2159,6 @@ class PaymentSheet internal constructor( } companion object { - - internal var linkHandler: LinkHandler? = null - /** * Create a [FlowController] that you configure with a client secret by calling * [configureWithPaymentIntent] or [configureWithSetupIntent]. 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/flowcontroller/DefaultFlowController.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowController.kt index 7838ec5b9f8..d9c178ffac4 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowController.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowController.kt @@ -25,6 +25,7 @@ import com.stripe.android.payments.core.injection.PRODUCT_USAGE import com.stripe.android.payments.paymentlauncher.PaymentResult import com.stripe.android.paymentsheet.ExternalPaymentMethodInterceptor import com.stripe.android.paymentsheet.InitializedViaCompose +import com.stripe.android.paymentsheet.LinkHandler import com.stripe.android.paymentsheet.PaymentOptionCallback import com.stripe.android.paymentsheet.PaymentOptionContract import com.stripe.android.paymentsheet.PaymentOptionResult @@ -47,8 +48,6 @@ import com.stripe.android.paymentsheet.ui.SepaMandateResult import com.stripe.android.paymentsheet.utils.canSave import com.stripe.android.uicore.utils.AnimationConstants import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @@ -70,6 +69,7 @@ internal class DefaultFlowController @Inject internal constructor( private val eventReporter: EventReporter, private val viewModel: FlowControllerViewModel, private val confirmationHandler: ConfirmationHandler, + private val linkHandler: LinkHandler, @Named(ENABLE_LOGGING) private val enableLogging: Boolean, @Named(PRODUCT_USAGE) private val productUsage: Set, private val configurationHandler: FlowControllerConfigurationHandler, @@ -123,7 +123,6 @@ internal class DefaultFlowController @Inject internal constructor( object : DefaultLifecycleObserver { override fun onDestroy(owner: LifecycleOwner) { activityResultLaunchers.forEach { it.unregister() } - PaymentSheet.FlowController.linkHandler = null IntentConfirmationInterceptor.createIntentCallback = null ExternalPaymentMethodInterceptor.externalPaymentMethodConfirmHandler = null } @@ -483,7 +482,6 @@ internal class DefaultFlowController @Inject internal constructor( } } - @OptIn(DelicateCoroutinesApi::class) internal fun onPaymentResult( paymentResult: PaymentResult, deferredIntentConfirmationType: DeferredIntentConfirmationType? = null, @@ -496,10 +494,7 @@ internal class DefaultFlowController @Inject internal constructor( val selection = viewModel.paymentSelection if (paymentResult is PaymentResult.Completed && selection != null && selection.isLink) { - GlobalScope.launch { - // This usage is intentional. We want the request to be sent without regard for the UI lifecycle. - PaymentSheet.FlowController.linkHandler?.logOut() - } + linkHandler.logOut() } viewModelScope.launch { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerStateComponent.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerStateComponent.kt index b9c83d737d5..ec37bc9da90 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerStateComponent.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerStateComponent.kt @@ -8,6 +8,7 @@ import com.stripe.android.paymentelement.confirmation.ConfirmationHandler import com.stripe.android.paymentelement.confirmation.injection.ExtendedPaymentElementConfirmationModule import com.stripe.android.payments.core.injection.STATUS_BAR_COLOR import com.stripe.android.payments.core.injection.StripeRepositoryModule +import com.stripe.android.paymentsheet.LinkHandler import com.stripe.android.paymentsheet.PaymentOptionsViewModel import com.stripe.android.paymentsheet.injection.PaymentSheetCommonModule import com.stripe.android.ui.core.forms.resources.injection.ResourceRepositoryModule @@ -32,6 +33,7 @@ import javax.inject.Singleton internal interface FlowControllerStateComponent { val flowControllerComponentBuilder: FlowControllerComponent.Builder val confirmationHandler: ConfirmationHandler + val linkHandler: LinkHandler fun inject(paymentOptionsViewModel: PaymentOptionsViewModel.Factory) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerViewModel.kt index dbf130dafd9..cd89c71ec05 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerViewModel.kt @@ -7,10 +7,13 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras import com.stripe.android.analytics.SessionSavedStateHandler import com.stripe.android.core.utils.requireApplication import com.stripe.android.paymentsheet.model.PaymentSelection +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch internal class FlowControllerViewModel( application: Application, @@ -39,8 +42,19 @@ internal class FlowControllerViewModel( handle[STATE_KEY] = value } + private val flowControllerStateFlow = handle.getStateFlow(STATE_KEY, null) private val restartSession = SessionSavedStateHandler.attachTo(this, handle) + init { + viewModelScope.launch { + flowControllerStateFlow.collectLatest { state -> + flowControllerStateComponent.linkHandler.setupLink( + state = state?.paymentSheetState?.paymentMethodMetadata?.linkState + ) + } + } + } + fun resetSession() { restartSession() } 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..b5f0f829984 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,13 @@ 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 +266,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 +348,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 +425,24 @@ 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, + initializationMode = PaymentElementLoader.InitializationMode.PaymentIntent( + clientSecret = "pi_123_secret_123", + ), ) - - 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 96925d070c5..98a42ac7201 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.link.gate.DefaultLinkGate import com.stripe.android.link.gate.LinkGate import com.stripe.android.networking.StripeApiRepository @@ -36,6 +37,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 @@ -164,5 +166,10 @@ internal interface ExtendedPaymentElementConfirmationTestModule { @Provides @Named(STRIPE_ACCOUNT_ID) fun providesStripeAccountId(config: PaymentConfiguration): () -> String? = { config.stripeAccountId } + + @Provides + @Singleton + 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 0e9343b554b..46716f41a1e 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.link.gate.DefaultLinkGate import com.stripe.android.link.gate.LinkGate import com.stripe.android.networking.StripeApiRepository @@ -36,6 +37,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 @@ -164,5 +166,10 @@ internal interface PaymentElementConfirmationTestModule { @Provides @Named(STRIPE_ACCOUNT_ID) fun providesStripeAccountId(config: PaymentConfiguration): () -> String? = { config.stripeAccountId } + + @Provides + @Singleton + 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..7d5698f44a3 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationDefinitionTest.kt @@ -0,0 +1,850 @@ +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 input is sign in`() = test( + initialAccountStatus = AccountStatus.Verified, + ) { + val confirmationOption = createLinkInlineSignupConfirmationOption() + + val action = definition.action( + confirmationOption = confirmationOption.copy( + userInput = UserInput.SignIn(email = "email@email.com"), + ), + confirmationParameters = CONFIRMATION_PARAMETERS, + ) + + val getAccountStatusFlowCall = coordinatorScenario.getAccountStatusFlowCalls.awaitItem() + + assertThat(getAccountStatusFlowCall.configuration).isEqualTo(confirmationOption.linkConfiguration) + + assertThat(action).isInstanceOf>() + + val launchAction = action.asLaunch() + + validateSkippedLaunchAction(confirmationOption, launchAction) + + assertThat(analyticsScenario.onLinkPopupSkippedCalls.awaitItem()).isNotNull() + } + + @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, + initializationMode = PaymentElementLoader.InitializationMode.PaymentIntent( + clientSecret = "pi_123_secret_123", + ), + ), + 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..7901b40d715 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentOptionsViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentOptionsViewModelTest.kt @@ -9,11 +9,14 @@ import com.stripe.android.common.model.asCommonConfiguration import com.stripe.android.core.strings.resolvableString import com.stripe.android.isInstanceOf import com.stripe.android.link.LinkConfigurationCoordinator +import com.stripe.android.link.TestFactory import com.stripe.android.link.model.AccountStatus +import com.stripe.android.link.ui.inline.InlineSignupViewState +import com.stripe.android.link.ui.inline.LinkSignupMode +import com.stripe.android.link.ui.inline.SignUpConsentAction 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,9 +24,9 @@ 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.forms.FormFieldValues import com.stripe.android.paymentsheet.model.PaymentSelection import com.stripe.android.paymentsheet.navigation.PaymentSheetScreen import com.stripe.android.paymentsheet.navigation.PaymentSheetScreen.AddFirstPaymentMethod @@ -37,6 +40,8 @@ import com.stripe.android.paymentsheet.ui.UpdatePaymentMethodInteractor import com.stripe.android.paymentsheet.utils.LinkTestUtils import com.stripe.android.testing.PaymentIntentFactory import com.stripe.android.testing.PaymentMethodFactory +import com.stripe.android.uicore.elements.IdentifierSpec +import com.stripe.android.uicore.forms.FormFieldEntry import com.stripe.android.utils.FakeLinkConfigurationCoordinator import com.stripe.android.utils.NullCardAccountRangeRepositoryFactory import kotlinx.coroutines.Dispatchers @@ -56,7 +61,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 { @@ -722,61 +726,73 @@ internal class PaymentOptionsViewModelTest { @Test fun `On link selection with save requested, selection should be updated with saveable link selection`() = - runTest { - val viewModel = createLinkViewModel() + linkInlineSelectionTest( + expectedCustomerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestReuse, + ) - viewModel.linkHandler.payWithLinkInline( - paymentSelection = createLinkInlinePaymentSelection( - customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestReuse, - input = UserInput.SignIn("email@email.com"), - ), - shouldCompleteLinkInlineFlow = false - ) + @Test + fun `On link selection with save not requested, selection should be updated with unsaveable link selection`() = + linkInlineSelectionTest( + expectedCustomerRequestedSave = PaymentSelection.CustomerRequestedSave.NoRequest, + ) - assertThat(viewModel.selection.value).isEqualTo( - PaymentSelection.New.GenericPaymentMethod( - paymentMethodCreateParams = LinkTestUtils.LINK_NEW_PAYMENT_DETAILS.paymentMethodCreateParams, - paymentMethodOptionsParams = PaymentMethodOptionsParams.Card( - setupFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.OffSession, + private fun linkInlineSelectionTest( + expectedCustomerRequestedSave: PaymentSelection.CustomerRequestedSave + ) = runTest { + Dispatchers.setMain(testDispatcher) + + val viewModel = createLinkViewModel() + + val linkInlineHandler = LinkInlineHandler.create() + val formHelper = DefaultFormHelper.create( + viewModel = viewModel, + paymentMethodMetadata = requireNotNull(viewModel.paymentMethodMetadata.value), + linkInlineHandler = linkInlineHandler, + ) + + viewModel.selection.test { + assertThat(awaitItem()).isNull() + + formHelper.onFormFieldValuesChanged( + formValues = FormFieldValues( + fieldValuePairs = mapOf( + IdentifierSpec.CardBrand to FormFieldEntry(CardBrand.Visa.code, true), ), - paymentMethodExtraParams = null, - customerRequestedSave = PaymentSelection.CustomerRequestedSave.RequestReuse, - iconResource = PaymentSheetR.drawable.stripe_ic_paymentsheet_link, - label = "···· 4242".resolvableString, - lightThemeIconUrl = null, - darkThemeIconUrl = null, - createdFromLink = true, - ) + userRequestedReuse = expectedCustomerRequestedSave, + ), + selectedPaymentMethodCode = "card", ) - } - @Test - fun `On link selection with save not requested, selection should be updated with unsaveable link selection`() = - runTest { - val viewModel = createLinkViewModel() + assertThat(awaitItem()).isInstanceOf() - viewModel.linkHandler.payWithLinkInline( - paymentSelection = createLinkInlinePaymentSelection( - customerRequestedSave = PaymentSelection.CustomerRequestedSave.NoRequest, - input = UserInput.SignIn("email@email.com"), - ), - shouldCompleteLinkInlineFlow = false + val input = UserInput.SignUp( + name = "John Doe", + email = "johndoe@email.com", + phone = "+15555555555", + consentAction = SignUpConsentAction.CheckboxWithPrefilledEmailAndPhone, + country = "US", ) - assertThat(viewModel.selection.value).isEqualTo( - PaymentSelection.New.GenericPaymentMethod( - paymentMethodCreateParams = LinkTestUtils.LINK_NEW_PAYMENT_DETAILS.paymentMethodCreateParams, - paymentMethodOptionsParams = PaymentMethodOptionsParams.Card(), - paymentMethodExtraParams = null, - customerRequestedSave = PaymentSelection.CustomerRequestedSave.NoRequest, - iconResource = PaymentSheetR.drawable.stripe_ic_paymentsheet_link, - label = "···· 4242".resolvableString, - lightThemeIconUrl = null, - darkThemeIconUrl = null, - createdFromLink = true, + linkInlineHandler.onStateUpdated( + InlineSignupViewState.create( + signupMode = LinkSignupMode.AlongsideSaveForFutureUse, + config = TestFactory.LINK_CONFIGURATION, + ).copy( + userInput = input, ) ) + + val selection = awaitItem() + + assertThat(selection).isInstanceOf() + + val inlineSelection = selection as PaymentSelection.New.LinkInline + + assertThat(inlineSelection.input).isEqualTo(input) + assertThat(inlineSelection.brand).isEqualTo(CardBrand.Visa) + assertThat(inlineSelection.customerRequestedSave).isEqualTo(expectedCustomerRequestedSave) } + } private fun createLinkViewModel(): PaymentOptionsViewModel { val linkConfigurationCoordinator = FakeLinkConfigurationCoordinator( @@ -787,7 +803,7 @@ internal class PaymentOptionsViewModelTest { return createViewModel( linkState = LinkState( configuration = LinkTestUtils.createLinkConfiguration(), - signupMode = null, + signupMode = LinkSignupMode.AlongsideSaveForFutureUse, loginState = LinkState.LoginState.LoggedOut ), linkConfigurationCoordinator = linkConfigurationCoordinator, @@ -816,18 +832,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 71d38e24c1f..3f2f1902b7f 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,21 @@ 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 + input = UserInput.SignUp( + email = "email@email.com", + phone = "+12267007611", + country = "CA", + name = "John Doe", + consentAction = SignUpConsentAction.Checkbox, + ), + ) ) + viewModel.checkout() + verify(intentConfirmationInterceptor).intercept( initializationMode = any(), paymentMethod = any(), @@ -924,14 +931,21 @@ 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 + input = UserInput.SignUp( + email = "email@email.com", + phone = "+12267007611", + country = "CA", + name = "John Doe", + consentAction = SignUpConsentAction.Checkbox, + ), + ) ) + viewModel.checkout() + verify(intentConfirmationInterceptor).intercept( initializationMode = any(), paymentMethod = any(), @@ -3158,6 +3172,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..dd7ac9c5f97 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 @@ -21,6 +21,7 @@ import com.stripe.android.googlepaylauncher.GooglePayPaymentMethodLauncher import com.stripe.android.googlepaylauncher.GooglePayPaymentMethodLauncherContractV2 import com.stripe.android.link.LinkActivityContract import com.stripe.android.link.LinkPaymentLauncher +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 @@ -91,6 +92,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 @@ -2297,7 +2299,7 @@ internal class DefaultFlowControllerTest { bacsMandateConfirmationLauncherFactory, cvcRecollectionLauncherFactory, errorReporter, - eventReporter + eventReporter, ) } @@ -2336,6 +2338,7 @@ internal class DefaultFlowControllerTest { ), errorReporter = errorReporter, initializedViaCompose = false, + linkHandler = mock(), confirmationHandler = createTestConfirmationHandlerFactory( bacsMandateConfirmationLauncherFactory = bacsMandateConfirmationLauncherFactory, googlePayPaymentMethodLauncherFactory = googlePayPaymentMethodLauncherFactory, @@ -2343,6 +2346,9 @@ internal class DefaultFlowControllerTest { stripePaymentLauncherAssistedFactory = paymentLauncherAssistedFactory, cvcRecollectionLauncherFactory = cvcRecollectionLauncherFactory, paymentConfiguration = PaymentConfiguration.getInstance(context), + linkConfigurationCoordinator = FakeLinkConfigurationCoordinator( + accountStatus = AccountStatus.Verified, + ), 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 4a18a74ca18..73843ffec12 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,11 @@ 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 +import org.mockito.kotlin.mock -private fun linkHandler(savedStateHandle: SavedStateHandle): LinkHandler { +private fun linkHandler(): LinkHandler { return LinkHandler( linkConfigurationCoordinator = FakeLinkConfigurationCoordinator(), - savedStateHandle = savedStateHandle, - linkStore = mock(), - linkAnalyticsComponentBuilder = mock(), ) } @@ -54,7 +51,7 @@ internal class FakeBaseSheetViewModel private constructor( canGoBack: Boolean, ): FakeBaseSheetViewModel { val savedStateHandle = SavedStateHandle() - val linkHandler = linkHandler(savedStateHandle) + val linkHandler = linkHandler() return FakeBaseSheetViewModel(savedStateHandle, linkHandler, paymentMethodMetadata).apply { if (canGoBack) { navigationHandler.resetTo( 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(