From b0a6dd87e65fef82f31977a4e3139c722ec9149d Mon Sep 17 00:00:00 2001 From: Till Hellmund Date: Mon, 30 Sep 2024 09:19:16 -0400 Subject: [PATCH] Confirm Link card brand payments (#9320) --- .../domain/CreateInstantDebitsResult.kt | 2 +- payments-core/api/payments-core.api | 10 +++-- .../ConfirmStripeIntentParamsFactory.kt | 8 ++++ .../model/ConfirmPaymentIntentParams.kt | 9 +++- .../android/model/ConfirmSetupIntentParams.kt | 9 +++- .../model/PaymentMethodCreateParams.kt | 10 ++++- .../java/com/stripe/android/model/LinkMode.kt | 5 ++- paymentsheet/detekt-baseline.xml | 1 + .../IntentConfirmationInterceptor.kt | 9 ++-- .../ach/USBankAccountFormViewModel.kt | 1 + ...efaultIntentConfirmationInterceptorTest.kt | 44 +++++++++++++++++++ 11 files changed, 95 insertions(+), 13 deletions(-) diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/CreateInstantDebitsResult.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/CreateInstantDebitsResult.kt index 7d673a12c97..d7757da2028 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/CreateInstantDebitsResult.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/CreateInstantDebitsResult.kt @@ -41,7 +41,7 @@ internal class RealCreateInstantDebitsResult @Inject constructor( consumerRepository.sharePaymentDetails( paymentDetailsId = paymentDetails.id, consumerSessionClientSecret = clientSecret, - expectedPaymentMethodType = "card", + expectedPaymentMethodType = elementsSessionContext.linkMode.expectedPaymentMethodType, ).paymentMethodId } else { repository.createPaymentMethod( diff --git a/payments-core/api/payments-core.api b/payments-core/api/payments-core.api index ac32823ff54..f9bbebdc7c3 100644 --- a/payments-core/api/payments-core.api +++ b/payments-core/api/payments-core.api @@ -2228,6 +2228,7 @@ public final class com/stripe/android/model/ConfirmPaymentIntentParams : com/str public final fun component12 ()Lcom/stripe/android/model/ConfirmPaymentIntentParams$SetupFutureUsage; public final fun component13 ()Lcom/stripe/android/model/ConfirmPaymentIntentParams$Shipping; public final fun component14 ()Ljava/lang/String; + public final fun component15 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Lcom/stripe/android/model/SourceParams; public final fun component4 ()Ljava/lang/String; @@ -2235,8 +2236,8 @@ public final class com/stripe/android/model/ConfirmPaymentIntentParams : com/str public final fun component6 ()Ljava/lang/String; public final fun component7 ()Ljava/lang/Boolean; public final fun component9 ()Lcom/stripe/android/model/PaymentMethodOptionsParams; - public final fun copy (Lcom/stripe/android/model/PaymentMethodCreateParams;Ljava/lang/String;Lcom/stripe/android/model/SourceParams;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;ZLcom/stripe/android/model/PaymentMethodOptionsParams;Ljava/lang/String;Lcom/stripe/android/model/MandateDataParams;Lcom/stripe/android/model/ConfirmPaymentIntentParams$SetupFutureUsage;Lcom/stripe/android/model/ConfirmPaymentIntentParams$Shipping;Ljava/lang/String;)Lcom/stripe/android/model/ConfirmPaymentIntentParams; - public static synthetic fun copy$default (Lcom/stripe/android/model/ConfirmPaymentIntentParams;Lcom/stripe/android/model/PaymentMethodCreateParams;Ljava/lang/String;Lcom/stripe/android/model/SourceParams;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;ZLcom/stripe/android/model/PaymentMethodOptionsParams;Ljava/lang/String;Lcom/stripe/android/model/MandateDataParams;Lcom/stripe/android/model/ConfirmPaymentIntentParams$SetupFutureUsage;Lcom/stripe/android/model/ConfirmPaymentIntentParams$Shipping;Ljava/lang/String;ILjava/lang/Object;)Lcom/stripe/android/model/ConfirmPaymentIntentParams; + public final fun copy (Lcom/stripe/android/model/PaymentMethodCreateParams;Ljava/lang/String;Lcom/stripe/android/model/SourceParams;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;ZLcom/stripe/android/model/PaymentMethodOptionsParams;Ljava/lang/String;Lcom/stripe/android/model/MandateDataParams;Lcom/stripe/android/model/ConfirmPaymentIntentParams$SetupFutureUsage;Lcom/stripe/android/model/ConfirmPaymentIntentParams$Shipping;Ljava/lang/String;Ljava/lang/String;)Lcom/stripe/android/model/ConfirmPaymentIntentParams; + public static synthetic fun copy$default (Lcom/stripe/android/model/ConfirmPaymentIntentParams;Lcom/stripe/android/model/PaymentMethodCreateParams;Ljava/lang/String;Lcom/stripe/android/model/SourceParams;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;ZLcom/stripe/android/model/PaymentMethodOptionsParams;Ljava/lang/String;Lcom/stripe/android/model/MandateDataParams;Lcom/stripe/android/model/ConfirmPaymentIntentParams$SetupFutureUsage;Lcom/stripe/android/model/ConfirmPaymentIntentParams$Shipping;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/stripe/android/model/ConfirmPaymentIntentParams; public static final fun create (Ljava/lang/String;)Lcom/stripe/android/model/ConfirmPaymentIntentParams; public static final fun create (Ljava/lang/String;Lcom/stripe/android/model/ConfirmPaymentIntentParams$Shipping;)Lcom/stripe/android/model/ConfirmPaymentIntentParams; public static final fun create (Ljava/lang/String;Lcom/stripe/android/model/ConfirmPaymentIntentParams$Shipping;Lcom/stripe/android/model/ConfirmPaymentIntentParams$SetupFutureUsage;)Lcom/stripe/android/model/ConfirmPaymentIntentParams; @@ -2379,8 +2380,9 @@ public final class com/stripe/android/model/ConfirmSetupIntentParams : com/strip public final fun component4 ()Ljava/lang/String; public final fun component6 ()Ljava/lang/String; public final fun component7 ()Lcom/stripe/android/model/MandateDataParams; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/model/PaymentMethodCreateParams;Ljava/lang/String;ZLjava/lang/String;Lcom/stripe/android/model/MandateDataParams;)Lcom/stripe/android/model/ConfirmSetupIntentParams; - public static synthetic fun copy$default (Lcom/stripe/android/model/ConfirmSetupIntentParams;Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/model/PaymentMethodCreateParams;Ljava/lang/String;ZLjava/lang/String;Lcom/stripe/android/model/MandateDataParams;ILjava/lang/Object;)Lcom/stripe/android/model/ConfirmSetupIntentParams; + public final fun component8 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/model/PaymentMethodCreateParams;Ljava/lang/String;ZLjava/lang/String;Lcom/stripe/android/model/MandateDataParams;Ljava/lang/String;)Lcom/stripe/android/model/ConfirmSetupIntentParams; + public static synthetic fun copy$default (Lcom/stripe/android/model/ConfirmSetupIntentParams;Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/model/PaymentMethodCreateParams;Ljava/lang/String;ZLjava/lang/String;Lcom/stripe/android/model/MandateDataParams;Ljava/lang/String;ILjava/lang/Object;)Lcom/stripe/android/model/ConfirmSetupIntentParams; public static final fun create (Lcom/stripe/android/model/PaymentMethodCreateParams;Ljava/lang/String;)Lcom/stripe/android/model/ConfirmSetupIntentParams; public static final fun create (Lcom/stripe/android/model/PaymentMethodCreateParams;Ljava/lang/String;Lcom/stripe/android/model/MandateDataParams;)Lcom/stripe/android/model/ConfirmSetupIntentParams; public static final fun create (Lcom/stripe/android/model/PaymentMethodCreateParams;Ljava/lang/String;Lcom/stripe/android/model/MandateDataParams;Ljava/lang/String;)Lcom/stripe/android/model/ConfirmSetupIntentParams; diff --git a/payments-core/src/main/java/com/stripe/android/ConfirmStripeIntentParamsFactory.kt b/payments-core/src/main/java/com/stripe/android/ConfirmStripeIntentParamsFactory.kt index 4cbdfc12074..f8020acf763 100644 --- a/payments-core/src/main/java/com/stripe/android/ConfirmStripeIntentParamsFactory.kt +++ b/payments-core/src/main/java/com/stripe/android/ConfirmStripeIntentParamsFactory.kt @@ -20,6 +20,7 @@ sealed class ConfirmStripeIntentParamsFactory abstract fun create( paymentMethodId: String, paymentMethodType: PaymentMethod.Type?, + expectedLinkPaymentMethodType: String?, optionsParams: PaymentMethodOptionsParams?, ): T @@ -35,6 +36,7 @@ sealed class ConfirmStripeIntentParamsFactory return create( paymentMethodId = paymentMethod.id.orEmpty(), paymentMethodType = paymentMethod.type, + expectedLinkPaymentMethodType = null, optionsParams = optionsParams, ) } @@ -67,6 +69,7 @@ internal class ConfirmPaymentIntentParamsFactory( override fun create( paymentMethodId: String, paymentMethodType: PaymentMethod.Type?, + expectedLinkPaymentMethodType: String?, optionsParams: PaymentMethodOptionsParams?, ): ConfirmPaymentIntentParams { return ConfirmPaymentIntentParams.createWithPaymentMethodId( @@ -76,6 +79,8 @@ internal class ConfirmPaymentIntentParamsFactory( mandateData = MandateDataParams(MandateDataParams.Type.Online.DEFAULT) .takeIf { paymentMethodType?.requiresMandate == true }, shipping = shipping + ).copy( + expectedPaymentMethodType = expectedLinkPaymentMethodType ) } @@ -99,6 +104,7 @@ internal class ConfirmSetupIntentParamsFactory( override fun create( paymentMethodId: String, paymentMethodType: PaymentMethod.Type?, + expectedLinkPaymentMethodType: String?, optionsParams: PaymentMethodOptionsParams?, ): ConfirmSetupIntentParams { return ConfirmSetupIntentParams.create( @@ -107,6 +113,8 @@ internal class ConfirmSetupIntentParamsFactory( mandateData = paymentMethodType?.requiresMandate?.let { MandateDataParams(MandateDataParams.Type.Online.DEFAULT) } + ).copy( + expectedPaymentMethodType = expectedLinkPaymentMethodType ) } diff --git a/payments-core/src/main/java/com/stripe/android/model/ConfirmPaymentIntentParams.kt b/payments-core/src/main/java/com/stripe/android/model/ConfirmPaymentIntentParams.kt index e3ea183aa96..15112271dcd 100644 --- a/payments-core/src/main/java/com/stripe/android/model/ConfirmPaymentIntentParams.kt +++ b/payments-core/src/main/java/com/stripe/android/model/ConfirmPaymentIntentParams.kt @@ -1,6 +1,7 @@ package com.stripe.android.model import android.os.Parcelable +import androidx.annotation.RestrictTo import com.stripe.android.model.ConfirmPaymentIntentParams.SetupFutureUsage import com.stripe.android.model.ConfirmPaymentIntentParams.SetupFutureUsage.OffSession import com.stripe.android.model.ConfirmPaymentIntentParams.SetupFutureUsage.OnSession @@ -116,7 +117,13 @@ data class ConfirmPaymentIntentParams internal constructor( * * See [receipt_email](https://stripe.com/docs/api/payment_intents/confirm#confirm_payment_intent-receipt_email). */ - var receiptEmail: String? = null + var receiptEmail: String? = null, + + /** + * Internal field to represent the expected payment method type when using the Link Card Brand. + */ + @field:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + var expectedPaymentMethodType: String? = null, ) : ConfirmStripeIntentParams { fun shouldSavePaymentMethod(): Boolean { return savePaymentMethod == true diff --git a/payments-core/src/main/java/com/stripe/android/model/ConfirmSetupIntentParams.kt b/payments-core/src/main/java/com/stripe/android/model/ConfirmSetupIntentParams.kt index 9a6de156471..23b8cdb6e3f 100644 --- a/payments-core/src/main/java/com/stripe/android/model/ConfirmSetupIntentParams.kt +++ b/payments-core/src/main/java/com/stripe/android/model/ConfirmSetupIntentParams.kt @@ -1,5 +1,6 @@ package com.stripe.android.model +import androidx.annotation.RestrictTo import com.stripe.android.model.ConfirmStripeIntentParams.Companion.PARAM_CLIENT_SECRET import com.stripe.android.model.ConfirmStripeIntentParams.Companion.PARAM_MANDATE_DATA import com.stripe.android.model.ConfirmStripeIntentParams.Companion.PARAM_MANDATE_ID @@ -44,7 +45,13 @@ data class ConfirmSetupIntentParams internal constructor( * * See [mandate_data](https://stripe.com/docs/api/setup_intents/confirm#confirm_setup_intent-mandate_data). */ - var mandateData: MandateDataParams? = null + var mandateData: MandateDataParams? = null, + + /** + * Internal field to represent the expected payment method type when using the Link Card Brand. + */ + @field:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + var expectedPaymentMethodType: String? = null, ) : ConfirmStripeIntentParams { override fun shouldUseStripeSdk(): Boolean { diff --git a/payments-core/src/main/java/com/stripe/android/model/PaymentMethodCreateParams.kt b/payments-core/src/main/java/com/stripe/android/model/PaymentMethodCreateParams.kt index 3ffacf830b0..c01a88972c2 100644 --- a/payments-core/src/main/java/com/stripe/android/model/PaymentMethodCreateParams.kt +++ b/payments-core/src/main/java/com/stripe/android/model/PaymentMethodCreateParams.kt @@ -317,11 +317,17 @@ data class PaymentMethodCreateParams internal constructor( } @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - fun instantDebitsPaymentMethodId(): String? { + fun linkBankPaymentMethodId(): String? { val linkParams = (toParamMap()["link"] as? Map<*, *>) ?: return null return linkParams["payment_method_id"] as? String } + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun expectedPaymentMethodType(): String? { + val linkParams = (toParamMap()["link"] as? Map<*, *>) ?: return null + return linkParams["expected_payment_method_type"] as? String + } + @Parcelize data class Card @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @@ -1308,6 +1314,7 @@ data class PaymentMethodCreateParams internal constructor( @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // For paymentsheet fun createInstantDebits( paymentMethodId: String, + expectedPaymentMethodType: String?, requiresMandate: Boolean, productUsage: Set, ): PaymentMethodCreateParams { @@ -1317,6 +1324,7 @@ data class PaymentMethodCreateParams internal constructor( overrideParamMap = mapOf( "link" to mapOf( "payment_method_id" to paymentMethodId, + "expected_payment_method_type" to expectedPaymentMethodType, ), ), productUsage = productUsage, diff --git a/payments-model/src/main/java/com/stripe/android/model/LinkMode.kt b/payments-model/src/main/java/com/stripe/android/model/LinkMode.kt index ba469a9258b..3df4ca6d8e2 100644 --- a/payments-model/src/main/java/com/stripe/android/model/LinkMode.kt +++ b/payments-model/src/main/java/com/stripe/android/model/LinkMode.kt @@ -6,5 +6,8 @@ import androidx.annotation.RestrictTo enum class LinkMode(val value: String) { Passthrough("PASSTHROUGH"), LinkPaymentMethod("LINK_PAYMENT_METHOD"), - LinkCardBrand("LINK_CARD_BRAND"), + LinkCardBrand("LINK_CARD_BRAND"); + + val expectedPaymentMethodType: String + get() = if (this == LinkCardBrand) "card" else "bank_account" } diff --git a/paymentsheet/detekt-baseline.xml b/paymentsheet/detekt-baseline.xml index 72073f466ff..8a5ebbdd34a 100644 --- a/paymentsheet/detekt-baseline.xml +++ b/paymentsheet/detekt-baseline.xml @@ -39,6 +39,7 @@ LongMethod:USBankAccountForm.kt$@Composable internal fun BillingDetailsForm( instantDebits: Boolean, formArgs: FormArguments, isProcessing: Boolean, isPaymentFlow: Boolean, nameController: TextFieldController, emailController: TextFieldController, phoneController: PhoneNumberController, addressController: AddressController, lastTextFieldIdentifier: IdentifierSpec?, sameAsShippingElement: SameAsShippingElement?, ) LongMethod:USBankAccountForm.kt$@Composable internal fun USBankAccountForm( formArgs: FormArguments, usBankAccountFormArgs: USBankAccountFormArguments, modifier: Modifier = Modifier, ) LongMethod:USBankAccountForm.kt$@Composable private fun AccountDetailsForm( showCheckbox: Boolean, isProcessing: Boolean, bankName: String?, last4: String?, saveForFutureUseElement: SaveForFutureUseElement, onRemoveAccount: () -> Unit, ) + LongMethod:USBankAccountFormViewModel.kt$USBankAccountFormViewModel$private fun createNewPaymentSelection( resultIdentifier: ResultIdentifier, last4: String, bankName: String, ): PaymentSelection.New.USBankAccount LongMethod:USBankAccountFormViewModelTest.kt$USBankAccountFormViewModelTest$@Test fun `Restores screen state when re-opening screen`() MagicNumber:BaseSheetActivity.kt$BaseSheetActivity$30 MagicNumber:NewPaymentMethodTabLayoutUI.kt$.3f diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/IntentConfirmationInterceptor.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/IntentConfirmationInterceptor.kt index 48847df3a5a..3665cf227f1 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/IntentConfirmationInterceptor.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/IntentConfirmationInterceptor.kt @@ -378,13 +378,14 @@ internal class DefaultIntentConfirmationInterceptor @Inject constructor( shipping = shippingValues, ) - val instantDebitsPaymentMethodId = paymentMethodCreateParams.instantDebitsPaymentMethodId() + val linkBankPaymentMethodId = paymentMethodCreateParams.linkBankPaymentMethodId() - val confirmParams = if (instantDebitsPaymentMethodId != null) { - // Instant Debits is a new payment selection, but we need to confirm the intent with a PaymentMethod ID. + val confirmParams = if (linkBankPaymentMethodId != null) { + // Link Bank Payments is a new payment selection, but we need to confirm the intent with a PaymentMethod ID. paramsFactory.create( - paymentMethodId = instantDebitsPaymentMethodId, + paymentMethodId = linkBankPaymentMethodId, paymentMethodType = PaymentMethod.Type.Link, + expectedLinkPaymentMethodType = paymentMethodCreateParams.expectedPaymentMethodType(), optionsParams = paymentMethodOptionsParams, ) } else { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormViewModel.kt index 135243ceb6f..391b7de5858 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormViewModel.kt @@ -563,6 +563,7 @@ internal class USBankAccountFormViewModel @Inject internal constructor( is ResultIdentifier.PaymentMethod -> { PaymentMethodCreateParams.createInstantDebits( paymentMethodId = resultIdentifier.id, + expectedPaymentMethodType = args.linkMode?.expectedPaymentMethodType, requiresMandate = true, productUsage = setOf("PaymentSheet"), ) diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/DefaultIntentConfirmationInterceptorTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/DefaultIntentConfirmationInterceptorTest.kt index 4c28947634c..7508a371537 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/DefaultIntentConfirmationInterceptorTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/DefaultIntentConfirmationInterceptorTest.kt @@ -578,6 +578,7 @@ class DefaultIntentConfirmationInterceptorTest { paymentMethodId = paymentMethodId, requiresMandate = false, productUsage = emptySet(), + expectedPaymentMethodType = null, ) val nextStep = interceptor.intercept( @@ -602,6 +603,49 @@ class DefaultIntentConfirmationInterceptorTest { ) } + @Test + fun `Creates correct confirm step when confirming Link Card Brand transaction`() = runTest { + val paymentMethodId = PaymentMethodFixtures.CARD_PAYMENT_METHOD.id!! + val clientSecret = "pi_1234_secret_4321" + + val interceptor = DefaultIntentConfirmationInterceptor( + stripeRepository = mock(), + publishableKeyProvider = { "pk" }, + stripeAccountIdProvider = { null }, + isFlowController = false, + ) + + val createParams = PaymentMethodCreateParams.createInstantDebits( + paymentMethodId = paymentMethodId, + requiresMandate = false, + productUsage = emptySet(), + expectedPaymentMethodType = "card", + ) + + val nextStep = interceptor.intercept( + initializationMode = InitializationMode.PaymentIntent(clientSecret), + paymentMethodCreateParams = createParams, + paymentMethodOptionsParams = null, + shippingValues = null, + customerRequestedSave = false, + ) + + val expectedConfirmParams = ConfirmPaymentIntentParams.createWithPaymentMethodId( + paymentMethodId = paymentMethodId, + clientSecret = clientSecret, + mandateData = MandateDataParams(MandateDataParams.Type.Online.DEFAULT), + ).copy( + expectedPaymentMethodType = "card", + ) + + assertThat(nextStep).isEqualTo( + IntentConfirmationInterceptor.NextStep.Confirm( + confirmParams = expectedConfirmParams, + isDeferred = false, + ) + ) + } + private fun succeedingCreateIntentCallback( expectedPaymentMethod: PaymentMethod, ): CreateIntentCallback {