diff --git a/paymentsheet/res/values/themes.xml b/paymentsheet/res/values/themes.xml index b922dae5a5c..054956a56c8 100644 --- a/paymentsheet/res/values/themes.xml +++ b/paymentsheet/res/values/themes.xml @@ -44,4 +44,10 @@ @color/stripe_link_window_background true + + diff --git a/paymentsheet/src/main/AndroidManifest.xml b/paymentsheet/src/main/AndroidManifest.xml index 6fc8699d491..55d7e7ea232 100644 --- a/paymentsheet/src/main/AndroidManifest.xml +++ b/paymentsheet/src/main/AndroidManifest.xml @@ -53,6 +53,15 @@ android:autoRemoveFromRecents="true" android:configChanges="orientation|keyboard|keyboardHidden|screenLayout|screenSize|smallestScreenSize" /> + + + fun getLinkAccountFlow(configuration: LinkConfiguration): StateFlow + suspend fun signInWithUserInput( configuration: LinkConfiguration, userInput: UserInput @@ -62,6 +65,10 @@ internal class RealLinkConfigurationCoordinator @Inject internal constructor( override fun getAccountStatusFlow(configuration: LinkConfiguration): Flow = getLinkPaymentLauncherComponent(configuration).linkAccountManager.accountStatus + override fun getLinkAccountFlow(configuration: LinkConfiguration): StateFlow { + return getLinkPaymentLauncherComponent(configuration).linkAccountManager.linkAccount + } + /** * Trigger Link sign in with the input collected from the user inline in PaymentSheet, whether * it's a new or existing account. diff --git a/paymentsheet/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt b/paymentsheet/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt index 63f8840547a..45ecefc268f 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt @@ -6,6 +6,7 @@ import androidx.activity.result.ActivityResultRegistry import com.stripe.android.link.LinkActivityResult.PaymentMethodObtained import com.stripe.android.link.account.LinkStore import com.stripe.android.link.injection.LinkAnalyticsComponent +import com.stripe.android.link.model.LinkAccount import javax.inject.Inject import javax.inject.Singleton @@ -72,9 +73,11 @@ internal class LinkPaymentLauncher @Inject internal constructor( */ fun present( configuration: LinkConfiguration, + linkAccount: LinkAccount? ) { val args = LinkActivityContract.Args( - configuration, + configuration = configuration, + linkAccount = linkAccount ) linkActivityResultLauncher?.launch(args) analyticsHelper.onLinkLaunched() diff --git a/paymentsheet/src/main/java/com/stripe/android/link/NativeLinkActivityContract.kt b/paymentsheet/src/main/java/com/stripe/android/link/NativeLinkActivityContract.kt index 22285fa7bdf..c7cac32ee81 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/NativeLinkActivityContract.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/NativeLinkActivityContract.kt @@ -20,7 +20,8 @@ internal class NativeLinkActivityContract @Inject constructor() : args = NativeLinkArgs( configuration = input.configuration, stripeAccountId = paymentConfiguration.stripeAccountId, - publishableKey = paymentConfiguration.publishableKey + publishableKey = paymentConfiguration.publishableKey, + linkAccount = input.linkAccount ) ) } diff --git a/paymentsheet/src/main/java/com/stripe/android/link/NativeLinkArgs.kt b/paymentsheet/src/main/java/com/stripe/android/link/NativeLinkArgs.kt index c4416cbf717..d7bcf8426b3 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/NativeLinkArgs.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/NativeLinkArgs.kt @@ -1,11 +1,13 @@ package com.stripe.android.link import android.os.Parcelable +import com.stripe.android.link.model.LinkAccount import kotlinx.parcelize.Parcelize @Parcelize internal data class NativeLinkArgs( val configuration: LinkConfiguration, val publishableKey: String, - val stripeAccountId: String? + val stripeAccountId: String?, + val linkAccount: LinkAccount? ) : Parcelable 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..9478a36e30d 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 @@ -207,6 +207,10 @@ internal class DefaultLinkAccountManager @Inject constructor( return newAccount } + internal fun setAccount(linkAccount: LinkAccount?) { + _linkAccount.value = linkAccount + } + override fun setLinkAccountFromLookupResult( lookup: ConsumerSessionLookup, startSession: Boolean, diff --git a/paymentsheet/src/main/java/com/stripe/android/link/express/LinkExpressActivity.kt b/paymentsheet/src/main/java/com/stripe/android/link/express/LinkExpressActivity.kt new file mode 100644 index 00000000000..ee5dfadfef0 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/link/express/LinkExpressActivity.kt @@ -0,0 +1,92 @@ +package com.stripe.android.link.express + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.annotation.VisibleForTesting +import androidx.compose.ui.window.Dialog +import androidx.core.os.bundleOf +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.NavHostController +import com.stripe.android.core.Logger +import com.stripe.android.link.NoArgsException +import com.stripe.android.link.theme.DefaultLinkTheme +import com.stripe.android.link.ui.verification.VerificationScreen +import com.stripe.android.link.ui.verification.VerificationViewModel +import com.stripe.android.paymentsheet.BuildConfig + +class LinkExpressActivity : ComponentActivity() { + internal var viewModel: VerificationViewModel? = null + + @VisibleForTesting + internal lateinit var navController: NavHostController + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + try { + viewModel = ViewModelProvider( + owner = this, + factory = VerificationViewModel.factory( + onVerificationSucceeded = { linkAccount -> + dismissWithResult(LinkExpressResult.Authenticated(linkAccount)) + }, + onChangeEmailClicked = {}, + onDismissClicked = { + dismissWithResult(LinkExpressResult.Canceled) + } + ) + )[VerificationViewModel::class.java] + } catch (e: NoArgsException) { + Logger.getInstance(BuildConfig.DEBUG).error("Failed to create VerificationViewModel", e) + setResult(Activity.RESULT_CANCELED) + finish() + } + + val vm = viewModel ?: return + + setContent { + Dialog( + onDismissRequest = { + dismissWithResult(LinkExpressResult.Canceled) + } + ) { + DefaultLinkTheme { + VerificationScreen(vm) + } + } + } + } + + private fun dismissWithResult(result: LinkExpressResult) { + val bundle = bundleOf( + LinkExpressContract.EXTRA_RESULT to result + ) + this@LinkExpressActivity.setResult( + RESULT_COMPLETE, + intent.putExtras(bundle) + ) + this@LinkExpressActivity.finish() + } + + companion object { + internal const val EXTRA_ARGS = "link_express_args" + internal const val RESULT_COMPLETE = 57576 + + internal fun createIntent( + context: Context, + args: LinkExpressArgs + ): Intent { + return Intent(context, LinkExpressActivity::class.java) + .putExtra(EXTRA_ARGS, args) + } + + internal fun getArgs(savedStateHandle: SavedStateHandle): LinkExpressArgs? { + return savedStateHandle.get(EXTRA_ARGS) + } + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/link/express/LinkExpressArgs.kt b/paymentsheet/src/main/java/com/stripe/android/link/express/LinkExpressArgs.kt new file mode 100644 index 00000000000..cff6895fd90 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/link/express/LinkExpressArgs.kt @@ -0,0 +1,14 @@ +package com.stripe.android.link.express + +import android.os.Parcelable +import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.model.LinkAccount +import kotlinx.parcelize.Parcelize + +@Parcelize +internal data class LinkExpressArgs( + val configuration: LinkConfiguration, + val publishableKey: String, + val stripeAccountId: String?, + val linkAccount: LinkAccount? +) : Parcelable diff --git a/paymentsheet/src/main/java/com/stripe/android/link/express/LinkExpressContract.kt b/paymentsheet/src/main/java/com/stripe/android/link/express/LinkExpressContract.kt new file mode 100644 index 00000000000..8f5008885fd --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/link/express/LinkExpressContract.kt @@ -0,0 +1,53 @@ +package com.stripe.android.link.express + +import android.app.Activity +import android.content.Context +import android.content.Intent +import androidx.activity.result.contract.ActivityResultContract +import androidx.core.os.BundleCompat +import com.stripe.android.PaymentConfiguration +import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.model.LinkAccount +import javax.inject.Inject + +internal class LinkExpressContract @Inject constructor() : + ActivityResultContract() { + + override fun createIntent(context: Context, input: Args): Intent { + val paymentConfiguration = PaymentConfiguration.getInstance(context) + return LinkExpressActivity.createIntent( + context = context, + args = LinkExpressArgs( + configuration = input.configuration, + stripeAccountId = paymentConfiguration.stripeAccountId, + publishableKey = paymentConfiguration.publishableKey, + linkAccount = input.linkAccount + ) + ) + } + + override fun parseResult(resultCode: Int, intent: Intent?): LinkExpressResult { + return when (resultCode) { + LinkExpressActivity.RESULT_COMPLETE -> { + val result = intent?.extras?.let { + BundleCompat.getParcelable(it, EXTRA_RESULT, LinkExpressResult::class.java) + } + return result ?: LinkExpressResult.Canceled + } + Activity.RESULT_CANCELED -> { + LinkExpressResult.Canceled + } + else -> LinkExpressResult.Canceled + } + } + + data class Args( + val configuration: LinkConfiguration, + val linkAccount: LinkAccount + ) + + companion object { + internal const val EXTRA_RESULT = + "com.stripe.android.link.express.LinkExpressContract.extra_result" + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/link/express/LinkExpressLauncher.kt b/paymentsheet/src/main/java/com/stripe/android/link/express/LinkExpressLauncher.kt new file mode 100644 index 00000000000..df189a0c9a0 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/link/express/LinkExpressLauncher.kt @@ -0,0 +1,68 @@ +package com.stripe.android.link.express + +import androidx.activity.result.ActivityResultCaller +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.ActivityResultRegistry +import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.model.LinkAccount +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class LinkExpressLauncher @Inject constructor( + private val linkExpressContract: LinkExpressContract +) { + private var linkActivityResultLauncher: + ActivityResultLauncher? = null + + fun register( + activityResultRegistry: ActivityResultRegistry, + callback: (LinkExpressResult) -> Unit, + ) { + linkActivityResultLauncher = activityResultRegistry.register( + "LinkPaymentLauncher", + linkExpressContract, + ) { linkExpressResult -> + handleActivityResult(linkExpressResult, callback) + } + } + + fun register( + activityResultCaller: ActivityResultCaller, + callback: (LinkExpressResult) -> Unit, + ) { + linkActivityResultLauncher = activityResultCaller.registerForActivityResult( + linkExpressContract + ) { linkExpressContract -> + handleActivityResult(linkExpressContract, callback) + } + } + + private fun handleActivityResult( + linkExpressResult: LinkExpressResult, + nextStep: (LinkExpressResult) -> Unit + ) { + nextStep(linkExpressResult) + } + + fun unregister() { + linkActivityResultLauncher?.unregister() + linkActivityResultLauncher = null + } + + /** + * Launch the Link UI to process a payment. + * + * @param configuration The payment and customer settings + */ + fun present( + configuration: LinkConfiguration, + linkAccount: LinkAccount + ) { + val args = LinkExpressContract.Args( + configuration = configuration, + linkAccount = linkAccount + ) + linkActivityResultLauncher?.launch(args) + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/link/express/LinkExpressResult.kt b/paymentsheet/src/main/java/com/stripe/android/link/express/LinkExpressResult.kt new file mode 100644 index 00000000000..ee105fd09ee --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/link/express/LinkExpressResult.kt @@ -0,0 +1,17 @@ +package com.stripe.android.link.express + +import android.os.Parcelable +import com.stripe.android.link.model.LinkAccount +import kotlinx.parcelize.Parcelize + +@Parcelize +internal sealed interface LinkExpressResult : Parcelable { + @Parcelize + data class Authenticated(val linkAccount: LinkAccount) : LinkExpressResult + + @Parcelize + data object Canceled : LinkExpressResult + + @Parcelize + data class Failed(val error: Throwable) : LinkExpressResult +} diff --git a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkComponent.kt b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkComponent.kt index 56e4eb200d2..70a811808d1 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkComponent.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkComponent.kt @@ -11,6 +11,7 @@ import com.stripe.android.link.LinkConfiguration import com.stripe.android.link.account.LinkAccountManager import com.stripe.android.link.analytics.LinkEventsReporter import com.stripe.android.link.confirmation.LinkConfirmationHandler +import com.stripe.android.link.model.LinkAccount import com.stripe.android.paymentelement.confirmation.injection.DefaultConfirmationModule import com.stripe.android.payments.core.injection.STATUS_BAR_COLOR import dagger.BindsInstance @@ -53,6 +54,9 @@ internal interface NativeLinkComponent { @BindsInstance fun context(context: Context): Builder + @BindsInstance + fun linkAccount(linkAccount: LinkAccount?): Builder + @BindsInstance fun savedStateHandle(savedStateHandle: SavedStateHandle): Builder diff --git a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt index d658eee3dda..d9b3cb49abe 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt @@ -20,12 +20,14 @@ import com.stripe.android.core.utils.ContextUtils.packageInfo import com.stripe.android.core.utils.DefaultDurationProvider import com.stripe.android.core.utils.DurationProvider import com.stripe.android.core.version.StripeSdkVersion +import com.stripe.android.link.LinkConfiguration import com.stripe.android.link.account.DefaultLinkAccountManager import com.stripe.android.link.account.LinkAccountManager import com.stripe.android.link.analytics.DefaultLinkEventsReporter import com.stripe.android.link.analytics.LinkEventsReporter import com.stripe.android.link.confirmation.DefaultLinkConfirmationHandler import com.stripe.android.link.confirmation.LinkConfirmationHandler +import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.repositories.LinkApiRepository import com.stripe.android.link.repositories.LinkRepository import com.stripe.android.networking.StripeApiRepository @@ -55,10 +57,6 @@ internal interface NativeLinkModule { @NativeLinkScope fun bindLinkEventsReporter(linkEventsReporter: DefaultLinkEventsReporter): LinkEventsReporter - @Binds - @NativeLinkScope - fun bindLinkAccountManager(linkAccountManager: DefaultLinkAccountManager): LinkAccountManager - @Binds @NativeLinkScope fun bindsErrorReporter(errorReporter: RealErrorReporter): ErrorReporter @@ -164,5 +162,24 @@ internal interface NativeLinkModule { @Provides @NativeLinkScope fun provideEventReporterMode(): EventReporter.Mode = EventReporter.Mode.Custom + + @Provides + @NativeLinkScope + fun provideLinkAccountManager( + configuration: LinkConfiguration, + linkRepository: LinkRepository, + linkEventsReporter: LinkEventsReporter, + errorReporter: ErrorReporter, + linkAccount: LinkAccount? + ): LinkAccountManager { + return DefaultLinkAccountManager( + config = configuration, + linkRepository = linkRepository, + linkEventsReporter = linkEventsReporter, + errorReporter = errorReporter + ).apply { + setAccount(linkAccount) + } + } } } diff --git a/paymentsheet/src/main/java/com/stripe/android/link/model/LinkAccount.kt b/paymentsheet/src/main/java/com/stripe/android/link/model/LinkAccount.kt index 17f6b22c9a2..d63cc2c02ee 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/model/LinkAccount.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/model/LinkAccount.kt @@ -1,21 +1,30 @@ package com.stripe.android.link.model +import android.os.Parcelable import com.stripe.android.model.ConsumerSession +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize /** * Immutable object representing a Link account. */ -internal class LinkAccount(private val consumerSession: ConsumerSession) { +@Parcelize +internal class LinkAccount(private val consumerSession: ConsumerSession) : Parcelable { + @IgnoredOnParcel val redactedPhoneNumber = consumerSession.redactedPhoneNumber + @IgnoredOnParcel val clientSecret = consumerSession.clientSecret + @IgnoredOnParcel val email = consumerSession.emailAddress + @IgnoredOnParcel val isVerified: Boolean = consumerSession.containsVerifiedSMSSession() || consumerSession.isVerifiedForSignup() + @IgnoredOnParcel val accountStatus = when { isVerified -> { AccountStatus.Verified diff --git a/paymentsheet/src/main/java/com/stripe/android/link/theme/Theme.kt b/paymentsheet/src/main/java/com/stripe/android/link/theme/Theme.kt index 46191f6a89c..11bab1ce233 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/theme/Theme.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/theme/Theme.kt @@ -2,6 +2,7 @@ package com.stripe.android.link.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable @@ -27,8 +28,13 @@ internal fun DefaultLinkTheme( colors = colors.materialColors, typography = Typography, shapes = MaterialTheme.shapes, - content = content - ) + ) { + Surface( + color = MaterialTheme.colors.background + ) { + content() + } + } } } diff --git a/paymentsheet/src/main/java/com/stripe/android/link/ui/LinkContent.kt b/paymentsheet/src/main/java/com/stripe/android/link/ui/LinkContent.kt index 6cd2e2a5abb..0289a6700bb 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/ui/LinkContent.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/ui/LinkContent.kt @@ -153,9 +153,14 @@ private fun Screens( val viewModel: VerificationViewModel = linkViewModel { parentComponent -> VerificationViewModel.factory( parentComponent = parentComponent, - goBack = goBack, - navigateAndClearStack = navigateAndClearStack, - linkAccount = linkAccount + onDismissClicked = goBack, + linkAccount = linkAccount, + onVerificationSucceeded = { + navigateAndClearStack(LinkScreen.Wallet) + }, + onChangeEmailClicked = { + navigateAndClearStack(LinkScreen.SignUp) + } ) } VerificationScreen(viewModel) @@ -164,17 +169,11 @@ private fun Screens( composable(LinkScreen.Wallet.route) { val linkAccount = getLinkAccount() ?: return@composable dismissWithResult(LinkActivityResult.Failed(NoLinkAccountFoundException())) - val viewModel: WalletViewModel = linkViewModel { parentComponent -> - WalletViewModel.factory( - parentComponent = parentComponent, - linkAccount = linkAccount, - navigate = navigate, - navigateAndClearStack = navigateAndClearStack, - dismissWithResult = dismissWithResult - ) - } - WalletScreen( - viewModel = viewModel, + WalletRoute( + linkAccount = linkAccount, + navigate = navigate, + navigateAndClearStack = navigateAndClearStack, + dismissWithResult = dismissWithResult, showBottomSheetContent = showBottomSheetContent, hideBottomSheetContent = hideBottomSheetContent ) @@ -192,6 +191,31 @@ private fun Screens( } } +@Composable +private fun WalletRoute( + linkAccount: LinkAccount, + navigate: (route: LinkScreen) -> Unit, + navigateAndClearStack: (route: LinkScreen) -> Unit, + dismissWithResult: (LinkActivityResult) -> Unit, + showBottomSheetContent: (BottomSheetContent?) -> Unit, + hideBottomSheetContent: () -> Unit +) { + val viewModel: WalletViewModel = linkViewModel { parentComponent -> + WalletViewModel.factory( + parentComponent = parentComponent, + linkAccount = linkAccount, + navigate = navigate, + navigateAndClearStack = navigateAndClearStack, + dismissWithResult = dismissWithResult + ) + } + WalletScreen( + viewModel = viewModel, + showBottomSheetContent = showBottomSheetContent, + hideBottomSheetContent = hideBottomSheetContent + ) +} + @Composable private fun PaymentMethodRoute( linkAccount: LinkAccount, diff --git a/paymentsheet/src/main/java/com/stripe/android/link/ui/verification/VerificationScreen.kt b/paymentsheet/src/main/java/com/stripe/android/link/ui/verification/VerificationScreen.kt index 66bc55d969e..01375402f3d 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/ui/verification/VerificationScreen.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/ui/verification/VerificationScreen.kt @@ -78,7 +78,7 @@ internal fun VerificationScreen( otpElement = viewModel.otpElement, focusRequester = focusRequester, onBack = viewModel::onBack, - onChangeEmailClick = viewModel::onChangeEmailClicked, + onChangeEmailClick = viewModel::onChangeEmailButtonClicked, onResendCodeClick = viewModel::resendCode ) } diff --git a/paymentsheet/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt index 6108ef39b96..1280fd4e17e 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/ui/verification/VerificationViewModel.kt @@ -1,16 +1,23 @@ package com.stripe.android.link.ui.verification +import android.app.Application +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.stripe.android.core.Logger import com.stripe.android.core.strings.ResolvableString import com.stripe.android.core.strings.resolvableString -import com.stripe.android.link.LinkScreen +import com.stripe.android.link.NoArgsException import com.stripe.android.link.account.LinkAccountManager import com.stripe.android.link.analytics.LinkEventsReporter +import com.stripe.android.link.express.LinkExpressActivity.Companion.getArgs +import com.stripe.android.link.express.LinkExpressArgs +import com.stripe.android.link.injection.DaggerNativeLinkComponent import com.stripe.android.link.injection.NativeLinkComponent import com.stripe.android.link.model.AccountStatus import com.stripe.android.link.model.LinkAccount @@ -29,18 +36,19 @@ import javax.inject.Inject * ViewModel that handles user verification confirmation logic. */ internal class VerificationViewModel @Inject constructor( - private val linkAccount: LinkAccount, + private val linkAccount: LinkAccount?, private val linkAccountManager: LinkAccountManager, private val linkEventsReporter: LinkEventsReporter, private val logger: Logger, - private val goBack: () -> Unit, - private val navigateAndClearStack: (route: LinkScreen) -> Unit, + private val onVerificationSucceeded: (LinkAccount) -> Unit, + private val onChangeEmailClicked: () -> Unit, + private val onDismissClicked: () -> Unit, ) : ViewModel() { private val _viewState = MutableStateFlow( value = VerificationViewState( - redactedPhoneNumber = linkAccount.redactedPhoneNumber, - email = linkAccount.email, + redactedPhoneNumber = linkAccount?.redactedPhoneNumber.orEmpty(), + email = linkAccount?.email.orEmpty(), isProcessing = false, requestFocus = true, errorMessage = null, @@ -60,6 +68,9 @@ internal class VerificationViewModel @Inject constructor( } private fun setUp() { + if (linkAccount == null) { + return onDismissClicked() + } if (linkAccount.accountStatus != AccountStatus.VerificationStarted) { startVerification() } @@ -80,11 +91,11 @@ internal class VerificationViewModel @Inject constructor( } linkAccountManager.confirmVerification(code).fold( - onSuccess = { + onSuccess = { linkAccount -> updateViewState { it.copy(isProcessing = false) } - navigateAndClearStack(LinkScreen.Wallet) + onVerificationSucceeded(linkAccount) }, onFailure = { otpElement.controller.reset() @@ -125,16 +136,16 @@ internal class VerificationViewModel @Inject constructor( fun onBack() { clearError() - goBack() + onDismissClicked() linkEventsReporter.on2FACancel() viewModelScope.launch { linkAccountManager.logOut() } } - fun onChangeEmailClicked() { + fun onChangeEmailButtonClicked() { clearError() - navigateAndClearStack(LinkScreen.SignUp) + onChangeEmailClicked() viewModelScope.launch { linkAccountManager.logOut() } @@ -179,8 +190,9 @@ internal class VerificationViewModel @Inject constructor( fun factory( parentComponent: NativeLinkComponent, linkAccount: LinkAccount, - goBack: () -> Unit, - navigateAndClearStack: (route: LinkScreen) -> Unit, + onVerificationSucceeded: (LinkAccount) -> Unit, + onChangeEmailClicked: () -> Unit, + onDismissClicked: () -> Unit, ): ViewModelProvider.Factory { return viewModelFactory { initializer { @@ -189,8 +201,44 @@ internal class VerificationViewModel @Inject constructor( linkAccountManager = parentComponent.linkAccountManager, linkEventsReporter = parentComponent.linkEventsReporter, logger = parentComponent.logger, - goBack = goBack, - navigateAndClearStack = navigateAndClearStack, + onVerificationSucceeded = onVerificationSucceeded, + onChangeEmailClicked = onChangeEmailClicked, + onDismissClicked = onDismissClicked, + ) + } + } + } + + fun factory( + savedStateHandle: SavedStateHandle? = null, + onVerificationSucceeded: (LinkAccount) -> Unit, + onChangeEmailClicked: () -> Unit, + onDismissClicked: () -> Unit, + ): ViewModelProvider.Factory { + return viewModelFactory { + initializer { + val handle: SavedStateHandle = savedStateHandle ?: createSavedStateHandle() + val app = this[APPLICATION_KEY] as Application + val args: LinkExpressArgs = getArgs(handle) ?: throw NoArgsException() + + val component = DaggerNativeLinkComponent + .builder() + .configuration(args.configuration) + .linkAccount(args.linkAccount) + .publishableKeyProvider { args.publishableKey } + .stripeAccountIdProvider { args.stripeAccountId } + .savedStateHandle(handle) + .context(app) + .build() + + VerificationViewModel( + linkAccount = args.linkAccount, + linkEventsReporter = component.linkEventsReporter, + linkAccountManager = component.linkAccountManager, + logger = component.logger, + onVerificationSucceeded = onVerificationSucceeded, + onChangeEmailClicked = onChangeEmailClicked, + onDismissClicked = onDismissClicked ) } } 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..0906e2eba87 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 @@ -2,12 +2,15 @@ package com.stripe.android.paymentelement.confirmation import com.stripe.android.common.model.CommonConfiguration import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.model.AccountStatus +import com.stripe.android.link.model.LinkAccount import com.stripe.android.lpmfoundations.paymentmethod.PaymentSheetCardBrandFilter import com.stripe.android.model.PaymentMethod import com.stripe.android.paymentelement.confirmation.bacs.BacsConfirmationOption 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.linkexpress.LinkExpressConfirmationOption import com.stripe.android.paymentsheet.model.PaymentSelection internal fun PaymentSelection.toConfirmationOption( @@ -68,7 +71,42 @@ internal fun PaymentSelection.toConfirmationOption( ) } is PaymentSelection.Link -> linkConfiguration?.let { - LinkConfirmationOption(configuration = linkConfiguration) + LinkConfirmationOption( + configuration = linkConfiguration, + linkAccount = null + ) + } + } +} + +internal fun LinkAccount.toConfirmationOption( + linkConfiguration: LinkConfiguration +): ConfirmationHandler.Option? { + return toLinkConfirmationOption( + linkConfiguration = linkConfiguration, + linkAccount = this + ) +} + +private fun toLinkConfirmationOption( + linkConfiguration: LinkConfiguration?, + linkAccount: LinkAccount +): ConfirmationHandler.Option? { + if (linkConfiguration == null) return null + return when (linkAccount.accountStatus) { + AccountStatus.Verified -> { + LinkConfirmationOption( + configuration = linkConfiguration, + linkAccount = linkAccount + ) + } + AccountStatus.NeedsVerification, + AccountStatus.VerificationStarted -> { + LinkExpressConfirmationOption( + configuration = linkConfiguration, + linkAccount = linkAccount + ) } + else -> null } } 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..e796b39372f 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.linkexpress.LinkExpressConfirmationModule import dagger.Module @Module( @@ -13,6 +14,7 @@ import dagger.Module ExternalPaymentMethodConfirmationModule::class, GooglePayConfirmationModule::class, LinkConfirmationModule::class, + LinkExpressConfirmationModule::class ] ) internal interface PaymentElementConfirmationModule diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationDefinition.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationDefinition.kt index 7e47161ee93..62a14f9310f 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationDefinition.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationDefinition.kt @@ -50,7 +50,10 @@ internal class LinkConfirmationDefinition( confirmationOption: LinkConfirmationOption, confirmationParameters: ConfirmationDefinition.Parameters, ) { - launcher.present(confirmationOption.configuration) + launcher.present( + configuration = confirmationOption.configuration, + linkAccount = confirmationOption.linkAccount + ) } override fun toResult( diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationOption.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationOption.kt index 6f0db93e443..f918162321f 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationOption.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationOption.kt @@ -1,10 +1,12 @@ package com.stripe.android.paymentelement.confirmation.link import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.model.LinkAccount import com.stripe.android.paymentelement.confirmation.ConfirmationHandler import kotlinx.parcelize.Parcelize @Parcelize internal data class LinkConfirmationOption( val configuration: LinkConfiguration, + val linkAccount: LinkAccount? ) : ConfirmationHandler.Option diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationDefinition.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationDefinition.kt new file mode 100644 index 00000000000..3b8b9d526f0 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationDefinition.kt @@ -0,0 +1,87 @@ +package com.stripe.android.paymentelement.confirmation.linkexpress + +import androidx.activity.result.ActivityResultCaller +import com.stripe.android.common.exception.stripeErrorMessage +import com.stripe.android.link.express.LinkExpressLauncher +import com.stripe.android.link.express.LinkExpressResult +import com.stripe.android.paymentelement.confirmation.ConfirmationDefinition +import com.stripe.android.paymentelement.confirmation.ConfirmationHandler +import com.stripe.android.paymentelement.confirmation.intent.DeferredIntentConfirmationType +import com.stripe.android.paymentelement.confirmation.link.LinkConfirmationOption + +internal class LinkExpressConfirmationDefinition( + private val linkExpressLauncher: LinkExpressLauncher, +) : ConfirmationDefinition { + override val key: String = "LinkExpress" + + override fun option(confirmationOption: ConfirmationHandler.Option): LinkExpressConfirmationOption? { + return confirmationOption as? LinkExpressConfirmationOption + } + + override fun createLauncher( + activityResultCaller: ActivityResultCaller, + onResult: (LinkExpressResult) -> Unit + ): LinkExpressLauncher { + return linkExpressLauncher.apply { + register(activityResultCaller, onResult) + } + } + + override fun unregister(launcher: LinkExpressLauncher) { + launcher.unregister() + } + + override suspend fun action( + confirmationOption: LinkExpressConfirmationOption, + confirmationParameters: ConfirmationDefinition.Parameters, + ): ConfirmationDefinition.Action { + return ConfirmationDefinition.Action.Launch( + launcherArguments = Unit, + receivesResultInProcess = false, + deferredIntentConfirmationType = null, + ) + } + + override fun launch( + launcher: LinkExpressLauncher, + arguments: Unit, + confirmationOption: LinkExpressConfirmationOption, + confirmationParameters: ConfirmationDefinition.Parameters, + ) { + launcher.present( + configuration = confirmationOption.configuration, + linkAccount = confirmationOption.linkAccount + ) + } + + override fun toResult( + confirmationOption: LinkExpressConfirmationOption, + confirmationParameters: ConfirmationDefinition.Parameters, + deferredIntentConfirmationType: DeferredIntentConfirmationType?, + result: LinkExpressResult + ): ConfirmationDefinition.Result { + return when (result) { + is LinkExpressResult.Authenticated -> { + ConfirmationDefinition.Result.NextStep( + parameters = confirmationParameters, + confirmationOption = LinkConfirmationOption( + configuration = confirmationOption.configuration, + linkAccount = result.linkAccount + ) + ) + } + LinkExpressResult.Canceled -> { + ConfirmationDefinition.Result.Canceled( + action = ConfirmationHandler.Result.Canceled.Action.InformCancellation, + ) + } + is LinkExpressResult.Failed -> { + ConfirmationDefinition.Result.Failed( + cause = result.error, + message = result.error.stripeErrorMessage(), + type = ConfirmationHandler.Result.Failed.ErrorType.Payment, + ) + } + } + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationModule.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationModule.kt new file mode 100644 index 00000000000..483db7685f7 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationModule.kt @@ -0,0 +1,26 @@ +package com.stripe.android.paymentelement.confirmation.linkexpress + +import com.stripe.android.link.express.LinkExpressLauncher +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 LinkExpressConfirmationModule { + @JvmSuppressWildcards + @Provides + @IntoSet + fun providesLinkConfirmationDefinition( + linkExpressLauncher: LinkExpressLauncher, + ): ConfirmationDefinition<*, *, *, *> { + return LinkExpressConfirmationDefinition( + linkExpressLauncher, + ) + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationOption.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationOption.kt new file mode 100644 index 00000000000..5b73183809d --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/linkexpress/LinkExpressConfirmationOption.kt @@ -0,0 +1,12 @@ +package com.stripe.android.paymentelement.confirmation.linkexpress + +import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.model.LinkAccount +import com.stripe.android.paymentelement.confirmation.ConfirmationHandler +import kotlinx.parcelize.Parcelize + +@Parcelize +internal data class LinkExpressConfirmationOption( + val configuration: LinkConfiguration, + val linkAccount: LinkAccount +) : ConfirmationHandler.Option 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..6f739b0a0ab 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/LinkHandler.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/LinkHandler.kt @@ -9,6 +9,7 @@ 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.model.LinkAccount import com.stripe.android.link.ui.inline.UserInput import com.stripe.android.model.ConfirmPaymentIntentParams import com.stripe.android.model.PaymentMethod @@ -18,6 +19,7 @@ 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.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.Flow @@ -59,7 +61,9 @@ internal class LinkHandler @Inject constructor( linkAnalyticsComponentBuilder.build().linkAnalyticsHelper } - fun setupLink(state: LinkState?) { + fun setupLink( + state: LinkState?, + ) { _isLinkEnabled.value = state != null if (state == null) return @@ -67,6 +71,36 @@ internal class LinkHandler @Inject constructor( _linkConfiguration.value = state.configuration } + fun setupLinkLaunchEagerly( + coroutineScope: CoroutineScope, + state: LinkState?, + launchEagerly: Boolean = false, + launchLink: suspend (LinkAccount) -> Unit = {}, + ) { + setupLink(state) + + if (state == null) return + + _linkConfiguration.value = state.configuration + + coroutineScope.launch { + if (launchEagerly.not()) return@launch + val linkAccount = + linkConfigurationCoordinator.getLinkAccountFlow(state.configuration).first() ?: return@launch + if (linkAccount.isVerified) { + launchLink(linkAccount) + } + when (linkAccount.accountStatus) { + AccountStatus.Verified, + AccountStatus.NeedsVerification, + AccountStatus.VerificationStarted -> { + launchLink(linkAccount) + } + else -> Unit + } + } + } + suspend fun payWithLinkInline( paymentSelection: PaymentSelection.New.LinkInline, shouldCompleteLinkInlineFlow: Boolean, 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..9f2b4a21334 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsViewModel.kt @@ -224,7 +224,9 @@ internal class PaymentOptionsViewModel @Inject constructor( } is PaymentSelection.Saved, is PaymentSelection.GooglePay, - is PaymentSelection.Link -> processExistingPaymentMethod(paymentSelection) + is PaymentSelection.Link -> { + processExistingPaymentMethod(paymentSelection) + } is PaymentSelection.New -> processNewOrExternalPaymentMethod(paymentSelection) is PaymentSelection.ExternalPaymentMethod -> processNewOrExternalPaymentMethod(paymentSelection) } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt index 3daf2325c62..5dfe0da5200 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt @@ -17,6 +17,7 @@ import com.stripe.android.core.Logger import com.stripe.android.core.exception.StripeException import com.stripe.android.core.injection.IOContext import com.stripe.android.core.strings.ResolvableString +import com.stripe.android.core.utils.FeatureFlags import com.stripe.android.core.utils.requireApplication import com.stripe.android.googlepaylauncher.GooglePayEnvironment import com.stripe.android.googlepaylauncher.GooglePayPaymentMethodLauncher @@ -296,7 +297,18 @@ internal class PaymentSheetViewModel @Inject internal constructor( setPaymentMethodMetadata(state.paymentMethodMetadata) - linkHandler.setupLink(state.paymentMethodMetadata.linkState) + linkHandler.setupLinkLaunchEagerly( + coroutineScope = viewModelScope, + state = state.paymentMethodMetadata.linkState, + launchEagerly = FeatureFlags.nativeLinkEnabled.isEnabled, + launchLink = { linkAccount -> + val configuration = state.paymentMethodMetadata.linkState?.configuration ?: return@setupLinkLaunchEagerly + val confirmationOption = linkAccount.toConfirmationOption(configuration) + confirmationOption?.let { + confirmWithConfirmationOption(it) + } + } + ) val pendingFailedPaymentResult = confirmationHandler.awaitResult() as? ConfirmationHandler.Result.Failed @@ -477,42 +489,46 @@ internal class PaymentSheetViewModel @Inject internal constructor( linkConfiguration = linkHandler.linkConfiguration.value, ) - confirmationOption?.let { option -> - val stripeIntent = awaitStripeIntent() - - confirmationHandler.start( - arguments = ConfirmationHandler.Args( - intent = stripeIntent, - confirmationOption = option, - initializationMode = args.initializationMode, - appearance = config.appearance, - shippingDetails = config.shippingDetails, - ), - ) - } ?: run { - val message = paymentSelection?.let { - "Cannot confirm using a ${it::class.qualifiedName} payment selection!" - } ?: "Cannot confirm without a payment selection!" + if (confirmationOption != null) { + return@launch confirmWithConfirmationOption(confirmationOption) + } - val exception = IllegalStateException(message) + val message = paymentSelection?.let { + "Cannot confirm using a ${it::class.qualifiedName} payment selection!" + } ?: "Cannot confirm without a payment selection!" - val event = paymentSelection?.let { - ErrorReporter.UnexpectedErrorEvent.PAYMENT_SHEET_INVALID_PAYMENT_SELECTION_ON_CHECKOUT - } ?: ErrorReporter.UnexpectedErrorEvent.PAYMENT_SHEET_NO_PAYMENT_SELECTION_ON_CHECKOUT + val exception = IllegalStateException(message) - errorReporter.report(event, StripeException.create(exception)) + val event = paymentSelection?.let { + ErrorReporter.UnexpectedErrorEvent.PAYMENT_SHEET_INVALID_PAYMENT_SELECTION_ON_CHECKOUT + } ?: ErrorReporter.UnexpectedErrorEvent.PAYMENT_SHEET_NO_PAYMENT_SELECTION_ON_CHECKOUT - processIntentResult( - ConfirmationHandler.Result.Failed( - cause = exception, - message = exception.stripeErrorMessage(), - type = ConfirmationHandler.Result.Failed.ErrorType.Internal, - ) + errorReporter.report(event, StripeException.create(exception)) + + processIntentResult( + ConfirmationHandler.Result.Failed( + cause = exception, + message = exception.stripeErrorMessage(), + type = ConfirmationHandler.Result.Failed.ErrorType.Internal, ) - } + ) } } + private suspend fun confirmWithConfirmationOption(confirmationOption: ConfirmationHandler.Option) { + val stripeIntent = awaitStripeIntent() + + confirmationHandler.start( + arguments = ConfirmationHandler.Args( + intent = stripeIntent, + confirmationOption = confirmationOption, + initializationMode = args.initializationMode, + appearance = config.appearance, + shippingDetails = config.shippingDetails, + ), + ) + } + override fun onPaymentResult(paymentResult: PaymentResult) { viewModelScope.launch(workContext) { val stripeIntent = awaitStripeIntent() 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..6bfe8d8f7e9 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 @@ -383,6 +383,7 @@ internal val PaymentSelection.paymentMethodType: String PaymentSelection.GooglePay -> "google_pay" PaymentSelection.Link -> "link" is PaymentSelection.New -> paymentMethodCreateParams.typeCode + PaymentSelection.Link -> "link" is PaymentSelection.Saved -> paymentMethod.type?.name ?: "card" } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/SelectSavedPaymentMethodsInteractor.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/SelectSavedPaymentMethodsInteractor.kt index 9dfd22e359f..f4381db497e 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/SelectSavedPaymentMethodsInteractor.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/SelectSavedPaymentMethodsInteractor.kt @@ -170,10 +170,18 @@ internal class DefaultSelectSavedPaymentMethodsInteractor( paymentOptionsItems: List, ): PaymentOptionsItem? { val paymentSelection = when (selection) { - is PaymentSelection.Saved, PaymentSelection.Link, PaymentSelection.GooglePay -> selection + is PaymentSelection.Saved, + PaymentSelection.Link, + PaymentSelection.GooglePay -> { + selection + } - is PaymentSelection.New, is PaymentSelection.ExternalPaymentMethod, null -> savedSelection?.let { - PaymentSelection.Saved(it) + is PaymentSelection.New, + is PaymentSelection.ExternalPaymentMethod, + null -> { + savedSelection?.let { + PaymentSelection.Saved(it) + } } } diff --git a/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityContractTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityContractTest.kt index 3f4a69ad313..6e6c9becd49 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityContractTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityContractTest.kt @@ -20,7 +20,10 @@ import org.robolectric.RobolectricTestRunner class LinkActivityContractTest { private val context: Context = ApplicationProvider.getApplicationContext() - private val args = LinkActivityContract.Args(TestFactory.LINK_CONFIGURATION) + private val args = LinkActivityContract.Args( + configuration = TestFactory.LINK_CONFIGURATION, + linkAccount = null + ) @get:Rule val featureFlagTestRule = FeatureFlagTestRule( diff --git a/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt index 0cbf72e2b78..071225a44bf 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt @@ -146,7 +146,8 @@ internal class LinkActivityViewModelTest { val mockArgs = NativeLinkArgs( configuration = mock(), publishableKey = "", - stripeAccountId = null + stripeAccountId = null, + linkAccount = null ) val savedStateHandle = SavedStateHandle() val factory = LinkActivityViewModel.factory(savedStateHandle) diff --git a/paymentsheet/src/test/java/com/stripe/android/link/LinkPaymentLauncherTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/LinkPaymentLauncherTest.kt index b77b0191f75..99619b13eae 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/LinkPaymentLauncherTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/LinkPaymentLauncherTest.kt @@ -82,11 +82,11 @@ internal class LinkPaymentLauncherTest { val registerCall = awaitRegisterCall() assertThat(registerCall).isNotNull() - linkPaymentLauncher.present(TestFactory.LINK_CONFIGURATION) + linkPaymentLauncher.present(TestFactory.LINK_CONFIGURATION, null) val launchCall = awaitLaunchCall() - assertThat(launchCall).isEqualTo(LinkActivityContract.Args(TestFactory.LINK_CONFIGURATION)) + assertThat(launchCall).isEqualTo(LinkActivityContract.Args(TestFactory.LINK_CONFIGURATION, null)) awaitNextRegisteredLauncher() } @@ -172,7 +172,7 @@ internal class LinkPaymentLauncherTest { var callbackParam: LinkActivityResult? = null linkPaymentLauncher.register(activityResultRegistry) { callbackParam = it } - linkPaymentLauncher.present(TestFactory.LINK_CONFIGURATION) + linkPaymentLauncher.present(TestFactory.LINK_CONFIGURATION, null) verifyActivityResultCallback( linkActivityResult = linkActivityResult, @@ -199,7 +199,7 @@ internal class LinkPaymentLauncherTest { var callbackParam: LinkActivityResult? = null linkPaymentLauncher.register(activityResultCaller) { callbackParam = it } - linkPaymentLauncher.present(TestFactory.LINK_CONFIGURATION) + linkPaymentLauncher.present(TestFactory.LINK_CONFIGURATION, null) val registerCall = awaitRegisterCall() registerCall.callback.asCallbackFor().onActivityResult(linkActivityResult) diff --git a/paymentsheet/src/test/java/com/stripe/android/link/NativeLinkActivityContractTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/NativeLinkActivityContractTest.kt index e43c3fb3d03..3420c70da7f 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/NativeLinkActivityContractTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/NativeLinkActivityContractTest.kt @@ -33,7 +33,7 @@ class NativeLinkActivityContractTest { @Test fun `intent is created correctly`() { val contract = NativeLinkActivityContract() - val args = LinkActivityContract.Args(TestFactory.LINK_CONFIGURATION) + val args = LinkActivityContract.Args(TestFactory.LINK_CONFIGURATION, null) val intent = contract.createIntent(ApplicationProvider.getApplicationContext(), args) @@ -46,7 +46,8 @@ class NativeLinkActivityContractTest { NativeLinkArgs( configuration = TestFactory.LINK_CONFIGURATION, publishableKey = "pk_test_abcdefg", - stripeAccountId = null + stripeAccountId = null, + linkAccount = null ) ) } diff --git a/paymentsheet/src/test/java/com/stripe/android/link/WebLinkActivityContractTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/WebLinkActivityContractTest.kt index 37b5aff9747..0dfe05b551a 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/WebLinkActivityContractTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/WebLinkActivityContractTest.kt @@ -39,7 +39,7 @@ class WebLinkActivityContractTest { override fun buildPaymentUserAgent(attribution: Set) = "test" } val contract = contract(stripeRepository) - val args = LinkActivityContract.Args(TestFactory.LINK_CONFIGURATION) + val args = LinkActivityContract.Args(TestFactory.LINK_CONFIGURATION, null) val intent = contract.createIntent(ApplicationProvider.getApplicationContext(), args) diff --git a/paymentsheet/src/test/java/com/stripe/android/link/account/FakeLinkAccountManager.kt b/paymentsheet/src/test/java/com/stripe/android/link/account/FakeLinkAccountManager.kt index fa64f8963ea..c35ac96470a 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/account/FakeLinkAccountManager.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/account/FakeLinkAccountManager.kt @@ -23,11 +23,11 @@ internal open class FakeLinkAccountManager : LinkAccountManager { override val accountStatus: Flow = _accountStatus var lookupConsumerResult: Result = Result.success(null) - var startVerificationResult: Result = Result.success(LinkAccount(ConsumerSession("", "", "", ""))) - var confirmVerificationResult: Result = Result.success(LinkAccount(ConsumerSession("", "", "", ""))) - var signUpResult: Result = Result.success(LinkAccount(ConsumerSession("", "", "", ""))) - var signInWithUserInputResult: Result = Result.success(LinkAccount(ConsumerSession("", "", "", ""))) - var logOutResult: Result = Result.success(ConsumerSession("", "", "", "")) + var startVerificationResult: Result = Result.success(TestFactory.LINK_ACCOUNT) + var confirmVerificationResult: Result = Result.success(TestFactory.LINK_ACCOUNT) + var signUpResult: Result = Result.success(TestFactory.LINK_ACCOUNT) + var signInWithUserInputResult: Result = Result.success(TestFactory.LINK_ACCOUNT) + var logOutResult: Result = Result.success(TestFactory.CONSUMER_SESSION) var createCardPaymentDetailsResult: Result = Result.success( value = TestFactory.LINK_NEW_PAYMENT_DETAILS ) diff --git a/paymentsheet/src/test/java/com/stripe/android/link/ui/verification/VerificationScreenTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/ui/verification/VerificationScreenTest.kt index b0c9f21fc47..3709c714d92 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/ui/verification/VerificationScreenTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/ui/verification/VerificationScreenTest.kt @@ -153,8 +153,9 @@ internal class VerificationScreenTest { linkAccountManager = linkAccountManager, linkEventsReporter = linkEventsReporter, logger = logger, - navigateAndClearStack = {}, - goBack = {}, + onVerificationSucceeded = {}, + onChangeEmailClicked = {}, + onDismissClicked = {}, linkAccount = TestFactory.LINK_ACCOUNT ) } diff --git a/paymentsheet/src/test/java/com/stripe/android/link/ui/verification/VerificationViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/ui/verification/VerificationViewModelTest.kt index bdd7a67a2f1..36e3201e83d 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/ui/verification/VerificationViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/ui/verification/VerificationViewModelTest.kt @@ -4,7 +4,6 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import com.stripe.android.core.Logger import com.stripe.android.core.strings.resolvableString -import com.stripe.android.link.LinkScreen import com.stripe.android.link.TestFactory import com.stripe.android.link.account.FakeLinkAccountManager import com.stripe.android.link.account.LinkAccountManager @@ -60,17 +59,16 @@ internal class VerificationViewModelTest { @Test fun `When confirmVerification succeeds then it navigates to Wallet`() = runTest(dispatcher) { - val screens = arrayListOf() - fun navigateAndClearStack(screen: LinkScreen) { - screens.add(screen) - } + var linkAccountCall: LinkAccount? = null val viewModel = createViewModel( - navigateAndClearStack = ::navigateAndClearStack + onVerificationSucceeded = { linkAccount -> + linkAccountCall = linkAccount + } ) viewModel.onVerificationCodeEntered("code") - assertThat(screens).isEqualTo(listOf(LinkScreen.Wallet)) + assertThat(linkAccountCall).isEqualTo(TestFactory.LINK_ACCOUNT) } @Test @@ -122,18 +120,17 @@ internal class VerificationViewModelTest { } } - val navScreens = arrayListOf() - fun navigateAndClearStack(screen: LinkScreen) { - navScreens.add(screen) - } + var changeEmailCount = 0 createViewModel( linkAccountManager = linkAccountManager, - navigateAndClearStack = ::navigateAndClearStack - ).onChangeEmailClicked() + onChangeEmailClicked = { + changeEmailCount += 1 + }, + ).onChangeEmailButtonClicked() assertThat(linkAccountManager.callCount).isEqualTo(1) - assertThat(navScreens).isEqualTo(listOf(LinkScreen.SignUp)) + assertThat(changeEmailCount).isEqualTo(1) } @Test @@ -213,15 +210,17 @@ internal class VerificationViewModelTest { linkAccountManager: LinkAccountManager = FakeLinkAccountManager(), linkEventsReporter: LinkEventsReporter = FakeLinkEventsReporter(), logger: Logger = FakeLogger(), - navigateAndClearStack: (LinkScreen) -> Unit = {}, - goBack: () -> Unit = {}, + onVerificationSucceeded: (LinkAccount) -> Unit = {}, + onChangeEmailClicked: () -> Unit = {}, + onDismissClicked: () -> Unit = {}, ): VerificationViewModel { return VerificationViewModel( linkAccountManager = linkAccountManager, linkEventsReporter = linkEventsReporter, logger = logger, - navigateAndClearStack = navigateAndClearStack, - goBack = goBack, + onVerificationSucceeded = onVerificationSucceeded, + onDismissClicked = onDismissClicked, + onChangeEmailClicked = onChangeEmailClicked, linkAccount = TestFactory.LINK_ACCOUNT ) } 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..8c5bc2bcaee 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 @@ -258,6 +258,7 @@ class ConfirmationHandlerOptionKtxTest { ).isEqualTo( LinkConfirmationOption( configuration = LINK_CONFIGURATION, + linkAccount = null ) ) } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationActivityTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationActivityTest.kt index 3bcc6ed80cb..5cf35c3c253 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationActivityTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationActivityTest.kt @@ -213,6 +213,7 @@ internal class LinkConfirmationActivityTest(private val nativeLinkEnabled: Boole val LINK_CONFIRMATION_OPTION = LinkConfirmationOption( configuration = TestFactory.LINK_CONFIGURATION, + linkAccount = TestFactory.LINK_ACCOUNT ) val CONFIRMATION_PARAMETERS = ConfirmationDefinition.Parameters( diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationDefinitionTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationDefinitionTest.kt index fed2b6733fc..c2037e3656b 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationDefinitionTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationDefinitionTest.kt @@ -269,6 +269,7 @@ internal class LinkConfirmationDefinitionTest { private val LINK_CONFIRMATION_OPTION = LinkConfirmationOption( configuration = TestFactory.LINK_CONFIGURATION, + linkAccount = TestFactory.LINK_ACCOUNT ) } } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationFlowTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationFlowTest.kt index 219e033a5eb..4d7df624d6e 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationFlowTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/link/LinkConfirmationFlowTest.kt @@ -147,6 +147,7 @@ class LinkConfirmationFlowTest { private val LINK_CONFIRMATION_OPTION = LinkConfirmationOption( configuration = TestFactory.LINK_CONFIGURATION, + linkAccount = TestFactory.LINK_ACCOUNT ) } } 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..5b3ad84a1f0 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 @@ -825,7 +825,7 @@ internal class DefaultFlowControllerTest { flowController.confirm() - verify(linkPaymentLauncher).present(any()) + verify(linkPaymentLauncher).present(any(), anyOrNull()) } @Test @@ -1215,7 +1215,7 @@ internal class DefaultFlowControllerTest { ) flowController.confirm() - verify(linkPaymentLauncher).present(any()) + verify(linkPaymentLauncher).present(any(), anyOrNull()) } @Test diff --git a/paymentsheet/src/test/java/com/stripe/android/utils/FakeLinkConfigurationCoordinator.kt b/paymentsheet/src/test/java/com/stripe/android/utils/FakeLinkConfigurationCoordinator.kt index 780f57beac3..c7f95c18400 100644 --- a/paymentsheet/src/test/java/com/stripe/android/utils/FakeLinkConfigurationCoordinator.kt +++ b/paymentsheet/src/test/java/com/stripe/android/utils/FakeLinkConfigurationCoordinator.kt @@ -4,8 +4,10 @@ import com.stripe.android.core.model.CountryCode 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.injection.LinkComponent import com.stripe.android.link.model.AccountStatus +import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.ui.inline.UserInput import com.stripe.android.model.CardBrand import com.stripe.android.model.ConsumerPaymentDetails @@ -52,6 +54,10 @@ internal class FakeLinkConfigurationCoordinator( return flowOf(accountStatus) } + override fun getLinkAccountFlow(configuration: LinkConfiguration): StateFlow { + return stateFlowOf(TestFactory.LINK_ACCOUNT) + } + override suspend fun signInWithUserInput(configuration: LinkConfiguration, userInput: UserInput): Result { return Result.success(true) } diff --git a/paymentsheet/src/test/java/com/stripe/android/utils/RecordingLinkPaymentLauncher.kt b/paymentsheet/src/test/java/com/stripe/android/utils/RecordingLinkPaymentLauncher.kt index 60b16954177..8479ebe0618 100644 --- a/paymentsheet/src/test/java/com/stripe/android/utils/RecordingLinkPaymentLauncher.kt +++ b/paymentsheet/src/test/java/com/stripe/android/utils/RecordingLinkPaymentLauncher.kt @@ -7,6 +7,7 @@ import com.stripe.android.link.LinkActivityResult import com.stripe.android.link.LinkConfiguration import com.stripe.android.link.LinkPaymentLauncher import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock @@ -37,7 +38,7 @@ internal object RecordingLinkPaymentLauncher { unregisterCalls.add(Unit) } - on { present(any()) } doAnswer { invocation -> + on { present(any(), anyOrNull()) } doAnswer { invocation -> val arguments = invocation.arguments presentCalls.add(