From 24577af40f239d0f00a8beee9449e110d7826c87 Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Tue, 14 Jun 2022 17:57:51 -0700 Subject: [PATCH 1/5] Restore selected payment method when user returns. --- link/api/link.api | 22 +-- link/build.gradle | 3 +- .../com/stripe/android/link/LinkActivity.kt | 14 +- .../android/link/LinkActivityContract.kt | 2 + .../stripe/android/link/LinkPaymentDetails.kt | 40 +++++- .../android/link/LinkPaymentLauncher.kt | 11 +- .../com/stripe/android/link/LinkScreen.kt | 11 +- .../link/account/LinkAccountManager.kt | 20 +-- .../link/repositories/LinkApiRepository.kt | 17 ++- .../link/repositories/LinkRepository.kt | 8 +- .../ui/paymentmethod/PaymentMethodScreen.kt | 66 ++++++--- .../paymentmethod/PaymentMethodViewModel.kt | 44 ++++-- .../paymentmethod/SupportedPaymentMethod.kt | 2 +- .../android/link/ui/wallet/WalletScreen.kt | 5 +- .../android/link/ui/wallet/WalletViewModel.kt | 31 ++-- .../android/link/LinkActivityContractTest.kt | 1 + .../android/link/LinkActivityViewModelTest.kt | 1 + .../android/link/LinkPaymentLauncherTest.kt | 2 +- .../link/account/LinkAccountManagerTest.kt | 14 +- .../repositories/LinkApiRepositoryTest.kt | 135 ++++++++++++------ .../PaymentMethodViewModelTest.kt | 109 +++++++++++--- .../link/ui/signup/SignUpViewModelTest.kt | 1 + .../link/ui/wallet/WalletViewModelTest.kt | 64 +++++++-- payments-ui-core/api/payments-ui-core.api | 4 + .../com/stripe/android/ui/core/forms/Utils.kt | 38 +++++ .../BaseAddPaymentMethodFragment.kt | 2 +- .../paymentsheet/PaymentOptionsViewModel.kt | 6 +- .../paymentsheet/PaymentSheetViewModel.kt | 4 +- .../paymentsheet/model/PaymentSelection.kt | 12 +- .../FormFragmentArguments.kt | 34 +---- .../ach/USBankAccountFormFragment.kt | 2 +- .../viewmodels/BaseSheetViewModel.kt | 31 ++-- 32 files changed, 533 insertions(+), 223 deletions(-) create mode 100644 payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/Utils.kt diff --git a/link/api/link.api b/link/api/link.api index 681fd93789a..4857af01b23 100644 --- a/link/api/link.api +++ b/link/api/link.api @@ -18,8 +18,8 @@ public final class com/stripe/android/link/LinkActivityContract$Args : com/strip public static final field $stable I public static final field CREATOR Landroid/os/Parcelable$Creator; public static final field Companion Lcom/stripe/android/link/LinkActivityContract$Args$Companion; - public final fun copy (Lcom/stripe/android/model/StripeIntent;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/link/LinkActivityContract$Args$InjectionParams;)Lcom/stripe/android/link/LinkActivityContract$Args; - public static synthetic fun copy$default (Lcom/stripe/android/link/LinkActivityContract$Args;Lcom/stripe/android/model/StripeIntent;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/link/LinkActivityContract$Args$InjectionParams;ILjava/lang/Object;)Lcom/stripe/android/link/LinkActivityContract$Args; + public final fun copy (Lcom/stripe/android/model/StripeIntent;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/link/LinkPaymentDetails;Lcom/stripe/android/link/LinkActivityContract$Args$InjectionParams;)Lcom/stripe/android/link/LinkActivityContract$Args; + public static synthetic fun copy$default (Lcom/stripe/android/link/LinkActivityContract$Args;Lcom/stripe/android/model/StripeIntent;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/link/LinkPaymentDetails;Lcom/stripe/android/link/LinkActivityContract$Args$InjectionParams;ILjava/lang/Object;)Lcom/stripe/android/link/LinkActivityContract$Args; public fun describeContents ()I public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I @@ -121,21 +121,11 @@ public final class com/stripe/android/link/LinkActivityViewModel_Factory_Members public static fun injectViewModel (Lcom/stripe/android/link/LinkActivityViewModel$Factory;Lcom/stripe/android/link/LinkActivityViewModel;)V } -public final class com/stripe/android/link/LinkPaymentDetails : android/os/Parcelable { +public abstract class com/stripe/android/link/LinkPaymentDetails : android/os/Parcelable { public static final field $stable I - public static final field CREATOR Landroid/os/Parcelable$Creator; - public fun (Lcom/stripe/android/model/ConsumerPaymentDetails$PaymentDetails;Lcom/stripe/android/model/PaymentMethodCreateParams;)V - public final fun component1 ()Lcom/stripe/android/model/ConsumerPaymentDetails$PaymentDetails; - public final fun component2 ()Lcom/stripe/android/model/PaymentMethodCreateParams; - public final fun copy (Lcom/stripe/android/model/ConsumerPaymentDetails$PaymentDetails;Lcom/stripe/android/model/PaymentMethodCreateParams;)Lcom/stripe/android/link/LinkPaymentDetails; - public static synthetic fun copy$default (Lcom/stripe/android/link/LinkPaymentDetails;Lcom/stripe/android/model/ConsumerPaymentDetails$PaymentDetails;Lcom/stripe/android/model/PaymentMethodCreateParams;ILjava/lang/Object;)Lcom/stripe/android/link/LinkPaymentDetails; - public fun describeContents ()I - public fun equals (Ljava/lang/Object;)Z - public final fun getPaymentDetails ()Lcom/stripe/android/model/ConsumerPaymentDetails$PaymentDetails; - public final fun getPaymentMethodCreateParams ()Lcom/stripe/android/model/PaymentMethodCreateParams; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; - public fun writeToParcel (Landroid/os/Parcel;I)V + public synthetic fun (Lcom/stripe/android/model/ConsumerPaymentDetails$PaymentDetails;Lcom/stripe/android/model/PaymentMethodCreateParams;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getPaymentDetails ()Lcom/stripe/android/model/ConsumerPaymentDetails$PaymentDetails; + public fun getPaymentMethodCreateParams ()Lcom/stripe/android/model/PaymentMethodCreateParams; } public final class com/stripe/android/link/LinkPaymentLauncher_Factory { diff --git a/link/build.gradle b/link/build.gradle index b5711ce807a..420feca4833 100644 --- a/link/build.gradle +++ b/link/build.gradle @@ -105,7 +105,8 @@ android { kotlinOptions { freeCompilerArgs = [ "-Xuse-experimental=androidx.compose.ui.ExperimentalComposeUiApi", - "-Xuse-experimental=kotlinx.coroutines.FlowPreview" + "-Xuse-experimental=kotlinx.coroutines.FlowPreview", + "-Xopt-in=kotlin.RequiresOptIn" ] jvmTarget = "1.8" } diff --git a/link/src/main/java/com/stripe/android/link/LinkActivity.kt b/link/src/main/java/com/stripe/android/link/LinkActivity.kt index 71e1d9e21f4..0c85ad39dc0 100644 --- a/link/src/main/java/com/stripe/android/link/LinkActivity.kt +++ b/link/src/main/java/com/stripe/android/link/LinkActivity.kt @@ -167,11 +167,21 @@ internal class LinkActivity : ComponentActivity() { } } - composable(LinkScreen.PaymentMethod.route) { + composable( + LinkScreen.PaymentMethod.route, + arguments = listOf( + navArgument(LinkScreen.PaymentMethod.loadArg) { + type = NavType.BoolType + } + ) + ) { backStackEntry -> + val loadFromArgs = backStackEntry.arguments + ?.getBoolean(LinkScreen.PaymentMethod.loadArg) ?: false linkAccount?.let { account -> PaymentMethodBody( account, - viewModel.injector + viewModel.injector, + loadFromArgs ) } } diff --git a/link/src/main/java/com/stripe/android/link/LinkActivityContract.kt b/link/src/main/java/com/stripe/android/link/LinkActivityContract.kt index 047030a3de8..e59a271dc69 100644 --- a/link/src/main/java/com/stripe/android/link/LinkActivityContract.kt +++ b/link/src/main/java/com/stripe/android/link/LinkActivityContract.kt @@ -31,6 +31,7 @@ class LinkActivityContract : * @param merchantName The customer-facing business name. * @param customerEmail Email of the customer, used to pre-fill the form. * @param customerPhone Phone number of the customer, used to pre-fill the form. + * @param selectedPaymentDetails The payment method previously selected by the user. * @param injectionParams Parameters needed to perform dependency injection. * If null, a new dependency graph will be created. */ @@ -41,6 +42,7 @@ class LinkActivityContract : internal val merchantName: String, internal val customerEmail: String? = null, internal val customerPhone: String? = null, + internal val selectedPaymentDetails: LinkPaymentDetails? = null, internal val injectionParams: InjectionParams? = null ) : ActivityStarter.Args { diff --git a/link/src/main/java/com/stripe/android/link/LinkPaymentDetails.kt b/link/src/main/java/com/stripe/android/link/LinkPaymentDetails.kt index 659bb97eae0..8c8042e9e25 100644 --- a/link/src/main/java/com/stripe/android/link/LinkPaymentDetails.kt +++ b/link/src/main/java/com/stripe/android/link/LinkPaymentDetails.kt @@ -1,8 +1,10 @@ package com.stripe.android.link import android.os.Parcelable +import com.stripe.android.link.ui.forms.FormController import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.ui.core.forms.convertToFormValuesMap import kotlinx.parcelize.Parcelize /** @@ -13,8 +15,36 @@ import kotlinx.parcelize.Parcelize * @param paymentMethodCreateParams The [PaymentMethodCreateParams] to be used to confirm * the Stripe Intent. */ -@Parcelize -data class LinkPaymentDetails( - val paymentDetails: ConsumerPaymentDetails.PaymentDetails, - val paymentMethodCreateParams: PaymentMethodCreateParams -) : Parcelable +sealed class LinkPaymentDetails( + open val paymentDetails: ConsumerPaymentDetails.PaymentDetails, + open val paymentMethodCreateParams: PaymentMethodCreateParams +) : Parcelable { + + /** + * A [ConsumerPaymentDetails.PaymentDetails] that is already saved to the consumer's account. + */ + @Parcelize + internal class Saved( + override val paymentDetails: ConsumerPaymentDetails.PaymentDetails, + override val paymentMethodCreateParams: PaymentMethodCreateParams + ) : LinkPaymentDetails(paymentDetails, paymentMethodCreateParams) + + /** + * A new [ConsumerPaymentDetails.PaymentDetails], whose data was just collected from the user. + * Must hold the original [PaymentMethodCreateParams] too in case we need to populate the form + * fields with the user-entered values. + */ + @Parcelize + internal class New( + override val paymentDetails: ConsumerPaymentDetails.PaymentDetails, + override val paymentMethodCreateParams: PaymentMethodCreateParams, + private val originalParams: PaymentMethodCreateParams + ) : LinkPaymentDetails(paymentDetails, paymentMethodCreateParams) { + + /** + * Build a flat map of the values entered by the user when creating this payment method, + * in a format that can be used to set the initial values in the [FormController]. + */ + fun buildFormValues() = convertToFormValuesMap(originalParams.toParamMap()) + } +} diff --git a/link/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt b/link/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt index 16d950723d7..4ad40078a87 100644 --- a/link/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt +++ b/link/src/main/java/com/stripe/android/link/LinkPaymentLauncher.kt @@ -112,14 +112,17 @@ class LinkPaymentLauncher @AssistedInject internal constructor( * @param stripeIntent the PaymentIntent or SetupIntent. * @param completePayment whether the payment should be completed, or the selected payment * method should be returned as a result. + * @param selectedPaymentDetails the payment method previously selected by the user, if they are + * returning to Link. It will be the initially selected value. * @param coroutineScope the coroutine scope used to collect the account status flow. */ suspend fun setup( stripeIntent: StripeIntent, completePayment: Boolean, + selectedPaymentDetails: LinkPaymentDetails?, coroutineScope: CoroutineScope ): AccountStatus { - val component = setupDependencies(stripeIntent, completePayment) + val component = setupDependencies(stripeIntent, completePayment, selectedPaymentDetails) accountStatus = component.linkAccountManager.accountStatus.stateIn(coroutineScope) linkAccountManager = component.linkAccountManager return accountStatus.value @@ -152,13 +155,14 @@ class LinkPaymentLauncher @AssistedInject internal constructor( paymentMethodCreateParams: PaymentMethodCreateParams ): Result = linkAccountManager.createPaymentDetails( - SupportedPaymentMethod.Card(), + SupportedPaymentMethod.Card, paymentMethodCreateParams ) private fun setupDependencies( stripeIntent: StripeIntent, - completePayment: Boolean + completePayment: Boolean, + selectedPaymentDetails: LinkPaymentDetails? ): LinkPaymentLauncherComponent { val args = LinkActivityContract.Args( stripeIntent, @@ -166,6 +170,7 @@ class LinkPaymentLauncher @AssistedInject internal constructor( merchantName, customerEmail, customerPhone, + selectedPaymentDetails, LinkActivityContract.Args.InjectionParams( injectorKey, productUsage, diff --git a/link/src/main/java/com/stripe/android/link/LinkScreen.kt b/link/src/main/java/com/stripe/android/link/LinkScreen.kt index 60e18427dee..e8b14de0582 100644 --- a/link/src/main/java/com/stripe/android/link/LinkScreen.kt +++ b/link/src/main/java/com/stripe/android/link/LinkScreen.kt @@ -12,7 +12,16 @@ internal sealed class LinkScreen( object Loading : LinkScreen("Loading") object Verification : LinkScreen("Verification") object Wallet : LinkScreen("Wallet") - object PaymentMethod : LinkScreen("PaymentMethod") + + class PaymentMethod(loadFromArgs: Boolean = false) : + LinkScreen("PaymentMethod?$loadArg=$loadFromArgs") { + override val route = super.route + + companion object { + const val loadArg = "loadFromArgs" + const val route = "PaymentMethod?$loadArg={$loadArg}" + } + } class CardEdit(paymentDetailsId: String) : LinkScreen("CardEdit?$idArg=${paymentDetailsId.urlEncode()}") { diff --git a/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt b/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt index bb7ef81515a..d8812bc152f 100644 --- a/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt +++ b/link/src/main/java/com/stripe/android/link/account/LinkAccountManager.kt @@ -9,7 +9,6 @@ import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.repositories.LinkRepository import com.stripe.android.link.ui.inline.UserInput import com.stripe.android.link.ui.paymentmethod.SupportedPaymentMethod -import com.stripe.android.model.ConsumerPaymentDetailsCreateParams import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams import com.stripe.android.model.ConsumerSession import com.stripe.android.model.PaymentMethodCreateParams @@ -193,9 +192,10 @@ internal class LinkAccountManager @Inject constructor( ): Result = linkAccount.value?.let { account -> createPaymentDetails( - paymentMethod.createParams(paymentMethodCreateParams, account.email), - args.stripeIntent, - paymentMethod.extraConfirmationParams(paymentMethodCreateParams) + paymentMethod, + paymentMethodCreateParams, + account.email, + args.stripeIntent ) } ?: Result.failure( IllegalStateException("A non-null Link account is needed to create payment details") @@ -205,14 +205,16 @@ internal class LinkAccountManager @Inject constructor( * Create a new payment method in the signed in consumer account. */ suspend fun createPaymentDetails( - paymentDetails: ConsumerPaymentDetailsCreateParams, - stripeIntent: StripeIntent, - extraConfirmationParams: Map? + paymentMethod: SupportedPaymentMethod, + paymentMethodCreateParams: PaymentMethodCreateParams, + userEmail: String, + stripeIntent: StripeIntent ) = retryingOnAuthError { clientSecret -> linkRepository.createPaymentDetails( - paymentDetails, + paymentMethod, + paymentMethodCreateParams, + userEmail, stripeIntent, - extraConfirmationParams, clientSecret, consumerPublishableKey ) diff --git a/link/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt b/link/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt index 9b6a14e65c0..1da8ec3727b 100644 --- a/link/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt +++ b/link/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt @@ -7,11 +7,12 @@ import com.stripe.android.core.injection.STRIPE_ACCOUNT_ID import com.stripe.android.core.networking.ApiRequest import com.stripe.android.link.LinkPaymentDetails import com.stripe.android.link.confirmation.ConfirmStripeIntentParamsFactory +import com.stripe.android.link.ui.paymentmethod.SupportedPaymentMethod import com.stripe.android.model.ConsumerPaymentDetails -import com.stripe.android.model.ConsumerPaymentDetailsCreateParams import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams import com.stripe.android.model.ConsumerSession import com.stripe.android.model.ConsumerSessionLookup +import com.stripe.android.model.PaymentMethodCreateParams import com.stripe.android.model.StripeIntent import com.stripe.android.networking.StripeRepository import kotlinx.coroutines.withContext @@ -209,16 +210,17 @@ internal class LinkApiRepository @Inject constructor( } override suspend fun createPaymentDetails( - paymentDetails: ConsumerPaymentDetailsCreateParams, + paymentMethod: SupportedPaymentMethod, + paymentMethodCreateParams: PaymentMethodCreateParams, + userEmail: String, stripeIntent: StripeIntent, - extraConfirmationParams: Map?, consumerSessionClientSecret: String, consumerPublishableKey: String? ): Result = withContext(workContext) { runCatching { stripeRepository.createPaymentDetails( consumerSessionClientSecret, - paymentDetails, + paymentMethod.createParams(paymentMethodCreateParams, userEmail), consumerPublishableKey?.let { ApiRequest.Options(it) } ?: ApiRequest.Options( @@ -226,14 +228,15 @@ internal class LinkApiRepository @Inject constructor( stripeAccountIdProvider() ) )?.paymentDetails?.first()?.let { - LinkPaymentDetails( + LinkPaymentDetails.New( it, ConfirmStripeIntentParamsFactory.createFactory(stripeIntent) .createPaymentMethodCreateParams( consumerSessionClientSecret, it, - extraConfirmationParams - ) + paymentMethod.extraConfirmationParams(paymentMethodCreateParams) + ), + paymentMethodCreateParams ) } }.fold( diff --git a/link/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt b/link/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt index b0d9dc9cce0..26757afe609 100644 --- a/link/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt +++ b/link/src/main/java/com/stripe/android/link/repositories/LinkRepository.kt @@ -1,11 +1,12 @@ package com.stripe.android.link.repositories import com.stripe.android.link.LinkPaymentDetails +import com.stripe.android.link.ui.paymentmethod.SupportedPaymentMethod import com.stripe.android.model.ConsumerPaymentDetails -import com.stripe.android.model.ConsumerPaymentDetailsCreateParams import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams import com.stripe.android.model.ConsumerSession import com.stripe.android.model.ConsumerSessionLookup +import com.stripe.android.model.PaymentMethodCreateParams import com.stripe.android.model.StripeIntent /** @@ -71,9 +72,10 @@ internal interface LinkRepository { * Create a new payment method in the consumer account. */ suspend fun createPaymentDetails( - paymentDetails: ConsumerPaymentDetailsCreateParams, + paymentMethod: SupportedPaymentMethod, + paymentMethodCreateParams: PaymentMethodCreateParams, + userEmail: String, stripeIntent: StripeIntent, - extraConfirmationParams: Map? = null, consumerSessionClientSecret: String, consumerPublishableKey: String? ): Result diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodScreen.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodScreen.kt index 24f5517f5ca..f8628091b21 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodScreen.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodScreen.kt @@ -1,15 +1,20 @@ package com.stripe.android.link.ui.paymentmethod +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -52,33 +57,52 @@ private fun PaymentMethodBodyPreview() { @Composable internal fun PaymentMethodBody( linkAccount: LinkAccount, - injector: NonFallbackInjector + injector: NonFallbackInjector, + loadFromArgs: Boolean ) { val viewModel: PaymentMethodViewModel = viewModel( - factory = PaymentMethodViewModel.Factory(linkAccount, injector) + factory = PaymentMethodViewModel.Factory(linkAccount, injector, loadFromArgs) ) - val formValues by viewModel.formController.completeFormValues.collectAsState(null) - val isProcessing by viewModel.isProcessing.collectAsState() - val errorMessage by viewModel.errorMessage.collectAsState() + val formController by viewModel.formController.collectAsState() - PaymentMethodBody( - isProcessing = isProcessing, - primaryButtonLabel = primaryButtonLabel(viewModel.args, LocalContext.current.resources), - primaryButtonEnabled = formValues != null, - secondaryButtonLabel = stringResource(id = viewModel.secondaryButtonLabel), - errorMessage = errorMessage, - onPrimaryButtonClick = { - formValues?.let { - viewModel.startPayment(it) + if (formController == null) { + Box( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } else { + formController?.let { + val formValues by it.completeFormValues.collectAsState(null) + val isProcessing by viewModel.isProcessing.collectAsState() + val errorMessage by viewModel.errorMessage.collectAsState() + + PaymentMethodBody( + isProcessing = isProcessing, + primaryButtonLabel = primaryButtonLabel( + viewModel.args, + LocalContext.current.resources + ), + primaryButtonEnabled = formValues != null, + secondaryButtonLabel = stringResource(id = viewModel.secondaryButtonLabel), + errorMessage = errorMessage, + onPrimaryButtonClick = { + formValues?.let { + viewModel.startPayment(it) + } + }, + onSecondaryButtonClick = viewModel::onSecondaryButtonClick + ) { + Form( + it, + viewModel.isEnabled + ) } - }, - onSecondaryButtonClick = viewModel::onSecondaryButtonClick - ) { - Form( - viewModel.formController, - viewModel.isEnabled - ) + } } } diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt index ba3dd1d8255..ebcc606f4d0 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt @@ -18,6 +18,7 @@ import com.stripe.android.link.injection.SignedInViewModelSubcomponent import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.model.Navigator import com.stripe.android.link.ui.ErrorMessage +import com.stripe.android.link.ui.forms.FormController import com.stripe.android.link.ui.getErrorMessage import com.stripe.android.payments.paymentlauncher.PaymentResult import com.stripe.android.ui.core.FieldValuesToParamsMapConverter @@ -43,7 +44,7 @@ internal class PaymentMethodViewModel @Inject constructor( private val navigator: Navigator, private val confirmationManager: ConfirmationManager, private val logger: Logger, - formControllerProvider: Provider + private val formControllerProvider: Provider ) : ViewModel() { private val stripeIntent = args.stripeIntent @@ -62,13 +63,26 @@ internal class PaymentMethodViewModel @Inject constructor( R.string.cancel } - val paymentMethod = SupportedPaymentMethod.Card() - val formController = formControllerProvider.get() - .formSpec(LayoutSpec(paymentMethod.formSpec)) - .initialValues(emptyMap()) - .viewOnlyFields(emptySet()) - .viewModelScope(viewModelScope) - .build().formController + val paymentMethod = SupportedPaymentMethod.Card + + val formController = MutableStateFlow(null) + + fun init(loadFromArgs: Boolean) { + formController.value = + (args.selectedPaymentDetails as? LinkPaymentDetails.New)?.takeIf { loadFromArgs }?.let { + formControllerProvider.get() + .formSpec(LayoutSpec(paymentMethod.formSpec)) + .initialValues(it.buildFormValues()) + .viewOnlyFields(emptySet()) + .viewModelScope(viewModelScope) + .build().formController + } ?: formControllerProvider.get() + .formSpec(LayoutSpec(paymentMethod.formSpec)) + .initialValues(emptyMap()) + .viewOnlyFields(emptySet()) + .viewModelScope(viewModelScope) + .build().formController + } fun startPayment(formValues: Map) { clearError() @@ -83,9 +97,10 @@ internal class PaymentMethodViewModel @Inject constructor( _isProcessing.value = true linkAccountManager.createPaymentDetails( - paymentMethod.createParams(paymentMethodCreateParams, linkAccount.email), - args.stripeIntent, - paymentMethod.extraConfirmationParams(paymentMethodCreateParams) + paymentMethod, + paymentMethodCreateParams, + linkAccount.email, + args.stripeIntent ).fold( onSuccess = { paymentDetails -> if (args.completePayment) { @@ -150,7 +165,8 @@ internal class PaymentMethodViewModel @Inject constructor( internal class Factory( private val linkAccount: LinkAccount, - private val injector: NonFallbackInjector + private val injector: NonFallbackInjector, + private val loadFromArgs: Boolean ) : ViewModelProvider.Factory, NonFallbackInjectable { @Inject @@ -162,7 +178,9 @@ internal class PaymentMethodViewModel @Inject constructor( injector.inject(this) return subComponentBuilderProvider.get() .linkAccount(linkAccount) - .build().paymentMethodViewModel as T + .build().paymentMethodViewModel.apply { + init(loadFromArgs) + } as T } } } diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/SupportedPaymentMethod.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/SupportedPaymentMethod.kt index 8930077608c..f694f9eb0e2 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/SupportedPaymentMethod.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/SupportedPaymentMethod.kt @@ -35,7 +35,7 @@ internal sealed class SupportedPaymentMethod( Map? = null @Parcelize - class Card : SupportedPaymentMethod( + object Card : SupportedPaymentMethod( PaymentMethod.Type.Card, LinkCardForm.items ) { diff --git a/link/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt b/link/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt index e8cdd6d9c10..560829a5dff 100644 --- a/link/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt +++ b/link/src/main/java/com/stripe/android/link/ui/wallet/WalletScreen.kt @@ -86,6 +86,7 @@ private fun WalletBodyPreview() { "4444" ) ), + initiallySelectedId = null, primaryButtonLabel = "Pay $10.99", errorMessage = null, onAddNewPaymentMethodClick = {}, @@ -119,6 +120,7 @@ internal fun WalletBody( WalletBody( isProcessing = isProcessing, paymentDetails = paymentDetails, + initiallySelectedId = viewModel.initiallySelectedId, primaryButtonLabel = primaryButtonLabel(viewModel.args, LocalContext.current.resources), errorMessage = errorMessage, onAddNewPaymentMethodClick = viewModel::addNewPaymentMethod, @@ -134,6 +136,7 @@ internal fun WalletBody( internal fun WalletBody( isProcessing: Boolean, paymentDetails: List, + initiallySelectedId: String?, primaryButtonLabel: String, errorMessage: ErrorMessage?, onAddNewPaymentMethodClick: () -> Unit, @@ -177,7 +180,7 @@ internal fun WalletBody( Spacer(modifier = Modifier.height(12.dp)) var selectedItemId by rememberSaveable { - mutableStateOf(getDefaultSelectedCard(paymentDetails)) + mutableStateOf(initiallySelectedId ?: getDefaultSelectedCard(paymentDetails)) } // Update selected item if it's not on the list anymore diff --git a/link/src/main/java/com/stripe/android/link/ui/wallet/WalletViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/wallet/WalletViewModel.kt index aa7f089fde1..1dbeabda81d 100644 --- a/link/src/main/java/com/stripe/android/link/ui/wallet/WalletViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/wallet/WalletViewModel.kt @@ -35,6 +35,7 @@ internal class WalletViewModel @Inject constructor( private val logger: Logger ) : ViewModel() { private val stripeIntent = args.stripeIntent + val initiallySelectedId = args.selectedPaymentDetails?.paymentDetails?.id private val _paymentDetails = MutableStateFlow>(emptyList()) @@ -47,7 +48,7 @@ internal class WalletViewModel @Inject constructor( val errorMessage: StateFlow = _errorMessage init { - loadPaymentDetails() + loadPaymentDetails(true) viewModelScope.launch { navigator.getResultFlow(CardEditViewModel.Result.KEY) @@ -102,7 +103,7 @@ internal class WalletViewModel @Inject constructor( ) navigator.dismiss( LinkActivityResult.Success.Selected( - LinkPaymentDetails(selectedPaymentDetails, params) + LinkPaymentDetails.Saved(selectedPaymentDetails, params) ) ) } @@ -117,7 +118,7 @@ internal class WalletViewModel @Inject constructor( } fun addNewPaymentMethod(clearBackStack: Boolean = false) { - navigator.navigateTo(LinkScreen.PaymentMethod, clearBackStack) + navigator.navigateTo(LinkScreen.PaymentMethod(), clearBackStack) } fun editPaymentMethod(paymentDetails: ConsumerPaymentDetails.PaymentDetails) { @@ -141,16 +142,28 @@ internal class WalletViewModel @Inject constructor( } } - private fun loadPaymentDetails() { + private fun loadPaymentDetails(initialSetup: Boolean = false) { _isProcessing.value = true viewModelScope.launch { linkAccountManager.listPaymentDetails().fold( onSuccess = { response -> - response.paymentDetails.filterIsInstance() - .takeIf { it.isNotEmpty() }?.let { - _paymentDetails.value = it - _isProcessing.value = false - } ?: addNewPaymentMethod(clearBackStack = true) + val hasSavedCards = + response.paymentDetails.filterIsInstance() + .takeIf { it.isNotEmpty() }?.let { + _paymentDetails.value = it + _isProcessing.value = false + true + } ?: false + + if (initialSetup && args.selectedPaymentDetails is LinkPaymentDetails.New) { + // User is returning and had previously added a new payment method + navigator.navigateTo( + LinkScreen.PaymentMethod(true), + clearBackStack = !hasSavedCards + ) + } else if (!hasSavedCards) { + addNewPaymentMethod(clearBackStack = true) + } }, // If we can't load the payment details there's nothing to see here onFailure = ::onFatal diff --git a/link/src/test/java/com/stripe/android/link/LinkActivityContractTest.kt b/link/src/test/java/com/stripe/android/link/LinkActivityContractTest.kt index c864d0453d6..edb6e71f322 100644 --- a/link/src/test/java/com/stripe/android/link/LinkActivityContractTest.kt +++ b/link/src/test/java/com/stripe/android/link/LinkActivityContractTest.kt @@ -27,6 +27,7 @@ class LinkActivityContractTest { "Merchant, Inc", "customer@email.com", "1234567890", + null, injectionParams ) diff --git a/link/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt b/link/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt index d3762b4713b..bf6556b2ff2 100644 --- a/link/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt +++ b/link/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt @@ -38,6 +38,7 @@ class LinkActivityViewModelTest { MERCHANT_NAME, CUSTOMER_EMAIL, CUSTOMER_PHONE, + null, LinkActivityContract.Args.InjectionParams( INJECTOR_KEY, setOf(PRODUCT_USAGE), diff --git a/link/src/test/java/com/stripe/android/link/LinkPaymentLauncherTest.kt b/link/src/test/java/com/stripe/android/link/LinkPaymentLauncherTest.kt index 86ed702234d..42df99d5430 100644 --- a/link/src/test/java/com/stripe/android/link/LinkPaymentLauncherTest.kt +++ b/link/src/test/java/com/stripe/android/link/LinkPaymentLauncherTest.kt @@ -49,7 +49,7 @@ class LinkPaymentLauncherTest { runTest { launch { val stripeIntent = StripeIntentFixtures.PI_SUCCEEDED - linkPaymentLauncher.setup(stripeIntent, true, this) + linkPaymentLauncher.setup(stripeIntent, true, null, this) linkPaymentLauncher.present(mockHostActivityLauncher) verify(mockHostActivityLauncher).launch( diff --git a/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt b/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt index 1fdb663b73e..05cb5c7109d 100644 --- a/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt +++ b/link/src/test/java/com/stripe/android/link/account/LinkAccountManagerTest.kt @@ -346,6 +346,7 @@ class LinkAccountManagerTest { anyOrNull(), anyOrNull(), anyOrNull(), + anyOrNull(), anyOrNull() ) ).thenReturn( @@ -353,10 +354,12 @@ class LinkAccountManagerTest { Result.success(mock()) ) - accountManager.createPaymentDetails(mock(), mock(), mock()) + accountManager.createPaymentDetails(mock(), mock(), "", mock()) verify(linkRepository, times(2)) - .createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + .createPaymentDetails( + anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull() + ) verify(linkRepository).lookupConsumer(anyOrNull(), anyOrNull()) assertThat(accountManager.linkAccount.value).isNotNull() @@ -374,6 +377,7 @@ class LinkAccountManagerTest { anyOrNull(), anyOrNull(), anyOrNull(), + anyOrNull(), anyOrNull() ) ).thenReturn( @@ -381,10 +385,12 @@ class LinkAccountManagerTest { Result.success(mock()) ) - accountManager.createPaymentDetails(mock(), mock(), mock()) + accountManager.createPaymentDetails(mock(), mock(), "", mock()) verify(linkRepository) - .createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + .createPaymentDetails( + anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull() + ) verify(linkRepository, times(0)).lookupConsumer(anyOrNull(), anyOrNull()) } diff --git a/link/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt b/link/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt index 5e859657072..0078e9d70fb 100644 --- a/link/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt +++ b/link/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt @@ -4,18 +4,23 @@ import com.google.common.truth.Truth.assertThat import com.stripe.android.core.Logger import com.stripe.android.core.networking.ApiRequest import com.stripe.android.link.LinkPaymentDetails -import com.stripe.android.link.confirmation.ConfirmPaymentIntentParamsFactory +import com.stripe.android.link.model.PaymentDetailsFixtures +import com.stripe.android.link.ui.paymentmethod.SupportedPaymentMethod import com.stripe.android.model.ConsumerPaymentDetails -import com.stripe.android.model.ConsumerPaymentDetailsCreateParams import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams import com.stripe.android.model.ConsumerSession import com.stripe.android.model.ConsumerSessionLookup import com.stripe.android.model.PaymentIntent +import com.stripe.android.model.PaymentMethodCreateParams import com.stripe.android.networking.StripeRepository +import com.stripe.android.ui.core.FieldValuesToParamsMapConverter +import com.stripe.android.ui.core.elements.IdentifierSpec +import com.stripe.android.ui.core.forms.FormFieldEntry import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test +import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat @@ -23,9 +28,11 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner import java.util.Locale @ExperimentalCoroutinesApi +@RunWith(RobolectricTestRunner::class) class LinkApiRepositoryTest { private val stripeRepository = mock() @@ -365,21 +372,35 @@ class LinkApiRepositoryTest { @Test fun `createPaymentDetails sends correct parameters`() = runTest { val secret = "secret" - val consumerPaymentDetailsCreateParams = - ConsumerPaymentDetailsCreateParams.Card(emptyMap(), "email@stripe.com") + val email = "email@stripe.com" val consumerKey = "key" linkRepository.createPaymentDetails( - paymentDetails = consumerPaymentDetailsCreateParams, + paymentMethod = SupportedPaymentMethod.Card, + paymentMethodCreateParams = cardPaymentMethodCreateParams, + userEmail = email, stripeIntent = paymentIntent, - extraConfirmationParams = null, consumerSessionClientSecret = secret, consumerPublishableKey = consumerKey ) verify(stripeRepository).createPaymentDetails( eq(secret), - eq(consumerPaymentDetailsCreateParams), + argThat { + toParamMap() == mapOf( + "type" to "card", + "billing_email_address" to "email@stripe.com", + "card" to mapOf( + "number" to "5555555555554444", + "exp_month" to "12", + "exp_year" to "2050" + ), + "billing_address" to mapOf( + "country_code" to "US", + "postal_code" to "12345" + ) + ) + }, eq(ApiRequest.Options(consumerKey)) ) } @@ -387,56 +408,78 @@ class LinkApiRepositoryTest { @Test fun `createPaymentDetails without consumerPublishableKey sends correct parameters`() = runTest { val secret = "secret" - val consumerPaymentDetailsCreateParams = - ConsumerPaymentDetailsCreateParams.Card(emptyMap(), "email@stripe.com") + val email = "email@stripe.com" linkRepository.createPaymentDetails( - paymentDetails = consumerPaymentDetailsCreateParams, + paymentMethod = SupportedPaymentMethod.Card, + paymentMethodCreateParams = cardPaymentMethodCreateParams, + userEmail = email, stripeIntent = paymentIntent, - extraConfirmationParams = null, consumerSessionClientSecret = secret, consumerPublishableKey = null ) verify(stripeRepository).createPaymentDetails( eq(secret), - eq(consumerPaymentDetailsCreateParams), + argThat { + toParamMap() == mapOf( + "type" to "card", + "billing_email_address" to "email@stripe.com", + "card" to mapOf( + "number" to "5555555555554444", + "exp_month" to "12", + "exp_year" to "2050" + ), + "billing_address" to mapOf( + "country_code" to "US", + "postal_code" to "12345" + ) + ) + }, eq(ApiRequest.Options(PUBLISHABLE_KEY, STRIPE_ACCOUNT_ID)) ) } @Test - fun `createPaymentDetails returns successful result`() = runTest { - val secret = "secret" - val paymentDetails = mock().apply { - whenever(id).thenReturn("id") - } - val consumerPaymentDetails = mock().apply { - whenever(this.paymentDetails).thenReturn(listOf(paymentDetails)) - } + fun `createPaymentDetails returns new LinkPaymentDetails when successful`() = runTest { + val consumerSessionSecret = "consumer_session_secret" + val email = "email@stripe.com" + val paymentDetails = PaymentDetailsFixtures.CONSUMER_SINGLE_PAYMENT_DETAILS whenever(stripeRepository.createPaymentDetails(any(), any(), any())) - .thenReturn(consumerPaymentDetails) + .thenReturn(paymentDetails) val result = linkRepository.createPaymentDetails( - paymentDetails = ConsumerPaymentDetailsCreateParams.Card( - emptyMap(), - "email@stripe.com" - ), + paymentMethod = SupportedPaymentMethod.Card, + paymentMethodCreateParams = cardPaymentMethodCreateParams, + userEmail = email, stripeIntent = paymentIntent, - extraConfirmationParams = null, - consumerSessionClientSecret = secret, + consumerSessionClientSecret = consumerSessionSecret, consumerPublishableKey = null ) assertThat(result.isSuccess).isTrue() - assertThat(result.getOrNull()).isEqualTo( - LinkPaymentDetails( - paymentDetails, - ConfirmPaymentIntentParamsFactory(paymentIntent) - .createPaymentMethodCreateParams( - secret, - paymentDetails - ) + + val newLinkPaymentDetails = result.getOrThrow() as LinkPaymentDetails.New + + assertThat(newLinkPaymentDetails.paymentDetails) + .isEqualTo(paymentDetails.paymentDetails.first()) + assertThat(newLinkPaymentDetails.paymentMethodCreateParams) + .isEqualTo( + PaymentMethodCreateParams.createLink( + paymentDetails.paymentDetails.first().id, + consumerSessionSecret, + mapOf("card" to mapOf("cvc" to "123")) + ) + ) + assertThat(newLinkPaymentDetails.buildFormValues()).isEqualTo( + mapOf( + IdentifierSpec.get("type") to "card", + IdentifierSpec.CardNumber to "5555555555554444", + IdentifierSpec.CardCvc to "123", + IdentifierSpec.CardExpMonth to "12", + IdentifierSpec.CardExpYear to "2050", + IdentifierSpec.Country to "US", + IdentifierSpec.PostalCode to "12345" ) ) } @@ -447,12 +490,10 @@ class LinkApiRepositoryTest { .thenThrow(RuntimeException("error")) val result = linkRepository.createPaymentDetails( - paymentDetails = ConsumerPaymentDetailsCreateParams.Card( - emptyMap(), - "email@stripe.com" - ), + paymentMethod = SupportedPaymentMethod.Card, + paymentMethodCreateParams = cardPaymentMethodCreateParams, + userEmail = "email@stripe.com", stripeIntent = paymentIntent, - extraConfirmationParams = null, consumerSessionClientSecret = "secret", consumerPublishableKey = null ) @@ -528,6 +569,20 @@ class LinkApiRepositoryTest { assertThat(result.isFailure).isTrue() } + private val cardPaymentMethodCreateParams = + FieldValuesToParamsMapConverter.transformToPaymentMethodCreateParams( + mapOf( + IdentifierSpec.CardNumber to FormFieldEntry("5555555555554444", true), + IdentifierSpec.CardCvc to FormFieldEntry("123", true), + IdentifierSpec.CardExpMonth to FormFieldEntry("12", true), + IdentifierSpec.CardExpYear to FormFieldEntry("2050", true), + IdentifierSpec.Country to FormFieldEntry("US", true), + IdentifierSpec.PostalCode to FormFieldEntry("12345", true) + ), + "card", + false + ) + companion object { const val PUBLISHABLE_KEY = "publishableKey" const val STRIPE_ACCOUNT_ID = "stripeAccountId" diff --git a/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt b/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt index 15e2e8d3278..8ef0daf48cd 100644 --- a/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt +++ b/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt @@ -23,9 +23,9 @@ import com.stripe.android.link.model.PaymentDetailsFixtures import com.stripe.android.link.model.StripeIntentFixtures import com.stripe.android.link.ui.ErrorMessage import com.stripe.android.model.ConfirmStripeIntentParams -import com.stripe.android.model.ConsumerPaymentDetailsCreateParams import com.stripe.android.model.PaymentMethodCreateParams import com.stripe.android.payments.paymentlauncher.PaymentResult +import com.stripe.android.ui.core.FieldValuesToParamsMapConverter import com.stripe.android.ui.core.elements.IdentifierSpec import com.stripe.android.ui.core.forms.FormFieldEntry import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -33,6 +33,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.KArgumentCaptor import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argWhere @@ -67,7 +68,7 @@ class PaymentMethodViewModelTest { private val formControllerSubcomponent = mock().apply { whenever(formController).thenReturn(mock()) } - private val formControllerProvider = Provider { + private val formControllerSubcomponentBuilder = mock().apply { whenever(formSpec(anyOrNull())).thenReturn(this) whenever(initialValues(anyOrNull())).thenReturn(this) @@ -75,7 +76,7 @@ class PaymentMethodViewModelTest { whenever(viewModelScope(anyOrNull())).thenReturn(this) whenever(build()).thenReturn(formControllerSubcomponent) } - } + private val formControllerProvider = Provider { formControllerSubcomponentBuilder } @Before fun before() { @@ -86,13 +87,20 @@ class PaymentMethodViewModelTest { @Test fun `startPayment creates PaymentDetails`() = runTest { - whenever(linkAccountManager.createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull())) - .thenReturn(Result.success(createLinkPaymentDetails())) + whenever( + linkAccountManager.createPaymentDetails( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + ).thenReturn(Result.success(createLinkPaymentDetails())) createViewModel().startPayment(cardFormFieldValues) - val paramsCaptor = argumentCaptor() + val paramsCaptor = argumentCaptor() verify(linkAccountManager).createPaymentDetails( + any(), paramsCaptor.capture(), any(), anyOrNull() @@ -101,15 +109,17 @@ class PaymentMethodViewModelTest { assertThat(paramsCaptor.firstValue.toParamMap()).isEqualTo( mapOf( "type" to "card", - "billing_email_address" to "email@stripe.com", "card" to mapOf( "number" to "5555555555554444", "exp_month" to "12", - "exp_year" to "2050" + "exp_year" to "2050", + "cvc" to "123" ), - "billing_address" to mapOf( - "country_code" to "US", - "postal_code" to "12345" + "billing_details" to mapOf( + "address" to mapOf( + "country" to "US", + "postal_code" to "12345" + ) ) ) ) @@ -120,7 +130,12 @@ class PaymentMethodViewModelTest { runTest { val value = createLinkPaymentDetails() whenever( - linkAccountManager.createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) + linkAccountManager.createPaymentDetails( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) ).thenReturn(Result.success(value)) createViewModel().startPayment(cardFormFieldValues) @@ -163,7 +178,12 @@ class PaymentMethodViewModelTest { val linkPaymentDetails = createLinkPaymentDetails() whenever( - linkAccountManager.createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) + linkAccountManager.createPaymentDetails( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) ).thenReturn(Result.success(linkPaymentDetails)) createViewModel().startPayment(cardFormFieldValues) @@ -179,7 +199,12 @@ class PaymentMethodViewModelTest { @Test fun `startPayment dismisses Link on success`() = runTest { whenever( - linkAccountManager.createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) + linkAccountManager.createPaymentDetails( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) ).thenReturn(Result.success(createLinkPaymentDetails())) var callback: PaymentConfirmationCallback? = null @@ -203,7 +228,12 @@ class PaymentMethodViewModelTest { @Test fun `startPayment starts processing`() = runTest { whenever( - linkAccountManager.createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) + linkAccountManager.createPaymentDetails( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) ).thenReturn(Result.success(createLinkPaymentDetails())) val viewModel = createViewModel() @@ -221,7 +251,12 @@ class PaymentMethodViewModelTest { @Test fun `startPayment stops processing on error`() = runTest { whenever( - linkAccountManager.createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) + linkAccountManager.createPaymentDetails( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) ).thenReturn(Result.success(createLinkPaymentDetails())) var callback: PaymentConfirmationCallback? = null @@ -253,7 +288,12 @@ class PaymentMethodViewModelTest { fun `when startPayment fails then an error message is shown`() = runTest { val errorMessage = "Error message" whenever( - linkAccountManager.createPaymentDetails(anyOrNull(), anyOrNull(), anyOrNull()) + linkAccountManager.createPaymentDetails( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) ).thenReturn(Result.failure(RuntimeException(errorMessage))) val viewModel = createViewModel() @@ -263,6 +303,27 @@ class PaymentMethodViewModelTest { assertThat(viewModel.errorMessage.value).isEqualTo(ErrorMessage.Raw(errorMessage)) } + @Test + fun `when loading from arguments fails then form is prefilled`() = runTest { + whenever(args.selectedPaymentDetails).thenReturn(createLinkPaymentDetails()) + createViewModel(true) + + val initialValuesCaptor: KArgumentCaptor> = argumentCaptor() + verify(formControllerSubcomponentBuilder).initialValues(initialValuesCaptor.capture()) + + assertThat(initialValuesCaptor.firstValue).containsAtLeastEntriesIn( + mapOf( + IdentifierSpec.get("type") to "card", + IdentifierSpec.CardNumber to "5555555555554444", + IdentifierSpec.CardCvc to "123", + IdentifierSpec.CardExpMonth to "12", + IdentifierSpec.CardExpYear to "2050", + IdentifierSpec.Country to "US", + IdentifierSpec.PostalCode to "12345" + ) + ) + } + @Test fun `when screen is root then secondaryButtonLabel is correct`() = runTest { whenever(navigator.isOnRootScreen()).thenReturn(true) @@ -323,14 +384,15 @@ class PaymentMethodViewModelTest { val factory = PaymentMethodViewModel.Factory( mock(), - injector + injector, + false ) val factorySpy = spy(factory) val createdViewModel = factorySpy.create(PaymentMethodViewModel::class.java) assertThat(createdViewModel).isEqualTo(vmToBeReturned) } - private fun createViewModel() = + private fun createViewModel(loadFromArgs: Boolean = false) = PaymentMethodViewModel( args, linkAccount, @@ -339,16 +401,21 @@ class PaymentMethodViewModelTest { confirmationManager, logger, formControllerProvider - ) + ).apply { init(loadFromArgs) } private fun createLinkPaymentDetails() = PaymentDetailsFixtures.CONSUMER_SINGLE_PAYMENT_DETAILS.paymentDetails.first().let { - LinkPaymentDetails( + LinkPaymentDetails.New( it, PaymentMethodCreateParams.createLink( it.id, CLIENT_SECRET, mapOf("card" to mapOf("cvc" to "123")) + ), + FieldValuesToParamsMapConverter.transformToPaymentMethodCreateParams( + cardFormFieldValues, + "card", + false ) ) } diff --git a/link/src/test/java/com/stripe/android/link/ui/signup/SignUpViewModelTest.kt b/link/src/test/java/com/stripe/android/link/ui/signup/SignUpViewModelTest.kt index 1eae6f449cf..1ea72fdf5c1 100644 --- a/link/src/test/java/com/stripe/android/link/ui/signup/SignUpViewModelTest.kt +++ b/link/src/test/java/com/stripe/android/link/ui/signup/SignUpViewModelTest.kt @@ -51,6 +51,7 @@ class SignUpViewModelTest { MERCHANT_NAME, CUSTOMER_EMAIL, CUSTOMER_PHONE, + null, LinkActivityContract.Args.InjectionParams( INJECTOR_KEY, setOf(PRODUCT_USAGE), diff --git a/link/src/test/java/com/stripe/android/link/ui/wallet/WalletViewModelTest.kt b/link/src/test/java/com/stripe/android/link/ui/wallet/WalletViewModelTest.kt index bea369f1682..b6cfb801384 100644 --- a/link/src/test/java/com/stripe/android/link/ui/wallet/WalletViewModelTest.kt +++ b/link/src/test/java/com/stripe/android/link/ui/wallet/WalletViewModelTest.kt @@ -37,8 +37,10 @@ import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argWhere import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.spy +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner @@ -97,9 +99,51 @@ class WalletViewModelTest { createViewModel() - verify(navigator).navigateTo(LinkScreen.PaymentMethod, true) + verify(navigator).navigateTo(argWhere { it is LinkScreen.PaymentMethod }, eq(true)) } + @Test + fun `On initialization when initially selected item exists then it is selected`() = runTest { + val selected = PaymentDetailsFixtures.CONSUMER_PAYMENT_DETAILS.paymentDetails.first() + whenever(args.selectedPaymentDetails).thenReturn( + LinkPaymentDetails.Saved( + selected, + mock() + ) + ) + whenever(linkAccountManager.listPaymentDetails()) + .thenReturn(Result.success(PaymentDetailsFixtures.CONSUMER_PAYMENT_DETAILS)) + + val viewModel = createViewModel() + + assertThat(viewModel.initiallySelectedId).isEqualTo(selected.id) + + verify(navigator, times(0)).navigateTo(anyOrNull(), anyOrNull()) + } + + @Test + fun `On initialization when initially selected is new then navigate to AddPaymentMethod`() = + runTest { + whenever(args.selectedPaymentDetails).thenReturn( + LinkPaymentDetails.New( + mock(), + mock(), + mock() + ) + ) + whenever(linkAccountManager.listPaymentDetails()) + .thenReturn(Result.success(PaymentDetailsFixtures.CONSUMER_PAYMENT_DETAILS)) + + createViewModel() + + verify(navigator).navigateTo( + argWhere { + it.route == LinkScreen.PaymentMethod(true).route + }, + eq(false) + ) + } + @Test fun `onSelectedPaymentDetails returns PaymentMethodCreateParams when completePayment is false`() { whenever(args.completePayment).thenReturn(false) @@ -110,15 +154,13 @@ class WalletViewModelTest { val paramsCaptor = argumentCaptor() verify(navigator).dismiss(paramsCaptor.capture()) - assertThat(paramsCaptor.firstValue).isEqualTo( - LinkActivityResult.Success.Selected( - LinkPaymentDetails( - paymentDetails, - PaymentMethodCreateParams.createLink( - paymentDetails.id, - CLIENT_SECRET - ) - ) + val selected = + (paramsCaptor.firstValue as LinkActivityResult.Success.Selected).paymentDetails + assertThat(selected.paymentDetails).isEqualTo(paymentDetails) + assertThat(selected.paymentMethodCreateParams).isEqualTo( + PaymentMethodCreateParams.createLink( + paymentDetails.id, + CLIENT_SECRET ) ) } @@ -229,7 +271,7 @@ class WalletViewModelTest { viewModel.addNewPaymentMethod() - verify(navigator).navigateTo(LinkScreen.PaymentMethod, false) + verify(navigator).navigateTo(argWhere { it is LinkScreen.PaymentMethod }, eq(false)) } @Test diff --git a/payments-ui-core/api/payments-ui-core.api b/payments-ui-core/api/payments-ui-core.api index 8d36e0bedd6..78c7638641c 100644 --- a/payments-ui-core/api/payments-ui-core.api +++ b/payments-ui-core/api/payments-ui-core.api @@ -582,6 +582,10 @@ public final class com/stripe/android/ui/core/forms/TransformSpecToElements { public final fun transform (Ljava/util/List;)Ljava/util/List; } +public final class com/stripe/android/ui/core/forms/UtilsKt { + public static final fun convertToFormValuesMap (Ljava/util/Map;)Ljava/util/Map; +} + public final class com/stripe/android/ui/core/forms/resources/AsyncResourceRepository_Factory : dagger/internal/Factory { public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/ui/core/forms/resources/AsyncResourceRepository_Factory; diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/Utils.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/Utils.kt new file mode 100644 index 00000000000..160866653c3 --- /dev/null +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/Utils.kt @@ -0,0 +1,38 @@ +package com.stripe.android.ui.core.forms + +import androidx.annotation.RestrictTo +import com.stripe.android.ui.core.elements.IdentifierSpec + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +fun convertToFormValuesMap(paramMap: Map): Map { + val mutableMap = mutableMapOf() + addPath(paramMap, "", mutableMap) + return mutableMap +} + +@Suppress("UNCHECKED_CAST") +private fun addPath( + paramMap: Map, + path: String, + output: MutableMap +) { + for (entry in paramMap.entries) { + when (entry.value) { + null -> { + output[IdentifierSpec.get(addPathKey(path, entry.key))] = null + } + is String -> { + output[IdentifierSpec.get(addPathKey(path, entry.key))] = entry.value as String + } + is Map<*, *> -> { + addPath(entry.value as Map, addPathKey(path, entry.key), output) + } + } + } +} + +private fun addPathKey(original: String, add: String) = if (original.isEmpty()) { + add +} else { + "$original[$add]" +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BaseAddPaymentMethodFragment.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BaseAddPaymentMethodFragment.kt index 5c7954a165c..dbaa6e0c79e 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BaseAddPaymentMethodFragment.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BaseAddPaymentMethodFragment.kt @@ -187,7 +187,7 @@ internal abstract class BaseAddPaymentMethodFragment : Fragment() { merchantName = sheetViewModel.merchantName, amount = sheetViewModel.amount.value, injectorKey = sheetViewModel.injectorKey, - newLpm = sheetViewModel.newLpm, + newLpm = sheetViewModel.newPaymentSelection, isShowingLinkInlineSignup = showLinkInlineSignup ) ) 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 be160a10769..d40fc61a66c 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsViewModel.kt @@ -71,7 +71,7 @@ internal class PaymentOptionsViewModel @Inject constructor( // Only used to determine if we should skip the list and go to the add card view. // and how to populate that view. - override var newLpm = args.newLpm + override var newPaymentSelection = args.newLpm // This is used in the case where the last card was new and not saved. In this scenario // when the payment options is opened it should jump to the add card, but if the user @@ -79,7 +79,7 @@ internal class PaymentOptionsViewModel @Inject constructor( private var hasTransitionToUnsavedCard = false private val shouldTransitionToUnsavedCard: Boolean get() = - !hasTransitionToUnsavedCard && newLpm != null + !hasTransitionToUnsavedCard && newPaymentSelection != null init { savedStateHandle[SAVE_GOOGLE_PAY_READY] = args.isGooglePayReady @@ -164,7 +164,7 @@ internal class PaymentOptionsViewModel @Inject constructor( override fun onLinkPaymentDetailsCollected(linkPaymentDetails: LinkPaymentDetails?) { linkPaymentDetails?.let { // Link PaymentDetails was created successfully, use it to confirm the Stripe Intent. - updateSelection(it.convertToPaymentSelection()) + updateSelection(PaymentSelection.New.Link(it)) onUserSelection() } ?: run { // Creating Link PaymentDetails failed, fallback to regular checkout. 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 7696024f9d1..33a507bf392 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt @@ -145,7 +145,7 @@ internal class PaymentSheetViewModel @Inject internal constructor( internal val isProcessingPaymentIntent get() = args.clientSecret is PaymentIntentClientSecret - override var newLpm: PaymentSelection.New? = null + override var newPaymentSelection: PaymentSelection.New? = null @VisibleForTesting internal var googlePayPaymentMethodLauncher: GooglePayPaymentMethodLauncher? = null @@ -410,7 +410,7 @@ internal class PaymentSheetViewModel @Inject internal constructor( override fun onLinkPaymentDetailsCollected(linkPaymentDetails: LinkPaymentDetails?) { linkPaymentDetails?.let { // Link PaymentDetails was created successfully, use it to confirm the Stripe Intent. - updateSelection(it.convertToPaymentSelection()) + updateSelection(PaymentSelection.New.Link(it)) checkout(CheckoutIdentifier.SheetBottomBuy) } ?: run { // Link PaymentDetails creationg failed, fallback to regular checkout. 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 e7a933e70a4..e626de646b8 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 @@ -3,6 +3,7 @@ package com.stripe.android.paymentsheet.model import android.os.Parcelable import androidx.annotation.DrawableRes import androidx.annotation.RestrictTo +import com.stripe.android.link.LinkPaymentDetails import com.stripe.android.model.CardBrand import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.model.PaymentMethod @@ -65,13 +66,16 @@ sealed class PaymentSelection : Parcelable { @Parcelize @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - data class Link( - val paymentDetails: ConsumerPaymentDetails.PaymentDetails, - override val paymentMethodCreateParams: PaymentMethodCreateParams - ) : New() { + data class Link(val linkPaymentDetails: LinkPaymentDetails) : New() { @IgnoredOnParcel override val customerRequestedSave = CustomerRequestedSave.NoRequest + @IgnoredOnParcel + private val paymentDetails = linkPaymentDetails.paymentDetails + + @IgnoredOnParcel + override val paymentMethodCreateParams = linkPaymentDetails.paymentMethodCreateParams + @IgnoredOnParcel @DrawableRes val iconResource = when (paymentDetails) { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/FormFragmentArguments.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/FormFragmentArguments.kt index 8d04ce9369f..324ae355df7 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/FormFragmentArguments.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/FormFragmentArguments.kt @@ -7,6 +7,7 @@ import com.stripe.android.model.PaymentMethodCreateParams import com.stripe.android.paymentsheet.PaymentSheet import com.stripe.android.ui.core.Amount import com.stripe.android.ui.core.elements.IdentifierSpec +import com.stripe.android.ui.core.forms.convertToFormValuesMap import kotlinx.parcelize.Parcelize @Parcelize @@ -22,11 +23,9 @@ internal data class FormFragmentArguments( ) : Parcelable internal fun FormFragmentArguments.getInitialValuesMap(): Map { - - list.clear() - initialPaymentMethodCreateParams?.let { - addPath(it.toParamMap(), "") - } + val initialValues = initialPaymentMethodCreateParams?.let { + convertToFormValuesMap(it.toParamMap()) + } ?: emptyMap() return mapOf( IdentifierSpec.Name to this.billingDetails?.name, @@ -38,28 +37,5 @@ internal fun FormFragmentArguments.getInitialValuesMap(): Map>() -private fun addPath(map: Map, path: String) { - for (entry in map.entries) { - when (entry.value) { - null -> { - list.add(IdentifierSpec.get(addPathKey(path, entry.key)) to null) - } - is String -> { - list.add(IdentifierSpec.get(addPathKey(path, entry.key)) to entry.value as String) - } - is Map<*, *> -> { - addPath(entry.value as Map, addPathKey(path, entry.key)) - } - } - } -} - -private fun addPathKey(original: String, add: String) = if (original.isEmpty()) { - add -} else { - "$original[$add]" + ).plus(initialValues) } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormFragment.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormFragment.kt index 9e5ce4e098e..3bead99f5d2 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormFragment.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormFragment.kt @@ -139,7 +139,7 @@ internal class USBankAccountFormFragment : Fragment() { sheetViewModel is PaymentSheetViewModel, clientSecret, sheetViewModel?.usBankAccountSavedScreenState, - (sheetViewModel?.newLpm as? PaymentSelection.New.USBankAccount) + (sheetViewModel?.newPaymentSelection as? PaymentSelection.New.USBankAccount) ) }, this diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/viewmodels/BaseSheetViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/viewmodels/BaseSheetViewModel.kt index f838c052d27..44a6da00d34 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/viewmodels/BaseSheetViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/viewmodels/BaseSheetViewModel.kt @@ -123,7 +123,7 @@ internal abstract class BaseSheetViewModel( resourceRepository.getLpmRepository().fromCode( savedStateHandle.get( SAVE_SELECTED_ADD_LPM - ) ?: newLpm?.paymentMethodCreateParams?.typeCode + ) ?: newPaymentSelection?.paymentMethodCreateParams?.typeCode ) ?: supportedPaymentMethods.first() ) set(value) = savedStateHandle.set(SAVE_SELECTED_ADD_LPM, value.code) @@ -196,14 +196,13 @@ internal abstract class BaseSheetViewModel( var linkVerificationCallback: LinkVerificationCallback? = null /** - * This should be initialized from the starter args, and then from that - * point forward it will be the last valid card seen or entered in the add card view. - * In contrast to selection, this field will not be updated by the list fragment. On the - * Payment Sheet it is used to save a new card that is added for when you go back to the list - * and reopen the card view. It is used on the Payment Options sheet similar to what is - * described above, and when you have an unsaved card. + * This should be initialized from the starter args, and then from that point forward it will be + * the last valid new payment method entered by the user. + * In contrast to selection, this field will not be updated by the list fragment. It is used to + * save a new payment method that is added so that the payment data entered is recovered when + * the user returns to that payment method type. */ - abstract var newLpm: PaymentSelection.New? + abstract var newPaymentSelection: PaymentSelection.New? abstract fun onFatal(throwable: Throwable) @@ -378,7 +377,7 @@ internal abstract class BaseSheetViewModel( open fun updateSelection(selection: PaymentSelection?) { if (selection is PaymentSelection.New) { - newLpm = selection + newPaymentSelection = selection } savedStateHandle[SAVE_SELECTION] = selection @@ -389,7 +388,7 @@ internal abstract class BaseSheetViewModel( fun getAddFragmentSelectedLpm() = savedStateHandle.getLiveData( SAVE_SELECTED_ADD_LPM, - newLpm?.paymentMethodCreateParams?.typeCode + newPaymentSelection?.paymentMethodCreateParams?.typeCode ).map { resourceRepository.getLpmRepository().fromCode(it) ?: supportedPaymentMethods.first() @@ -440,7 +439,14 @@ internal abstract class BaseSheetViewModel( if (stripeIntent.paymentMethodTypes.contains(PaymentMethod.Type.Link.code)) { viewModelScope.launch { - when (linkLauncher.setup(stripeIntent, completePayment, this)) { + when ( + linkLauncher.setup( + stripeIntent, + completePayment, + (newPaymentSelection as? PaymentSelection.New.Link)?.linkPaymentDetails, + this + ) + ) { AccountStatus.Verified -> launchLink() AccountStatus.VerificationStarted, AccountStatus.NeedsVerification -> { @@ -570,9 +576,6 @@ internal abstract class BaseSheetViewModel( linkActivityResultLauncher = null } - protected fun LinkPaymentDetails.convertToPaymentSelection() = - PaymentSelection.New.Link(paymentDetails, paymentMethodCreateParams) - data class UserErrorMessage(val message: String) /** From c73baad7c3289c0dba588be30c1dc8dcbc486cb1 Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Tue, 14 Jun 2022 18:12:08 -0700 Subject: [PATCH 2/5] fixes --- .../ui/paymentmethod/PaymentMethodViewModel.kt | 14 +++++--------- .../ui/paymentmethod/PaymentMethodViewModelTest.kt | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt index ebcc606f4d0..4322bcd277b 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt @@ -69,18 +69,14 @@ internal class PaymentMethodViewModel @Inject constructor( fun init(loadFromArgs: Boolean) { formController.value = - (args.selectedPaymentDetails as? LinkPaymentDetails.New)?.takeIf { loadFromArgs }?.let { - formControllerProvider.get() - .formSpec(LayoutSpec(paymentMethod.formSpec)) - .initialValues(it.buildFormValues()) - .viewOnlyFields(emptySet()) - .viewModelScope(viewModelScope) - .build().formController - } ?: formControllerProvider.get() + formControllerProvider.get() .formSpec(LayoutSpec(paymentMethod.formSpec)) - .initialValues(emptyMap()) .viewOnlyFields(emptySet()) .viewModelScope(viewModelScope) + .initialValues( + (args.selectedPaymentDetails as? LinkPaymentDetails.New) + ?.takeIf { loadFromArgs }?.buildFormValues() ?: emptyMap() + ) .build().formController } diff --git a/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt b/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt index 8ef0daf48cd..3cae717916c 100644 --- a/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt +++ b/link/src/test/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModelTest.kt @@ -304,7 +304,7 @@ class PaymentMethodViewModelTest { } @Test - fun `when loading from arguments fails then form is prefilled`() = runTest { + fun `when loading from arguments then form is prefilled`() = runTest { whenever(args.selectedPaymentDetails).thenReturn(createLinkPaymentDetails()) createViewModel(true) From ff47fd29c68eace35453a3e0a3a0c07198ba7006 Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Tue, 14 Jun 2022 18:18:05 -0700 Subject: [PATCH 3/5] apidump --- payments-ui-core/api/payments-ui-core.api | 1 - 1 file changed, 1 deletion(-) diff --git a/payments-ui-core/api/payments-ui-core.api b/payments-ui-core/api/payments-ui-core.api index 78c7638641c..00d989d3daa 100644 --- a/payments-ui-core/api/payments-ui-core.api +++ b/payments-ui-core/api/payments-ui-core.api @@ -583,7 +583,6 @@ public final class com/stripe/android/ui/core/forms/TransformSpecToElements { } public final class com/stripe/android/ui/core/forms/UtilsKt { - public static final fun convertToFormValuesMap (Ljava/util/Map;)Ljava/util/Map; } public final class com/stripe/android/ui/core/forms/resources/AsyncResourceRepository_Factory : dagger/internal/Factory { From 1afbde903403b0a231c265f0ac3bd4cbabbcb2d0 Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Tue, 14 Jun 2022 18:33:47 -0700 Subject: [PATCH 4/5] Fix instrumentation tests --- .../java/com/stripe/android/link/ui/wallet/WalletScreenTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/link/src/androidTest/java/com/stripe/android/link/ui/wallet/WalletScreenTest.kt b/link/src/androidTest/java/com/stripe/android/link/ui/wallet/WalletScreenTest.kt index 227851cbadd..3e6830d8d01 100644 --- a/link/src/androidTest/java/com/stripe/android/link/ui/wallet/WalletScreenTest.kt +++ b/link/src/androidTest/java/com/stripe/android/link/ui/wallet/WalletScreenTest.kt @@ -325,6 +325,7 @@ internal class WalletScreenTest { WalletBody( isProcessing = isProcessing, paymentDetails = paymentDetails, + initiallySelectedId = null, primaryButtonLabel = primaryButtonLabel, errorMessage = errorMessage, onAddNewPaymentMethodClick = onAddNewPaymentMethodClick, From 08faf32f8a62490ca889a7f37306a3aa598a3cd8 Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Thu, 16 Jun 2022 15:10:12 -0700 Subject: [PATCH 5/5] fix merge --- .../main/java/com/stripe/android/link/LinkPaymentDetails.kt | 3 +-- .../android/link/ui/paymentmethod/PaymentMethodViewModel.kt | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/link/src/main/java/com/stripe/android/link/LinkPaymentDetails.kt b/link/src/main/java/com/stripe/android/link/LinkPaymentDetails.kt index 8c8042e9e25..61403d6c4d0 100644 --- a/link/src/main/java/com/stripe/android/link/LinkPaymentDetails.kt +++ b/link/src/main/java/com/stripe/android/link/LinkPaymentDetails.kt @@ -1,7 +1,6 @@ package com.stripe.android.link import android.os.Parcelable -import com.stripe.android.link.ui.forms.FormController import com.stripe.android.model.ConsumerPaymentDetails import com.stripe.android.model.PaymentMethodCreateParams import com.stripe.android.ui.core.forms.convertToFormValuesMap @@ -43,7 +42,7 @@ sealed class LinkPaymentDetails( /** * Build a flat map of the values entered by the user when creating this payment method, - * in a format that can be used to set the initial values in the [FormController]. + * in a format that can be used to set the initial values in the FormController. */ fun buildFormValues() = convertToFormValuesMap(originalParams.toParamMap()) } diff --git a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt index 165228e5ec7..1171e868457 100644 --- a/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt +++ b/link/src/main/java/com/stripe/android/link/ui/paymentmethod/PaymentMethodViewModel.kt @@ -17,10 +17,10 @@ import com.stripe.android.link.injection.SignedInViewModelSubcomponent import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.model.Navigator import com.stripe.android.link.ui.ErrorMessage -import com.stripe.android.link.ui.forms.FormController import com.stripe.android.link.ui.getErrorMessage import com.stripe.android.payments.paymentlauncher.PaymentResult import com.stripe.android.ui.core.FieldValuesToParamsMapConverter +import com.stripe.android.ui.core.FormController import com.stripe.android.ui.core.elements.IdentifierSpec import com.stripe.android.ui.core.elements.LayoutSpec import com.stripe.android.ui.core.forms.FormFieldEntry @@ -78,8 +78,8 @@ internal class PaymentMethodViewModel @Inject constructor( ?.takeIf { loadFromArgs }?.buildFormValues() ?: emptyMap() ) .stripeIntent(args.stripeIntent) - .merchantName(args.merchantName) - .build().formController + .merchantName(args.merchantName) + .build().formController } fun startPayment(formValues: Map) {