Skip to content

Commit

Permalink
Show Instant Debits in payment methods carousel (#8264)
Browse files Browse the repository at this point in the history
* Show Instant Debits in payment methods carousel

* Add InstantDebits requirement tests

* Update PaymentMethodMetadata test

* Update payment options test

* Address code review feedback

- Remove `Link.code` check in requirement
- Update tests with correct feature flag value

* Add toggle for Instant Debits in playground
  • Loading branch information
tillh-stripe authored Apr 16, 2024
1 parent cf8b0ce commit c766796
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 7 deletions.
1 change: 1 addition & 0 deletions payments-ui-core/res/values/donottranslate.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<string name="stripe_paymentsheet_payment_method_clearpay">Clearpay</string>
<string name="stripe_paymentsheet_payment_method_paypal">PayPal</string>
<string name="stripe_paymentsheet_payment_method_us_bank_account">US Bank Account</string>
<string name="stripe_paymentsheet_payment_method_instant_debits">Bank</string>
<string name="stripe_paymentsheet_payment_method_upi">UPI</string>
<string name="stripe_paymentsheet_payment_method_cashapp">Cash App Pay</string>
<string name="stripe_paymentsheet_payment_method_grabpay">GrabPay</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.stripe.android.paymentsheet.example.playground.settings

import com.stripe.android.core.utils.FeatureFlags

internal object EnableInstantDebitsSettingsDefinition : BooleanSettingsDefinition(
key = "enableInstantDebits",
displayName = "Enable Instant Debits",
defaultValue = false,
) {
override fun valueUpdated(value: Boolean, playgroundSettings: PlaygroundSettings) {
FeatureFlags.instantDebits.setEnabled(value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ internal class PlaygroundSettings private constructor(
CustomerSettingsDefinition,
CheckoutModeSettingsDefinition,
LinkSettingsDefinition,
EnableInstantDebitsSettingsDefinition,
CountrySettingsDefinition,
CurrencySettingsDefinition,
GooglePaySettingsDefinition,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.stripe.android.lpmfoundations.paymentmethod

import com.stripe.android.core.utils.FeatureFlags
import com.stripe.android.model.PaymentIntent
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethod.Type.USBankAccount

internal enum class AddPaymentMethodRequirement {
/** A special case that indicates the payment method is always unsupported by PaymentSheet. */
Expand Down Expand Up @@ -48,12 +49,24 @@ internal enum class AddPaymentMethodRequirement {
/** Requires a valid us bank verification method. */
ValidUsBankVerificationMethod {
override fun isMetBy(metadata: PaymentMethodMetadata): Boolean {
val pmo = metadata.stripeIntent.getPaymentMethodOptions()[PaymentMethod.Type.USBankAccount.code]
val pmo = metadata.stripeIntent.getPaymentMethodOptions()[USBankAccount.code]
val verificationMethod = (pmo as? Map<*, *>)?.get("verification_method") as? String
val supportsVerificationMethod = verificationMethod in setOf("instant", "automatic")
val isDeferred = metadata.stripeIntent.clientSecret == null
return supportsVerificationMethod || isDeferred
}
},

/** Requires that Instant Debits are possible for this transaction. */
InstantDebits {
override fun isMetBy(metadata: PaymentMethodMetadata): Boolean {
val paymentMethodTypes = metadata.stripeIntent.paymentMethodTypes
val noUsBankAccount = USBankAccount.code !in paymentMethodTypes
val supportsBankAccounts = "bank_account" in metadata.stripeIntent.linkFundingSources
val isDeferred = metadata.stripeIntent.clientSecret == null
val isEnabled = FeatureFlags.instantDebits.isEnabled
return noUsBankAccount && supportsBankAccounts && !isDeferred && isEnabled
}
};

abstract fun isMetBy(metadata: PaymentMethodMetadata): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.stripe.android.lpmfoundations.paymentmethod.definitions.FpxDefinition
import com.stripe.android.lpmfoundations.paymentmethod.definitions.GiroPayDefinition
import com.stripe.android.lpmfoundations.paymentmethod.definitions.GrabPayDefinition
import com.stripe.android.lpmfoundations.paymentmethod.definitions.IdealDefinition
import com.stripe.android.lpmfoundations.paymentmethod.definitions.InstantDebitsDefinition
import com.stripe.android.lpmfoundations.paymentmethod.definitions.KlarnaDefinition
import com.stripe.android.lpmfoundations.paymentmethod.definitions.KonbiniDefinition
import com.stripe.android.lpmfoundations.paymentmethod.definitions.MobilePayDefinition
Expand Down Expand Up @@ -53,6 +54,7 @@ internal object PaymentMethodRegistry {
GiroPayDefinition,
GrabPayDefinition,
IdealDefinition,
InstantDebitsDefinition,
KlarnaDefinition,
KonbiniDefinition,
MobilePayDefinition,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.stripe.android.lpmfoundations.paymentmethod.definitions

import com.stripe.android.lpmfoundations.luxe.SupportedPaymentMethod
import com.stripe.android.lpmfoundations.paymentmethod.AddPaymentMethodRequirement
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodDefinition
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata
import com.stripe.android.lpmfoundations.paymentmethod.UiDefinitionFactory
import com.stripe.android.model.PaymentMethod
import com.stripe.android.uicore.elements.FormElement
import com.stripe.android.ui.core.R as PaymentsUiCoreR

internal object InstantDebitsDefinition : PaymentMethodDefinition {

override val type: PaymentMethod.Type = PaymentMethod.Type.Link

override val supportedAsSavedPaymentMethod: Boolean = false

override fun requirementsToBeUsedAsNewPaymentMethod(
hasIntentToSetup: Boolean
): Set<AddPaymentMethodRequirement> = setOf(
AddPaymentMethodRequirement.FinancialConnectionsSdk,
AddPaymentMethodRequirement.InstantDebits,
)

override fun requiresMandate(metadata: PaymentMethodMetadata): Boolean = true

override fun uiDefinitionFactory(): UiDefinitionFactory = InstantDebitsUiDefinitionFactory
}

private object InstantDebitsUiDefinitionFactory : UiDefinitionFactory.Simple {

override fun createSupportedPaymentMethod(): SupportedPaymentMethod {
return SupportedPaymentMethod(
code = InstantDebitsDefinition.type.code,
displayNameResource = PaymentsUiCoreR.string.stripe_paymentsheet_payment_method_instant_debits,
iconResource = PaymentsUiCoreR.drawable.stripe_ic_paymentsheet_pm_bank,
tintIconOnSelection = true,
lightThemeIconUrl = null,
darkThemeIconUrl = null,
)
}

// Instant Debits uses its own mechanism, not these form elements.
override fun createFormElements(
metadata: PaymentMethodMetadata,
arguments: UiDefinitionFactory.Arguments,
): List<FormElement> {
return emptyList()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import com.stripe.android.link.ui.inline.LinkInlineSignup
import com.stripe.android.link.ui.inline.LinkOptionalInlineSignup
import com.stripe.android.link.ui.inline.LinkSignupMode
import com.stripe.android.lpmfoundations.luxe.SupportedPaymentMethod
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethod.Type.Link
import com.stripe.android.model.PaymentMethod.Type.USBankAccount
import com.stripe.android.paymentsheet.PaymentMethodsUI
import com.stripe.android.paymentsheet.R
import com.stripe.android.paymentsheet.forms.FormFieldValues
Expand Down Expand Up @@ -130,7 +131,7 @@ private fun FormElement(
}
}
) {
if (selectedItem.code == PaymentMethod.Type.USBankAccount.code) {
if (selectedItem.code == USBankAccount.code || selectedItem.code == Link.code) {
USBankAccountForm(
formArgs = formArguments,
usBankAccountFormArgs = usBankAccountFormArguments,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
package com.stripe.android.lpmfoundations.paymentmethod

import com.google.common.truth.Truth.assertThat
import com.stripe.android.core.utils.FeatureFlags
import com.stripe.android.lpmfoundations.paymentmethod.AddPaymentMethodRequirement.InstantDebits
import com.stripe.android.model.Address
import com.stripe.android.model.PaymentIntent
import com.stripe.android.model.PaymentIntentFixtures
import com.stripe.android.model.SetupIntentFixtures
import com.stripe.android.model.StripeIntent
import com.stripe.android.testing.FeatureFlagTestRule
import com.stripe.android.testing.PaymentIntentFactory
import org.junit.Rule
import org.junit.Test

internal class AddPaymentMethodRequirementTest {

@get:Rule
val instantDebitsFeatureRule = FeatureFlagTestRule(
featureFlag = FeatureFlags.instantDebits,
isEnabled = false,
)

@Test
fun testUnsupportedReturnsFalse() {
val metadata = PaymentMethodMetadataFactory.create()
Expand Down Expand Up @@ -156,4 +168,72 @@ internal class AddPaymentMethodRequirementTest {
val metadata = PaymentMethodMetadataFactory.create()
assertThat(AddPaymentMethodRequirement.ValidUsBankVerificationMethod.isMetBy(metadata)).isFalse()
}

@Test
fun testInstantDebitsReturnsTrue() {
instantDebitsFeatureRule.setEnabled(true)

val metadata = PaymentMethodMetadataFactory.create(
stripeIntent = createValidInstantDebitsPaymentIntent(),
)

assertThat(InstantDebits.isMetBy(metadata)).isTrue()
}

@Test
fun testInstantDebitsReturnsFalseIfFeatureNotEnabled() {
instantDebitsFeatureRule.setEnabled(false)

val metadata = PaymentMethodMetadataFactory.create(
stripeIntent = createValidInstantDebitsPaymentIntent(),
)

assertThat(InstantDebits.isMetBy(metadata)).isFalse()
}

@Test
fun testInstantDebitsReturnsFalseIfDeferredIntent() {
instantDebitsFeatureRule.setEnabled(true)

val metadata = PaymentMethodMetadataFactory.create(
stripeIntent = createValidInstantDebitsPaymentIntent().copy(
clientSecret = null,
),
)

assertThat(InstantDebits.isMetBy(metadata)).isFalse()
}

@Test
fun testInstantDebitsReturnsFalseIfShowingUsBankAccount() {
instantDebitsFeatureRule.setEnabled(true)

val metadata = PaymentMethodMetadataFactory.create(
stripeIntent = createValidInstantDebitsPaymentIntent().copy(
paymentMethodTypes = listOf("card", "link", "us_bank_account"),
),
)

assertThat(InstantDebits.isMetBy(metadata)).isFalse()
}

@Test
fun testInstantDebitsReturnsFalseIfOnlyCardFundingSource() {
instantDebitsFeatureRule.setEnabled(true)

val metadata = PaymentMethodMetadataFactory.create(
stripeIntent = createValidInstantDebitsPaymentIntent().copy(
linkFundingSources = listOf("card"),
),
)

assertThat(InstantDebits.isMetBy(metadata)).isFalse()
}

private fun createValidInstantDebitsPaymentIntent(): PaymentIntent {
return PaymentIntentFactory.create(
paymentMethodTypes = listOf("card", "link"),
linkFundingSources = listOf("card", "bank_account"),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ internal class PaymentMethodMetadataTest {

@Test
fun `filterSupportedPaymentMethods removes unsupported paymentMethodTypes`() {
val metadata = PaymentMethodMetadataFactory.create()
val stripeIntent = PaymentIntentFixtures.PI_REQUIRES_PAYMENT_METHOD.copy(
paymentMethodTypes = listOf("card", "pay_now"),
)
val metadata = PaymentMethodMetadataFactory.create(stripeIntent)
val supportedPaymentMethods = metadata.supportedPaymentMethodDefinitions()
assertThat(supportedPaymentMethods).hasSize(1)
assertThat(supportedPaymentMethods.first().type.code).isEqualTo("card")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.stripe.android.PaymentConfiguration
import com.stripe.android.common.ui.BottomSheetContentTestTag
import com.stripe.android.core.Logger
import com.stripe.android.core.injection.WeakMapInjectorRegistry
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFactory
import com.stripe.android.model.PaymentIntentFixtures
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodFixtures
Expand Down Expand Up @@ -171,8 +172,15 @@ internal class PaymentOptionsActivityTest {

@Test
fun `ContinueButton should be hidden when returning to payment options`() {
val paymentMethodMetadata = PaymentMethodMetadataFactory.create(
stripeIntent = PaymentIntentFixtures.PI_REQUIRES_PAYMENT_METHOD.copy(
paymentMethodTypes = listOf("card"),
)
)

val args = PAYMENT_OPTIONS_CONTRACT_ARGS.updateState(
paymentMethods = PaymentMethodFixtures.createCards(5)
paymentMethods = PaymentMethodFixtures.createCards(5),
stripeIntent = paymentMethodMetadata.stripeIntent,
)

runActivityScenario(args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import androidx.annotation.RestrictTo
import com.stripe.android.core.BuildConfig

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
object FeatureFlags
object FeatureFlags {
val instantDebits = FeatureFlag()
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class FeatureFlag {
Expand Down

0 comments on commit c766796

Please sign in to comment.