From c3a734ac6ee3fe6993c14d2ba797ea0032302c33 Mon Sep 17 00:00:00 2001 From: Samer Alabi Date: Thu, 30 Nov 2023 13:50:35 -0500 Subject: [PATCH] Add `Bacs` support. --- payments-ui-core/res/values/strings.xml | 4 ++ payments-ui-core/src/main/assets/lpms.json | 5 ++ .../forms/PaymentMethodRequirements.kt | 9 +++ .../java/com/stripe/android/ui/core/FormUI.kt | 6 ++ .../core/elements/ContactInformationSpec.kt | 6 +- .../ui/core/forms/resources/LpmRepository.kt | 27 ++++++++ .../java/com/stripe/android/lpm/TestBacs.kt | 46 ++++++++++++++ .../android/test/core/FieldPopulator.kt | 25 ++++++++ .../android/test/core/PlaygroundTestDriver.kt | 9 ++- .../android/test/core/TestParameters.kt | 12 ++++ .../stripe/android/test/core/ui/Selectors.kt | 61 +++++++++++++++++++ .../src/test/resources/bacs_debit-support.csv | 49 +++++++++++++++ 12 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 paymentsheet-example/src/androidTest/java/com/stripe/android/lpm/TestBacs.kt create mode 100644 paymentsheet/src/test/resources/bacs_debit-support.csv diff --git a/payments-ui-core/res/values/strings.xml b/payments-ui-core/res/values/strings.xml index 2dbd7186560..80409f5d7c9 100644 --- a/payments-ui-core/res/values/strings.xml +++ b/payments-ui-core/res/values/strings.xml @@ -4,6 +4,8 @@ interest-free payments of with ]]> Back + + Full name Account number @@ -67,6 +69,8 @@ Buy using a UPI ID AU BECS Direct Debit + + Bacs Direct Debit Card diff --git a/payments-ui-core/src/main/assets/lpms.json b/payments-ui-core/src/main/assets/lpms.json index 98c09df806b..78142a765bb 100644 --- a/payments-ui-core/src/main/assets/lpms.json +++ b/payments-ui-core/src/main/assets/lpms.json @@ -755,6 +755,11 @@ } } }, + { + "type": "bacs_debit", + "async": true, + "fields": [] + }, { "type": "revolut_pay", "async": false, diff --git a/payments-ui-core/src/main/java/com/stripe/android/paymentsheet/forms/PaymentMethodRequirements.kt b/payments-ui-core/src/main/java/com/stripe/android/paymentsheet/forms/PaymentMethodRequirements.kt index cb8e5e6193c..ad652935f97 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/paymentsheet/forms/PaymentMethodRequirements.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/paymentsheet/forms/PaymentMethodRequirements.kt @@ -227,6 +227,15 @@ internal val AuBecsDebitRequirement = PaymentMethodRequirements( confirmPMFromCustomer = true ) +/** + * This defines the requirements for usage as a Payment Method. + */ +internal val BacsDebitRequirement = PaymentMethodRequirements( + piRequirements = setOf(Delayed), + siRequirements = null, + confirmPMFromCustomer = null +) + internal val ZipRequirement = PaymentMethodRequirements( piRequirements = emptySet(), siRequirements = null, diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/FormUI.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/FormUI.kt index bd892db1cc8..121d4ce64af 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/FormUI.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/FormUI.kt @@ -24,6 +24,8 @@ import com.stripe.android.ui.core.elements.SaveForFutureUseElement import com.stripe.android.ui.core.elements.SaveForFutureUseElementUI import com.stripe.android.ui.core.elements.StaticTextElement import com.stripe.android.ui.core.elements.StaticTextElementUI +import com.stripe.android.uicore.elements.CheckboxFieldElement +import com.stripe.android.uicore.elements.CheckboxFieldUI import com.stripe.android.uicore.elements.FormElement import com.stripe.android.uicore.elements.IdentifierSpec import com.stripe.android.uicore.elements.OTPElement @@ -76,6 +78,10 @@ fun FormUI( hiddenIdentifiers, lastTextFieldIdentifier ) + is CheckboxFieldElement -> CheckboxFieldUI( + controller = element.controller, + enabled = enabled + ) is StaticTextElement -> StaticTextElementUI(element) is SaveForFutureUseElement -> SaveForFutureUseElementUI(enabled, element) is AfterpayClearpayHeaderElement -> AfterpayClearpayElementUI( diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/ContactInformationSpec.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/ContactInformationSpec.kt index e746ededed8..c708f7e0ece 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/ContactInformationSpec.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/ContactInformationSpec.kt @@ -1,6 +1,7 @@ package com.stripe.android.ui.core.elements import androidx.annotation.RestrictTo +import androidx.annotation.StringRes import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import com.stripe.android.ui.core.R @@ -23,6 +24,9 @@ data class ContactInformationSpec( val collectEmail: Boolean = true, @SerialName("collect_phone") val collectPhone: Boolean = true, + @SerialName("name_label") + @StringRes + val nameLabel: Int = R.string.stripe_name_on_card ) : FormItemSpec() { override val apiPath: IdentifierSpec = IdentifierSpec() @@ -31,7 +35,7 @@ data class ContactInformationSpec( SimpleTextElement( controller = SimpleTextFieldController( textFieldConfig = SimpleTextFieldConfig( - label = R.string.stripe_name_on_card, + label = nameLabel, capitalization = KeyboardCapitalization.Words, keyboard = KeyboardType.Text ), diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/resources/LpmRepository.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/resources/LpmRepository.kt index bddc362884e..2203f954887 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/resources/LpmRepository.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/resources/LpmRepository.kt @@ -20,6 +20,7 @@ import com.stripe.android.paymentsheet.forms.AlipayRequirement import com.stripe.android.paymentsheet.forms.AlmaRequirement import com.stripe.android.paymentsheet.forms.AmazonPayRequirement import com.stripe.android.paymentsheet.forms.AuBecsDebitRequirement +import com.stripe.android.paymentsheet.forms.BacsDebitRequirement import com.stripe.android.paymentsheet.forms.BancontactRequirement import com.stripe.android.paymentsheet.forms.BlikRequirement import com.stripe.android.paymentsheet.forms.BoletoRequirement @@ -46,7 +47,10 @@ import com.stripe.android.paymentsheet.forms.UpiRequirement import com.stripe.android.paymentsheet.forms.ZipRequirement import com.stripe.android.ui.core.BillingDetailsCollectionConfiguration import com.stripe.android.ui.core.R +import com.stripe.android.ui.core.elements.AddressSpec import com.stripe.android.ui.core.elements.AfterpayClearpayHeaderElement.Companion.isClearpay +import com.stripe.android.ui.core.elements.BacsDebitBankAccountSpec +import com.stripe.android.ui.core.elements.BacsDebitConfirmSpec import com.stripe.android.ui.core.elements.CardBillingSpec import com.stripe.android.ui.core.elements.CardDetailsSectionSpec import com.stripe.android.ui.core.elements.CashAppPayMandateTextSpec @@ -468,6 +472,29 @@ class LpmRepository constructor( requirement = AuBecsDebitRequirement, formSpec = LayoutSpec(sharedDataSpec.fields) ) + PaymentMethod.Type.BacsDebit.code -> { + val localFields = listOf( + ContactInformationSpec( + collectPhone = false, + nameLabel = R.string.stripe_bacs_full_name + ), + BacsDebitBankAccountSpec(), + AddressSpec(), + BacsDebitConfirmSpec() + ) + + SupportedPaymentMethod( + code = "bacs_debit", + requiresMandate = true, + displayNameResource = R.string.stripe_paymentsheet_payment_method_bacs_debit, + iconResource = R.drawable.stripe_ic_paymentsheet_pm_bank, + lightThemeIconUrl = sharedDataSpec.selectorIcon?.lightThemePng, + darkThemeIconUrl = sharedDataSpec.selectorIcon?.darkThemePng, + tintIconOnSelection = true, + requirement = BacsDebitRequirement, + formSpec = LayoutSpec(items = sharedDataSpec.fields + localFields) + ) + } PaymentMethod.Type.USBankAccount.code -> { val pmo = stripeIntent.getPaymentMethodOptions()[PaymentMethod.Type.USBankAccount.code] val verificationMethod = (pmo as? Map<*, *>)?.get("verification_method") as? String diff --git a/paymentsheet-example/src/androidTest/java/com/stripe/android/lpm/TestBacs.kt b/paymentsheet-example/src/androidTest/java/com/stripe/android/lpm/TestBacs.kt new file mode 100644 index 00000000000..8b2fe28bfa1 --- /dev/null +++ b/paymentsheet-example/src/androidTest/java/com/stripe/android/lpm/TestBacs.kt @@ -0,0 +1,46 @@ +package com.stripe.android.lpm + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.stripe.android.BasePlaygroundTest +import com.stripe.android.paymentsheet.example.playground.settings.AutomaticPaymentMethodsSettingsDefinition +import com.stripe.android.paymentsheet.example.playground.settings.Country +import com.stripe.android.paymentsheet.example.playground.settings.CountrySettingsDefinition +import com.stripe.android.paymentsheet.example.playground.settings.Currency +import com.stripe.android.paymentsheet.example.playground.settings.CurrencySettingsDefinition +import com.stripe.android.paymentsheet.example.playground.settings.DelayedPaymentMethodsSettingsDefinition +import com.stripe.android.test.core.AuthorizeAction +import com.stripe.android.test.core.TestParameters +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +internal class TestBacs : BasePlaygroundTest() { + @Test + fun testBacsWhenConfirmed() { + testDriver.confirmNewOrGuestComplete( + testParameters = createTestParameters(AuthorizeAction.Bacs.Confirm) + ) + } + + @Test + fun testBacsWhenCancelled() { + testDriver.confirmNewOrGuestComplete( + testParameters = createTestParameters(AuthorizeAction.Bacs.ModifyDetails) + ) + } + + private fun createTestParameters( + bacsAuthAction: AuthorizeAction.Bacs + ): TestParameters { + return TestParameters.create( + paymentMethodCode = "bacs_debit", + ) { settings -> + settings[AutomaticPaymentMethodsSettingsDefinition] = true + settings[DelayedPaymentMethodsSettingsDefinition] = true + settings[CountrySettingsDefinition] = Country.GB + settings[CurrencySettingsDefinition] = Currency.GBP + }.copy( + authorizationAction = bacsAuthAction + ) + } +} diff --git a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/FieldPopulator.kt b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/FieldPopulator.kt index 91eb9920030..b26b94aa1df 100644 --- a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/FieldPopulator.kt +++ b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/FieldPopulator.kt @@ -14,6 +14,8 @@ import com.stripe.android.paymentsheet.example.playground.settings.DefaultBillin import com.stripe.android.test.core.ui.Selectors import com.stripe.android.ui.core.elements.AddressSpec import com.stripe.android.ui.core.elements.AuBankAccountNumberSpec +import com.stripe.android.ui.core.elements.BacsDebitBankAccountSpec +import com.stripe.android.ui.core.elements.BacsDebitConfirmSpec import com.stripe.android.ui.core.elements.BoletoTaxIdSpec import com.stripe.android.ui.core.elements.BsbSpec import com.stripe.android.ui.core.elements.CardBillingSpec @@ -99,6 +101,8 @@ internal class FieldPopulator( val cardCvc: String = "321", val auBecsBsbNumber: String = "000000", val auBecsAccountNumber: String = "000123456", + val bacsSortCode: String = "108800", + val bacsAccountNumber: String = "00012345", val boletoTaxId: String = "00000000000", ) @@ -156,6 +160,16 @@ internal class FieldPopulator( selectors.getAuAccountNumber() .assertContentDescriptionEquals(values.auBecsAccountNumber) } + is BacsDebitBankAccountSpec -> { + selectors.getBacsAccountNumber() + .assertContentDescriptionEquals(values.bacsAccountNumber) + selectors.getBacsSortCode() + .assertContentDescriptionEquals(values.bacsSortCode) + } + is BacsDebitConfirmSpec -> { + selectors.getBacsConfirmed() + .assertIsOn() + } is DropdownSpec -> {} is IbanSpec -> {} is KlarnaCountrySpec -> {} @@ -212,6 +226,17 @@ internal class FieldPopulator( performTextInput(values.auBecsAccountNumber) } } + is BacsDebitBankAccountSpec -> { + selectors.getBacsAccountNumber() + .performTextInput(values.bacsAccountNumber) + selectors.getBacsSortCode() + .performTextInput(values.bacsSortCode) + } + is BacsDebitConfirmSpec -> { + selectors.getBacsConfirmed() + .performScrollTo() + .performClick() + } is IbanSpec -> {} is KlarnaCountrySpec -> {} is CardBillingSpec -> { diff --git a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/PlaygroundTestDriver.kt b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/PlaygroundTestDriver.kt index c7d2a122ec7..5364b90abf8 100644 --- a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/PlaygroundTestDriver.kt +++ b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/PlaygroundTestDriver.kt @@ -770,7 +770,14 @@ internal class PlaygroundTestDriver( }.isSuccess } } - + is AuthorizeAction.Bacs.Confirm -> {} + is AuthorizeAction.Bacs.ModifyDetails -> { + buyButton.apply { + waitProcessingComplete() + isEnabled() + isDisplayed() + } + } null -> {} } } else { diff --git a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/TestParameters.kt b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/TestParameters.kt index 5bacc0ac565..e3a1e5f6adf 100644 --- a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/TestParameters.kt +++ b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/TestParameters.kt @@ -129,4 +129,16 @@ internal sealed interface AuthorizeAction { override fun text(checkoutMode: CheckoutMode): String = "" override val requiresBrowser: Boolean = true } + + sealed interface Bacs : AuthorizeAction { + object Confirm : Bacs { + override fun text(checkoutMode: CheckoutMode): String = "Confirm" + override val requiresBrowser: Boolean = false + } + + object ModifyDetails : Bacs { + override fun text(checkoutMode: CheckoutMode): String = "Modify details" + override val requiresBrowser: Boolean = false + } + } } diff --git a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/ui/Selectors.kt b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/ui/Selectors.kt index f87520ac8e3..a5681a4a6a4 100644 --- a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/ui/Selectors.kt +++ b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/ui/Selectors.kt @@ -2,7 +2,10 @@ package com.stripe.android.test.core.ui import android.content.pm.PackageManager import androidx.annotation.StringRes +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isToggleable import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText @@ -27,6 +30,7 @@ import com.stripe.android.ui.core.elements.SAVE_FOR_FUTURE_CHECKBOX_TEST_TAG import kotlin.time.Duration.Companion.seconds import com.stripe.android.R as StripeR import com.stripe.android.core.R as CoreR +import com.stripe.android.paymentsheet.R as PaymentSheetR import com.stripe.android.ui.core.R as PaymentsUiCoreR import com.stripe.android.uicore.R as UiCoreR @@ -122,6 +126,7 @@ internal class Selectors( private val checkoutMode = testParameters.playgroundSettingsSnapshot[CheckoutModeSettingsDefinition] + @OptIn(ExperimentalTestApi::class) val authorizeAction = when (testParameters.authorizationAction) { is AuthorizeAction.AuthorizePayment -> { object : UiAutomatorText( @@ -163,6 +168,50 @@ internal class Selectors( ) {} } + is AuthorizeAction.Bacs.Confirm -> { + object : UiAutomatorText( + label = testParameters.authorizationAction.text(checkoutMode), + className = "android.widget.Button", + device = device + ) { + override fun click() { + composeTestRule.waitUntilExactlyOneExists( + hasText( + getResourceString( + PaymentSheetR.string.stripe_paymentsheet_bacs_mandate_title + ) + ) + ) + + composeTestRule.onNodeWithText( + getResourceString(PaymentSheetR.string.stripe_paymentsheet_confirm) + ).performClick() + } + } + } + + is AuthorizeAction.Bacs.ModifyDetails -> { + object : UiAutomatorText( + label = testParameters.authorizationAction.text(checkoutMode), + className = "android.widget.Button", + device = device + ) { + override fun click() { + composeTestRule.waitUntilExactlyOneExists( + hasText(getResourceString(PaymentSheetR.string.stripe_paymentsheet_bacs_mandate_title)) + ) + + composeTestRule.onNodeWithText( + getResourceString( + PaymentSheetR + .string + .stripe_paymentsheet_bacs_modify_details_button_label + ) + ).performClick() + } + } + } + else -> null } @@ -213,6 +262,18 @@ internal class Selectors( getResourceString(StripeR.string.stripe_becs_widget_account_number) ) + fun getBacsSortCode() = composeTestRule.onNodeWithText( + getResourceString(PaymentsUiCoreR.string.stripe_bacs_sort_code) + ) + + fun getBacsAccountNumber() = composeTestRule.onNodeWithText( + getResourceString(StripeR.string.stripe_becs_widget_account_number) + ) + + fun getBacsConfirmed() = composeTestRule.onNode( + isToggleable().and(hasTestTag("BACS_MANDATE_CHECKBOX")) + ) + fun getBoletoTaxId() = composeTestRule.onNodeWithText( getResourceString(PaymentsUiCoreR.string.stripe_boleto_tax_id_label) ) diff --git a/paymentsheet/src/test/resources/bacs_debit-support.csv b/paymentsheet/src/test/resources/bacs_debit-support.csv new file mode 100644 index 00000000000..24b2e01fb76 --- /dev/null +++ b/paymentsheet/src/test/resources/bacs_debit-support.csv @@ -0,0 +1,49 @@ +lpm, hasCustomer, allowsDelayedPayment, intentSetupFutureUsage, intentHasShipping, intentLpms, supportCustomerSavedCard, formExists, formType, supportsAdding +bacs_debit, true, true, off_session, false, card/bacs_debit, false, false, not available, false +bacs_debit, true, true, off_session, false, card/eps/bacs_debit, false, false, not available, false +bacs_debit, true, false, off_session, false, card/bacs_debit, false, false, not available, false +bacs_debit, true, false, off_session, false, card/eps/bacs_debit, false, false, not available, false +bacs_debit, true, true, on_session, false, card/bacs_debit, false, false, not available, false +bacs_debit, true, true, on_session, false, card/eps/bacs_debit, false, false, not available, false +bacs_debit, true, false, on_session, false, card/bacs_debit, false, false, not available, false +bacs_debit, true, false, on_session, false, card/eps/bacs_debit, false, false, not available, false +bacs_debit, true, true, null, false, card/bacs_debit, false, true, oneTime, true +bacs_debit, true, true, null, false, card/eps/bacs_debit, false, true, oneTime, true +bacs_debit, true, false, null, false, card/bacs_debit, false, false, not available, false +bacs_debit, true, false, null, false, card/eps/bacs_debit, false, false, not available, false +bacs_debit, false, true, off_session, false, card/bacs_debit, false, false, not available, false +bacs_debit, false, true, off_session, false, card/eps/bacs_debit, false, false, not available, false +bacs_debit, false, false, off_session, false, card/bacs_debit, false, false, not available, false +bacs_debit, false, false, off_session, false, card/eps/bacs_debit, false, false, not available, false +bacs_debit, false, true, on_session, false, card/bacs_debit, false, false, not available, false +bacs_debit, false, true, on_session, false, card/eps/bacs_debit, false, false, not available, false +bacs_debit, false, false, on_session, false, card/bacs_debit, false, false, not available, false +bacs_debit, false, false, on_session, false, card/eps/bacs_debit, false, false, not available, false +bacs_debit, false, true, null, false, card/bacs_debit, false, true, oneTime, true +bacs_debit, false, true, null, false, card/eps/bacs_debit, false, true, oneTime, true +bacs_debit, false, false, null, false, card/bacs_debit, false, false, not available, false +bacs_debit, false, false, null, false, card/eps/bacs_debit, false, false, not available, false +bacs_debit, true, true, off_session, true, card/bacs_debit, false, false, not available, false +bacs_debit, true, true, off_session, true, card/eps/bacs_debit, false, false, not available, false +bacs_debit, true, false, off_session, true, card/bacs_debit, false, false, not available, false +bacs_debit, true, false, off_session, true, card/eps/bacs_debit, false, false, not available, false +bacs_debit, true, true, on_session, true, card/bacs_debit, false, false, not available, false +bacs_debit, true, true, on_session, true, card/eps/bacs_debit, false, false, not available, false +bacs_debit, true, false, on_session, true, card/bacs_debit, false, false, not available, false +bacs_debit, true, false, on_session, true, card/eps/bacs_debit, false, false, not available, false +bacs_debit, true, true, null, true, card/bacs_debit, false, true, oneTime, true +bacs_debit, true, true, null, true, card/eps/bacs_debit, false, true, oneTime, true +bacs_debit, true, false, null, true, card/bacs_debit, false, false, not available, false +bacs_debit, true, false, null, true, card/eps/bacs_debit, false, false, not available, false +bacs_debit, false, true, off_session, true, card/bacs_debit, false, false, not available, false +bacs_debit, false, true, off_session, true, card/eps/bacs_debit, false, false, not available, false +bacs_debit, false, false, off_session, true, card/bacs_debit, false, false, not available, false +bacs_debit, false, false, off_session, true, card/eps/bacs_debit, false, false, not available, false +bacs_debit, false, true, on_session, true, card/bacs_debit, false, false, not available, false +bacs_debit, false, true, on_session, true, card/eps/bacs_debit, false, false, not available, false +bacs_debit, false, false, on_session, true, card/bacs_debit, false, false, not available, false +bacs_debit, false, false, on_session, true, card/eps/bacs_debit, false, false, not available, false +bacs_debit, false, true, null, true, card/bacs_debit, false, true, oneTime, true +bacs_debit, false, true, null, true, card/eps/bacs_debit, false, true, oneTime, true +bacs_debit, false, false, null, true, card/bacs_debit, false, false, not available, false +bacs_debit, false, false, null, true, card/eps/bacs_debit, false, false, not available, false \ No newline at end of file