diff --git a/assets/images/bank-debit.svg b/assets/images/bank-debit.svg new file mode 100644 index 0000000000..dc61f40270 --- /dev/null +++ b/assets/images/bank-debit.svg @@ -0,0 +1 @@ + diff --git a/client/payment-method-icons/bank-debit/icon.svg b/client/payment-method-icons/bank-debit/icon.svg new file mode 100644 index 0000000000..01d68b796f --- /dev/null +++ b/client/payment-method-icons/bank-debit/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/payment-method-icons/bank-debit/index.js b/client/payment-method-icons/bank-debit/index.js new file mode 100644 index 0000000000..bee7ba6dc4 --- /dev/null +++ b/client/payment-method-icons/bank-debit/index.js @@ -0,0 +1,7 @@ +import React from 'react'; +import IconWithShell from '../styles/icon-with-shell'; +import icon from './icon.svg'; + +const BankDebitIcon = ( props ) => ; + +export default BankDebitIcon; diff --git a/client/payment-method-icons/index.js b/client/payment-method-icons/index.js index c1ad9d7bac..7e032411de 100644 --- a/client/payment-method-icons/index.js +++ b/client/payment-method-icons/index.js @@ -16,6 +16,7 @@ import BoletoIcon from './boleto'; import OxxoIcon from './oxxo'; import WechatPayIcon from './wechat-pay'; import CashAppIcon from './cashapp'; +import BankDebitIcon from './bank-debit'; export default { alipay: AlipayIcon, @@ -36,5 +37,6 @@ export default { oxxo: OxxoIcon, wechat_pay: WechatPayIcon, cashapp: CashAppIcon, - bacs_debit: CreditCardIcon, + us_bank_account: BankDebitIcon, + bacs_debit: BankDebitIcon, }; diff --git a/client/payment-methods-map.js b/client/payment-methods-map.js index 20255dd485..ed1bf03712 100644 --- a/client/payment-methods-map.js +++ b/client/payment-methods-map.js @@ -3,6 +3,7 @@ import icons from './payment-method-icons'; const accountCountry = window.wc_stripe_settings_params?.account_country || 'US'; +const isAchEnabled = window.wc_stripe_settings_params?.is_ach_enabled === '1'; const paymentMethodsMap = { card: { @@ -241,6 +242,19 @@ const paymentMethodsMap = { }, }; +if ( isAchEnabled ) { + paymentMethodsMap.us_bank_account = { + id: 'us_bank_account', + label: __( 'ACH Direct Debit', 'woocommerce-gateway-stripe' ), + description: __( + 'ACH lets you accept payments from customers with a US bank account.', + 'woocommerce-gateway-stripe' + ), + Icon: icons.us_bank_account, + currencies: [ 'USD' ], + }; +} + // Enable Bacs according to feature flag value if ( window.wc_stripe_settings_params?.is_bacs_enabled ) { paymentMethodsMap.bacs_debit = { diff --git a/client/stripe-utils/constants.js b/client/stripe-utils/constants.js index 02f7de36b9..59fa58d9dd 100644 --- a/client/stripe-utils/constants.js +++ b/client/stripe-utils/constants.js @@ -26,6 +26,7 @@ export const PAYMENT_METHOD_LINK = 'link'; * Payment method names constants with the `stripe` prefix */ export const PAYMENT_METHOD_STRIPE_CARD = 'stripe'; +export const PAYMENT_METHOD_STRIPE_ACH = 'stripe_us_bank_account'; export const PAYMENT_METHOD_STRIPE_GIROPAY = 'stripe_giropay'; export const PAYMENT_METHOD_STRIPE_EPS = 'stripe_eps'; export const PAYMENT_METHOD_STRIPE_IDEAL = 'stripe_ideal'; @@ -48,6 +49,7 @@ export const PAYMENT_METHOD_STRIPE_BACS = 'stripe_bacs_debit'; export function getPaymentMethodsConstants() { return { card: PAYMENT_METHOD_STRIPE_CARD, + us_bank_account: PAYMENT_METHOD_STRIPE_ACH, giropay: PAYMENT_METHOD_STRIPE_GIROPAY, eps: PAYMENT_METHOD_STRIPE_EPS, ideal: PAYMENT_METHOD_STRIPE_IDEAL, diff --git a/includes/abstracts/abstract-wc-stripe-payment-gateway.php b/includes/abstracts/abstract-wc-stripe-payment-gateway.php index 65fc00490a..bca889a540 100644 --- a/includes/abstracts/abstract-wc-stripe-payment-gateway.php +++ b/includes/abstracts/abstract-wc-stripe-payment-gateway.php @@ -337,6 +337,7 @@ public function payment_icons() { return apply_filters( 'wc_stripe_payment_icons', [ + WC_Stripe_Payment_Methods::ACH => 'ACH', WC_Stripe_Payment_Methods::ALIPAY => 'Alipay', WC_Stripe_Payment_Methods::WECHAT_PAY => 'Wechat Pay', WC_Stripe_Payment_Methods::BANCONTACT => 'Bancontact', diff --git a/includes/class-wc-stripe-intent-controller.php b/includes/class-wc-stripe-intent-controller.php index 756a732b5f..5a53fab60d 100644 --- a/includes/class-wc-stripe-intent-controller.php +++ b/includes/class-wc-stripe-intent-controller.php @@ -967,6 +967,7 @@ private function build_base_payment_intent_request_params( $payment_information */ public function is_mandate_data_required( $selected_payment_type, $is_using_saved_payment_method = false ) { $payment_methods_with_mandates = [ + WC_Stripe_Payment_Methods::ACH, WC_Stripe_Payment_Methods::BACS_DEBIT, WC_Stripe_Payment_Methods::SEPA_DEBIT, WC_Stripe_Payment_Methods::BANCONTACT, diff --git a/includes/constants/class-wc-stripe-payment-methods.php b/includes/constants/class-wc-stripe-payment-methods.php index 66c1a79dbc..c7617cc519 100644 --- a/includes/constants/class-wc-stripe-payment-methods.php +++ b/includes/constants/class-wc-stripe-payment-methods.php @@ -4,6 +4,8 @@ * Class WC_Stripe_Payment_Methods */ class WC_Stripe_Payment_Methods { + + const ACH = 'us_bank_account'; const AFFIRM = 'affirm'; const AFTERPAY_CLEARPAY = 'afterpay_clearpay'; const ALIPAY = 'alipay'; diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index e7ecac4060..a1783d12a0 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -22,6 +22,7 @@ class WC_Stripe_UPE_Payment_Gateway extends WC_Gateway_Stripe { */ const UPE_AVAILABLE_METHODS = [ WC_Stripe_UPE_Payment_Method_CC::class, + WC_Stripe_UPE_Payment_Method_ACH::class, WC_Stripe_UPE_Payment_Method_Alipay::class, WC_Stripe_UPE_Payment_Method_Giropay::class, WC_Stripe_UPE_Payment_Method_Klarna::class, @@ -155,6 +156,11 @@ public function __construct() { foreach ( self::UPE_AVAILABLE_METHODS as $payment_method_class ) { + // Show ACH only if feature is enabled. + if ( WC_Stripe_UPE_Payment_Method_ACH::class === $payment_method_class && ! WC_Stripe_Feature_Flags::is_ach_lpm_enabled() ) { + continue; + } + /** Show Sofort if it's already enabled. Hide from the new merchants and keep it for the old ones who are already using this gateway, until we remove it completely. * Stripe is deprecating Sofort https://support.stripe.com/questions/sofort-is-being-deprecated-as-a-standalone-payment-method. */ @@ -2156,6 +2162,10 @@ protected function prepare_payment_information_from_request( WC_Order $order ) { 'has_subscription' => $this->has_subscription( $order->get_id() ), ]; + if ( 'us_bank_account' === $selected_payment_type ) { + WC_Stripe_API::attach_payment_method_to_customer( $payment_information['customer'], $payment_method_id ); + } + if ( ! empty( $payment_method_id ) ) { $payment_method_details = WC_Stripe_API::get_payment_method( $payment_method_id ); $payment_information['payment_method'] = $payment_method_id; @@ -2167,7 +2177,7 @@ protected function prepare_payment_information_from_request( WC_Order $order ) { $order, $payment_method_details ); - $payment_information['capture_method'] = $capture_method; + $payment_information['capture_method'] = $capture_method; } else { $confirmation_token_id = sanitize_text_field( wp_unslash( $_POST['wc-stripe-confirmation-token'] ?? '' ) ); $payment_information['confirmation_token'] = $confirmation_token_id; diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-ach.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-ach.php new file mode 100644 index 0000000000..f4079cb9a0 --- /dev/null +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-ach.php @@ -0,0 +1,49 @@ +stripe_id = self::STRIPE_ID; + $this->title = __( 'ACH Direct Debit', 'woocommerce-gateway-stripe' ); + $this->is_reusable = false; // Usually ACH requires verification per transaction. + $this->supported_currencies = [ 'USD' ]; + $this->supported_countries = [ 'US' ]; + $this->label = __( 'ACH Direct Debit', 'woocommerce-gateway-stripe' ); + $this->description = __( 'Pay directly from your US bank account via ACH.', 'woocommerce-gateway-stripe' ); + } + + /** + * Checks if ACH is available for the Stripe account's country. + * + * @return bool True if US-based account; false otherwise. + */ + public function is_available_for_account_country() { + return in_array( WC_Stripe::get_instance()->account->get_account_country(), $this->supported_countries, true ); + } + + /** + * Returns string representing payment method type + * to query to retrieve saved payment methods from Stripe. + */ + public function get_retrievable_type() { + return $this->get_id(); + } +} diff --git a/tests/phpunit/test-class-wc-stripe-upe-payment-gateway.php b/tests/phpunit/test-class-wc-stripe-upe-payment-gateway.php index 2165d05f8d..c0e9223cad 100644 --- a/tests/phpunit/test-class-wc-stripe-upe-payment-gateway.php +++ b/tests/phpunit/test-class-wc-stripe-upe-payment-gateway.php @@ -117,6 +117,8 @@ class WC_Stripe_UPE_Payment_Gateway_Test extends WP_UnitTestCase { public function set_up() { parent::set_up(); + update_option( WC_Stripe_Feature_Flags::LPM_ACH_FEATURE_FLAG_NAME, 'yes' ); + $mock_account = $this->getMockBuilder( 'WC_Stripe_Account' ) ->disableOriginalConstructor() ->getMock(); @@ -181,6 +183,11 @@ public function set_up() { ); } + public function tear_down() { + parent::tear_down(); + delete_option( WC_Stripe_Feature_Flags::LPM_ACH_FEATURE_FLAG_NAME ); + } + /** * Helper function to set $_POST vars for saved payment method. */ @@ -258,6 +265,7 @@ public function get_upe_available_payment_methods_provider() { 'US', [ WC_Stripe_UPE_Payment_Method_CC::STRIPE_ID, + WC_Stripe_UPE_Payment_Method_ACH::STRIPE_ID, WC_Stripe_UPE_Payment_Method_Alipay::STRIPE_ID, WC_Stripe_UPE_Payment_Method_Klarna::STRIPE_ID, WC_Stripe_UPE_Payment_Method_Affirm::STRIPE_ID, diff --git a/tests/phpunit/test-class-wc-stripe-upe-payment-method.php b/tests/phpunit/test-class-wc-stripe-upe-payment-method.php index c5c0d30de4..a8f2818fed 100644 --- a/tests/phpunit/test-class-wc-stripe-upe-payment-method.php +++ b/tests/phpunit/test-class-wc-stripe-upe-payment-method.php @@ -73,49 +73,51 @@ class WC_Stripe_UPE_Payment_Method_Test extends WP_UnitTestCase { * Mock capabilities object from Stripe response--all inactive. */ const MOCK_INACTIVE_CAPABILITIES_RESPONSE = [ - 'alipay_payments' => 'inactive', - 'bancontact_payments' => 'inactive', - 'card_payments' => 'inactive', - 'eps_payments' => 'inactive', - 'giropay_payments' => 'inactive', - 'klarna_payments' => 'inactive', - 'affirm_payments' => 'inactive', - 'clearpay_afterpay_payments' => 'inactive', - 'ideal_payments' => 'inactive', - 'p24_payments' => 'inactive', - 'sepa_debit_payments' => 'inactive', - 'sofort_payments' => 'inactive', - 'transfers' => 'inactive', - 'multibanco_payments' => 'inactive', - 'boleto_payments' => 'inactive', - 'oxxo_payments' => 'inactive', - 'link_payments' => 'inactive', - 'wechat_pay_payments' => 'inactive', + 'alipay_payments' => 'inactive', + 'bancontact_payments' => 'inactive', + 'card_payments' => 'inactive', + 'eps_payments' => 'inactive', + 'giropay_payments' => 'inactive', + 'klarna_payments' => 'inactive', + 'affirm_payments' => 'inactive', + 'clearpay_afterpay_payments' => 'inactive', + 'ideal_payments' => 'inactive', + 'p24_payments' => 'inactive', + 'sepa_debit_payments' => 'inactive', + 'sofort_payments' => 'inactive', + 'transfers' => 'inactive', + 'multibanco_payments' => 'inactive', + 'boleto_payments' => 'inactive', + 'oxxo_payments' => 'inactive', + 'link_payments' => 'inactive', + 'wechat_pay_payments' => 'inactive', + 'us_bank_account_ach_payments' => 'inactive', ]; /** * Mock capabilities object from Stripe response--all active. */ const MOCK_ACTIVE_CAPABILITIES_RESPONSE = [ - 'alipay_payments' => 'active', - 'bancontact_payments' => 'active', - 'card_payments' => 'active', - 'eps_payments' => 'active', - 'giropay_payments' => 'active', - 'klarna_payments' => 'active', - 'affirm_payments' => 'active', - 'clearpay_afterpay_payments' => 'active', - 'ideal_payments' => 'active', - 'p24_payments' => 'active', - 'sepa_debit_payments' => 'active', - 'sofort_payments' => 'active', - 'transfers' => 'active', - 'multibanco_payments' => 'active', - 'boleto_payments' => 'active', - 'oxxo_payments' => 'active', - 'link_payments' => 'active', - 'cashapp_payments' => 'active', - 'wechat_pay_payments' => 'active', + 'alipay_payments' => 'active', + 'bancontact_payments' => 'active', + 'card_payments' => 'active', + 'eps_payments' => 'active', + 'giropay_payments' => 'active', + 'klarna_payments' => 'active', + 'affirm_payments' => 'active', + 'clearpay_afterpay_payments' => 'active', + 'ideal_payments' => 'active', + 'p24_payments' => 'active', + 'sepa_debit_payments' => 'active', + 'sofort_payments' => 'active', + 'transfers' => 'active', + 'multibanco_payments' => 'active', + 'boleto_payments' => 'active', + 'oxxo_payments' => 'active', + 'link_payments' => 'active', + 'cashapp_payments' => 'active', + 'wechat_pay_payments' => 'active', + 'us_bank_account_ach_payments' => 'active', ]; /** @@ -245,6 +247,7 @@ public function test_payment_methods_show_correct_default_outputs() { $boleto_method = $this->mock_payment_methods['boleto']; $oxxo_method = $this->mock_payment_methods['oxxo']; $wechat_pay_method = $this->mock_payment_methods['wechat_pay']; + $ach_method = $this->mock_payment_methods['us_bank_account']; $this->assertEquals( WC_Stripe_Payment_Methods::CARD, $card_method->get_id() ); $this->assertEquals( 'Credit / Debit Card', $card_method->get_label() ); @@ -345,6 +348,13 @@ public function test_payment_methods_show_correct_default_outputs() { $this->assertFalse( $wechat_pay_method->is_reusable() ); $this->assertEquals( WC_Stripe_Payment_Methods::WECHAT_PAY, $wechat_pay_method->get_retrievable_type() ); $this->assertEquals( '', $wechat_pay_method->get_testing_instructions() ); + + $this->assertEquals( WC_Stripe_Payment_Methods::ACH, $ach_method->get_id() ); + $this->assertEquals( 'ACH Direct Debit', $ach_method->get_label() ); + $this->assertEquals( 'ACH Direct Debit', $ach_method->get_title() ); + $this->assertFalse( $ach_method->is_reusable() ); // Currently non-reusable; future improvement may change this. + $this->assertEquals( WC_Stripe_Payment_Methods::ACH, $ach_method->get_retrievable_type() ); + $this->assertEquals( '', $ach_method->get_testing_instructions() ); } /** @@ -375,6 +385,7 @@ public function test_card_payment_method_capability_is_always_enabled() { $multibanco_method = $this->mock_payment_methods['multibanco']; $oxxo_method = $this->mock_payment_methods['oxxo']; $wechat_pay_method = $this->mock_payment_methods['wechat_pay']; + $ach_method = $this->mock_payment_methods['us_bank_account']; $this->assertTrue( $card_method->is_enabled_at_checkout() ); $this->assertFalse( $klarna_method->is_enabled_at_checkout() ); @@ -390,6 +401,7 @@ public function test_card_payment_method_capability_is_always_enabled() { $this->assertFalse( $multibanco_method->is_enabled_at_checkout() ); $this->assertFalse( $oxxo_method->is_enabled_at_checkout() ); $this->assertFalse( $wechat_pay_method->is_enabled_at_checkout() ); + $this->assertFalse( $ach_method->is_enabled_at_checkout() ); } /** @@ -605,6 +617,7 @@ public function test_payment_methods_are_reusable_if_cart_contains_subscription( public function test_payment_methods_support_custom_name_and_description() { $payment_method_ids = [ + WC_Stripe_Payment_Methods::ACH, WC_Stripe_Payment_Methods::CARD, WC_Stripe_Payment_Methods::KLARNA, WC_Stripe_Payment_Methods::AFTERPAY_CLEARPAY, diff --git a/woocommerce-gateway-stripe.php b/woocommerce-gateway-stripe.php index 1d53409a88..f7ce5e299c 100644 --- a/woocommerce-gateway-stripe.php +++ b/woocommerce-gateway-stripe.php @@ -222,6 +222,7 @@ public function init() { require_once __DIR__ . '/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php'; require_once __DIR__ . '/includes/payment-methods/class-wc-stripe-upe-payment-method.php'; require_once __DIR__ . '/includes/payment-methods/class-wc-stripe-upe-payment-method-cc.php'; + require_once __DIR__ . '/includes/payment-methods/class-wc-stripe-upe-payment-method-ach.php'; require_once __DIR__ . '/includes/payment-methods/class-wc-stripe-upe-payment-method-alipay.php'; require_once __DIR__ . '/includes/payment-methods/class-wc-stripe-upe-payment-method-bacs.php'; require_once __DIR__ . '/includes/payment-methods/class-wc-stripe-upe-payment-method-giropay.php';