diff --git a/client/api/index.js b/client/api/index.js
index e76f7010c..65b729bc8 100644
--- a/client/api/index.js
+++ b/client/api/index.js
@@ -137,13 +137,15 @@ export default class WCStripeAPI {
/**
* Creates an intent based on a payment method.
*
- * @param {number} orderId The id of the order if creating the intent on Order Pay page.
+ * @param {number|null} orderId The id of the order if creating the intent on Order Pay page.
+ * @param {string|null} paymentMethodType The type of payment method.
*
* @return {Promise} The final promise for the request to the server.
*/
- createIntent( orderId ) {
+ createIntent( orderId = null, paymentMethodType = null ) {
return this.request( this.getAjaxUrl( 'create_payment_intent' ), {
stripe_order_id: orderId,
+ payment_method_type: paymentMethodType,
_ajax_nonce: this.options?.createPaymentIntentNonce,
} )
.then( ( response ) => {
diff --git a/client/classic/upe/deferred-intent.js b/client/classic/upe/deferred-intent.js
index c222796e5..3ae3830f6 100644
--- a/client/classic/upe/deferred-intent.js
+++ b/client/classic/upe/deferred-intent.js
@@ -6,6 +6,7 @@ import {
getStripeServerData,
isPaymentMethodRestrictedToLocation,
isUsingSavedPaymentMethod,
+ paymentMethodSupportsDeferredIntent,
togglePaymentMethodForCountry,
} from '../../stripe-utils';
import './style.scss';
@@ -54,6 +55,11 @@ jQuery( function ( $ ) {
maybeMountStripePaymentElement();
}
+ // For payment methods that don't support deferred intents, we mount the Payment Element only when it's selected.
+ $( 'form.checkout' ).on( 'change', 'input[name="payment_method"]', () => {
+ maybeMountStripePaymentElement();
+ } );
+
// My Account > Payment Methods page submit.
$( 'form#add_payment_method' ).on( 'submit', function () {
return processPayment(
@@ -76,20 +82,32 @@ jQuery( function ( $ ) {
}
} );
- // If the card element selector doesn't exist, then do nothing.
- // For example, when a 100% discount coupon is applied).
- // We also don't re-mount if already mounted in DOM.
async function maybeMountStripePaymentElement() {
- if (
- $( '.wc-stripe-upe-element' ).length &&
- ! $( '.wc-stripe-upe-element' ).children().length
- ) {
- for ( const upeElement of $(
- '.wc-stripe-upe-element'
- ).toArray() ) {
- await mountStripePaymentElement( api, upeElement );
- restrictPaymentMethodToLocation( upeElement );
+ // If the card element selector doesn't exist, do nothing.
+ // For example, when a 100% discount coupon is applied.
+ if ( ! $( '.wc-stripe-upe-element' ).length ) {
+ return;
+ }
+
+ const selectedMethod = getSelectedUPEGatewayPaymentMethod();
+ for ( const upeElement of $( '.wc-stripe-upe-element' ).toArray() ) {
+ // Maybe hide the payment method based on the billing country.
+ restrictPaymentMethodToLocation( upeElement );
+
+ // Don't mount if it's already mounted.
+ if ( $( upeElement ).children().length ) {
+ continue;
+ }
+
+ // Payment methods that don't support deferred intents don't need to be mounted unless they are selected.
+ if (
+ upeElement.dataset.paymentMethodType !== selectedMethod &&
+ ! paymentMethodSupportsDeferredIntent( upeElement )
+ ) {
+ continue;
}
+
+ await mountStripePaymentElement( api, upeElement );
}
}
@@ -97,7 +115,7 @@ jQuery( function ( $ ) {
if ( isPaymentMethodRestrictedToLocation( upeElement ) ) {
togglePaymentMethodForCountry( upeElement );
- // this event only applies to the checkout form, but not "place order" or "add payment method" pages.
+ // This event only applies to the checkout form, but not "pay for order" or "add payment method" pages.
$( '#billing_country' ).on( 'change', function () {
togglePaymentMethodForCountry( upeElement );
} );
diff --git a/client/classic/upe/payment-processing.js b/client/classic/upe/payment-processing.js
index 8a6d473c3..b54b3a2e4 100644
--- a/client/classic/upe/payment-processing.js
+++ b/client/classic/upe/payment-processing.js
@@ -1,6 +1,7 @@
import { __, sprintf } from '@wordpress/i18n';
import {
appendPaymentMethodIdToForm,
+ appendPaymentIntentIdToForm,
getPaymentMethodTypes,
initializeUPEAppearance,
isLinkEnabled,
@@ -24,11 +25,11 @@ import {
} from 'wcstripe/stripe-utils/constants';
const gatewayUPEComponents = {};
-
const paymentMethodsConfig = getStripeServerData()?.paymentMethodsConfig;
for ( const paymentMethodType in paymentMethodsConfig ) {
gatewayUPEComponents[ paymentMethodType ] = {
+ intentId: null,
elements: null,
upeElement: null,
};
@@ -66,28 +67,46 @@ export function validateElements( elements ) {
}
/**
- * Creates a Stripe payment element with the specified payment method type and options. The function
- * retrieves the necessary data from the UPE configuration and initializes the appearance. It then creates the
- * Stripe elements and the Stripe payment element, which is attached to the gatewayUPEComponents object afterward.
+ * Creates a Stripe payment element with the specified payment method type and options.
+ *
+ * If the payment method doesn't support deferred intent, the intent must be created first.
+ * Then, the payment element is created with the intent's client secret.
*
- * @todo Make paymentMethodType required when Split is implemented.
+ * Finally, the payment element is mounted and attached to the gatewayUPEComponents object.
*
* @param {Object} api The API object used to create the Stripe payment element.
* @param {string} paymentMethodType The type of Stripe payment method to create.
* @return {Object} A promise that resolves with the created Stripe payment element.
*/
-function createStripePaymentElement( api, paymentMethodType = null ) {
+async function createStripePaymentElement( api, paymentMethodType ) {
const amount = Number( getStripeServerData()?.cartTotal );
const paymentMethodTypes = getPaymentMethodTypes( paymentMethodType );
- const options = {
- mode: amount < 1 ? 'setup' : 'payment',
- currency: getStripeServerData()?.currency.toLowerCase(),
- amount,
- paymentMethodCreation: 'manual',
- paymentMethodTypes,
- appearance: initializeUPEAppearance( api ),
- fonts: getFontRulesFromPage(),
- };
+ const { supportsDeferredIntent } =
+ paymentMethodsConfig[ paymentMethodType ] || {};
+ let options;
+
+ // If the payment method doesn't support deferred intent, the intent must be created here.
+ if ( ! supportsDeferredIntent ) {
+ const intent = await api.createIntent( null, paymentMethodType );
+ gatewayUPEComponents[ paymentMethodType ].intentId = intent.id;
+
+ options = {
+ appearance: initializeUPEAppearance( api ),
+ paymentMethodCreation: 'manual',
+ fonts: getFontRulesFromPage(),
+ clientSecret: intent.client_secret,
+ };
+ } else {
+ options = {
+ mode: amount < 1 ? 'setup' : 'payment',
+ currency: getStripeServerData()?.currency.toLowerCase(),
+ amount,
+ paymentMethodCreation: 'manual',
+ paymentMethodTypes,
+ appearance: initializeUPEAppearance( api ),
+ fonts: getFontRulesFromPage(),
+ };
+ }
const elements = api.getStripe().elements( options );
@@ -333,6 +352,14 @@ export const processPayment = (
paymentMethodObject.paymentMethod.id
);
+ // Append the intent ID to the form if it was previously created through a non-deferred intent.
+ if ( gatewayUPEComponents[ paymentMethodType ].intentId ) {
+ appendPaymentIntentIdToForm(
+ jQueryForm,
+ gatewayUPEComponents[ paymentMethodType ].intentId
+ );
+ }
+
let stopFormSubmission = false;
await additionalActionsHandler(
paymentMethodObject.paymentMethod,
diff --git a/client/payment-method-icons/index.js b/client/payment-method-icons/index.js
index 7e032411d..c533bd1c5 100644
--- a/client/payment-method-icons/index.js
+++ b/client/payment-method-icons/index.js
@@ -39,4 +39,5 @@ export default {
cashapp: CashAppIcon,
us_bank_account: BankDebitIcon,
bacs_debit: BankDebitIcon,
+ acss_debit: BankDebitIcon,
};
diff --git a/client/payment-methods-map.js b/client/payment-methods-map.js
index ed1bf0371..d21b81257 100644
--- a/client/payment-methods-map.js
+++ b/client/payment-methods-map.js
@@ -4,6 +4,8 @@ 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 isAcssEnabled = window.wc_stripe_settings_params?.is_acss_enabled === '1';
+const isBacsEnabled = window.wc_stripe_settings_params?.is_bacs_enabled === '1';
const paymentMethodsMap = {
card: {
@@ -242,6 +244,7 @@ const paymentMethodsMap = {
},
};
+// Enable ACH according to feature flag value.
if ( isAchEnabled ) {
paymentMethodsMap.us_bank_account = {
id: 'us_bank_account',
@@ -255,8 +258,22 @@ if ( isAchEnabled ) {
};
}
-// Enable Bacs according to feature flag value
-if ( window.wc_stripe_settings_params?.is_bacs_enabled ) {
+// Enable ACSS according to feature flag value.
+if ( isAcssEnabled ) {
+ paymentMethodsMap.acss_debit = {
+ id: 'acss_debit',
+ label: __( 'Pre-Authorized Debit', 'woocommerce-gateway-stripe' ),
+ description: __(
+ 'Canadian Pre-Authorized Debit is a payment method that allows customers to pay using their Canadian bank account.',
+ 'woocommerce-gateway-stripe'
+ ),
+ Icon: icons.acss_debit,
+ currencies: [ 'CAD' ],
+ };
+}
+
+// Enable Bacs according to feature flag value.
+if ( isBacsEnabled ) {
paymentMethodsMap.bacs_debit = {
id: 'bacs_debit',
label: 'Bacs Direct Debit',
diff --git a/client/stripe-utils/constants.js b/client/stripe-utils/constants.js
index 4b1f91195..26af9e484 100644
--- a/client/stripe-utils/constants.js
+++ b/client/stripe-utils/constants.js
@@ -45,6 +45,7 @@ export const PAYMENT_METHOD_STRIPE_AFTERPAY_CLEARPAY =
'stripe_afterpay_clearpay';
export const PAYMENT_METHOD_STRIPE_WECHAT_PAY = 'stripe_wechat_pay';
export const PAYMENT_METHOD_STRIPE_CASHAPP = 'stripe_cashapp';
+export const PAYMENT_METHOD_STRIPE_ACSS = 'stripe_acss_debit';
export const PAYMENT_METHOD_STRIPE_BACS = 'stripe_bacs_debit';
export function getPaymentMethodsConstants() {
@@ -67,6 +68,7 @@ export function getPaymentMethodsConstants() {
afterpay_clearpay: PAYMENT_METHOD_STRIPE_AFTERPAY_CLEARPAY,
wechat_pay: PAYMENT_METHOD_STRIPE_WECHAT_PAY,
cashapp: PAYMENT_METHOD_STRIPE_CASHAPP,
+ acss_debit: PAYMENT_METHOD_STRIPE_ACSS,
bacs_debit: PAYMENT_METHOD_STRIPE_BACS,
};
}
diff --git a/client/stripe-utils/utils.js b/client/stripe-utils/utils.js
index a2014433e..6d4cb36d2 100644
--- a/client/stripe-utils/utils.js
+++ b/client/stripe-utils/utils.js
@@ -309,6 +309,12 @@ export const appendPaymentMethodIdToForm = ( form, paymentMethodId ) => {
);
};
+export const appendPaymentIntentIdToForm = ( form, paymentIntentId ) => {
+ form.append(
+ ``
+ );
+};
+
export const appendSetupIntentToForm = ( form, setupIntent ) => {
form.append(
``
@@ -554,7 +560,7 @@ export const getPaymentMethodName = ( paymentMethodType ) => {
*
* @param {Object} upeElement The selector of the DOM element of particular payment method to mount the UPE element to.
* @return {boolean} Whether the payment method is restricted to selected billing country.
- **/
+ */
export const isPaymentMethodRestrictedToLocation = ( upeElement ) => {
const paymentMethodsConfig =
getStripeServerData()?.paymentMethodsConfig || {};
@@ -563,8 +569,21 @@ export const isPaymentMethodRestrictedToLocation = ( upeElement ) => {
};
/**
+ * Determines if the payment method supports deferred intent.
+ *
* @param {Object} upeElement The selector of the DOM element of particular payment method to mount the UPE element to.
- **/
+ * @return {boolean} Whether the payment method supports deferred intent.
+ */
+export const paymentMethodSupportsDeferredIntent = ( upeElement ) => {
+ const paymentMethodsConfig =
+ getStripeServerData()?.paymentMethodsConfig || {};
+ const paymentMethodType = upeElement.dataset.paymentMethodType;
+ return !! paymentMethodsConfig[ paymentMethodType ]?.supportsDeferredIntent;
+};
+
+/**
+ * @param {Object} upeElement The selector of the DOM element of particular payment method to mount the UPE element to.
+ */
export const togglePaymentMethodForCountry = ( upeElement ) => {
const paymentMethodsConfig =
getStripeServerData()?.paymentMethodsConfig || {};
@@ -585,6 +604,14 @@ export const togglePaymentMethodForCountry = ( upeElement ) => {
upeContainer.style.display = 'block';
} else {
upeContainer.style.display = 'none';
+ // Also uncheck the radio button if it's selected.
+ const radioButton = document.querySelector(
+ `input[name="payment_method"][value="stripe_${ paymentMethodType }"]`
+ );
+
+ if ( radioButton ) {
+ radioButton.checked = false;
+ }
}
};
diff --git a/includes/abstracts/abstract-wc-stripe-payment-gateway.php b/includes/abstracts/abstract-wc-stripe-payment-gateway.php
index bca889a54..5548f7b79 100644
--- a/includes/abstracts/abstract-wc-stripe-payment-gateway.php
+++ b/includes/abstracts/abstract-wc-stripe-payment-gateway.php
@@ -338,21 +338,22 @@ public function payment_icons() {
'wc_stripe_payment_icons',
[
WC_Stripe_Payment_Methods::ACH => '
',
+ WC_Stripe_Payment_Methods::ACSS_DEBIT => '
',
WC_Stripe_Payment_Methods::ALIPAY => '
',
WC_Stripe_Payment_Methods::WECHAT_PAY => '
',
WC_Stripe_Payment_Methods::BANCONTACT => '
',
WC_Stripe_Payment_Methods::IDEAL => '
',
WC_Stripe_Payment_Methods::P24 => '
',
WC_Stripe_Payment_Methods::GIROPAY => '
',
- WC_Stripe_Payment_Methods::KLARNA => '
',
- WC_Stripe_Payment_Methods::AFFIRM => '
',
+ WC_Stripe_Payment_Methods::KLARNA => '
',
+ WC_Stripe_Payment_Methods::AFFIRM => '
',
WC_Stripe_Payment_Methods::EPS => '
',
WC_Stripe_Payment_Methods::MULTIBANCO => '
',
WC_Stripe_Payment_Methods::SOFORT => '
',
WC_Stripe_Payment_Methods::SEPA => '
',
WC_Stripe_Payment_Methods::BOLETO => '
',
WC_Stripe_Payment_Methods::OXXO => '
',
- 'cards' => '
',
+ 'cards' => '
',
WC_Stripe_Payment_Methods::CASHAPP_PAY => '
',
]
);
diff --git a/includes/class-wc-stripe-account.php b/includes/class-wc-stripe-account.php
index e29abd048..33fa8dfd5 100644
--- a/includes/class-wc-stripe-account.php
+++ b/includes/class-wc-stripe-account.php
@@ -38,6 +38,7 @@ class WC_Stripe_Account {
'charge.refund.updated',
'review.opened',
'review.closed',
+ 'payment_intent.processing',
'payment_intent.succeeded',
'payment_intent.payment_failed',
'payment_intent.amount_capturable_updated',
diff --git a/includes/class-wc-stripe-intent-controller.php b/includes/class-wc-stripe-intent-controller.php
index 2c79df285..d8c8528b8 100644
--- a/includes/class-wc-stripe-intent-controller.php
+++ b/includes/class-wc-stripe-intent-controller.php
@@ -322,7 +322,8 @@ public function create_payment_intent_ajax() {
}
// If paying from order, we need to get the total from the order instead of the cart.
- $order_id = isset( $_POST['stripe_order_id'] ) ? absint( $_POST['stripe_order_id'] ) : null;
+ $order_id = isset( $_POST['stripe_order_id'] ) ? absint( $_POST['stripe_order_id'] ) : null;
+ $payment_method_type = isset( $_POST['payment_method_type'] ) ? wc_clean( wp_unslash( $_POST['payment_method_type'] ) ) : '';
if ( $order_id ) {
$order = wc_get_order( $order_id );
@@ -331,7 +332,7 @@ public function create_payment_intent_ajax() {
}
}
- wp_send_json_success( $this->create_payment_intent( $order_id ), 200 );
+ wp_send_json_success( $this->create_payment_intent( $order_id, $payment_method_type ), 200 );
} catch ( Exception $e ) {
WC_Stripe_Logger::log( 'Create payment intent error: ' . $e->getMessage() );
// Send back error so it can be displayed to the customer.
@@ -348,11 +349,13 @@ public function create_payment_intent_ajax() {
/**
* Creates payment intent using current cart or order and store details.
*
- * @param {int} $order_id The id of the order if intent created from Order.
+ * @param int|null $order_id The id of the order if intent created from Order.
+ * @param string|null $payment_method_type The type of payment method to use for the intent.
+ *
* @throws Exception - If the create intent call returns with an error.
* @return array
*/
- public function create_payment_intent( $order_id = null ) {
+ public function create_payment_intent( $order_id = null, $payment_method_type = null ) {
$amount = WC()->cart->get_total( false );
$order = wc_get_order( $order_id );
if ( is_a( $order, 'WC_Order' ) ) {
@@ -360,19 +363,20 @@ public function create_payment_intent( $order_id = null ) {
}
$gateway = $this->get_upe_gateway();
- $enabled_payment_methods = $gateway->get_upe_enabled_at_checkout_payment_method_ids( $order_id );
+ $enabled_payment_methods = $payment_method_type ? [ $payment_method_type ] : $gateway->get_upe_enabled_at_checkout_payment_method_ids( $order_id );
$currency = get_woocommerce_currency();
$capture = $gateway->is_automatic_capture_enabled();
- $payment_intent = WC_Stripe_API::request(
- [
- 'amount' => WC_Stripe_Helper::get_stripe_amount( $amount, strtolower( $currency ) ),
- 'currency' => strtolower( $currency ),
- 'payment_method_types' => $enabled_payment_methods,
- 'capture_method' => $capture ? 'automatic' : 'manual',
- ],
- 'payment_intents'
- );
+ $request = [
+ 'amount' => WC_Stripe_Helper::get_stripe_amount( $amount, strtolower( $currency ) ),
+ 'currency' => strtolower( $currency ),
+ 'payment_method_types' => $enabled_payment_methods,
+ 'capture_method' => $capture ? 'automatic' : 'manual',
+ ];
+
+ $request = $this->maybe_add_mandate_options( $request, $payment_method_type );
+
+ $payment_intent = WC_Stripe_API::request( $request, 'payment_intents' );
if ( ! empty( $payment_intent->error ) ) {
throw new Exception( $payment_intent->error->message );
@@ -818,6 +822,31 @@ private function add_mandate_data( $request ) {
return $request;
}
+ /**
+ * Adds mandate options to the request if required.
+ *
+ * @param array $request The request array to add the mandate options to.
+ * @param string|null $payment_method_type The type of payment method to use for the intent.
+ *
+ * @return array
+ */
+ private function maybe_add_mandate_options( $request, $payment_method_type ) {
+ // Add required mandate options for ACSS.
+ if ( WC_Stripe_UPE_Payment_Method_ACSS::STRIPE_ID === $payment_method_type ) {
+ $request['payment_method_options'] = [
+ 'acss_debit' => [
+ 'mandate_options' => [
+ 'payment_schedule' => 'interval',
+ 'interval_description' => __( 'One-time payment', 'woocommerce-gateway-stripe' ), // TODO: Change to cadence if purchasing a subscription.
+ 'transaction_type' => 'personal',
+ ],
+ ],
+ ];
+ }
+
+ return $request;
+ }
+
/**
* Updates and confirm a payment intent with the given payment information.
* Used for dPE.
@@ -972,7 +1001,8 @@ private function build_base_payment_intent_request_params( $payment_information
* Determines if mandate data is required for deferred intent UPE payment.
*
* A mandate must be provided before a deferred intent UPE payment can be processed.
- * This applies to SEPA, Bancontact, iDeal, Sofort, Cash App and Link payment methods.
+ * This applies to SEPA, Bancontact, iDeal, Sofort, Cash App, Link payment methods,
+ * ACH, ACSS Debit and BACS.
* https://docs.stripe.com/payments/finalize-payments-on-the-server
*
* @param string $selected_payment_type The name of the selected UPE payment type.
@@ -983,6 +1013,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::ACSS_DEBIT,
WC_Stripe_Payment_Methods::BACS_DEBIT,
WC_Stripe_Payment_Methods::SEPA_DEBIT,
WC_Stripe_Payment_Methods::BANCONTACT,
diff --git a/includes/class-wc-stripe-webhook-handler.php b/includes/class-wc-stripe-webhook-handler.php
index c188ae5c7..0e67180a5 100644
--- a/includes/class-wc-stripe-webhook-handler.php
+++ b/includes/class-wc-stripe-webhook-handler.php
@@ -991,6 +991,16 @@ public function process_payment_intent( $notification ) {
$is_wallet_payment = in_array( $payment_type_meta, WC_Stripe_Payment_Methods::WALLET_PAYMENT_METHODS, true );
switch ( $notification->type ) {
+ // Asynchronous payment methods such as bank debits will only provide a charge ID at `payment_intent.processing`, once the required actions are taken by the customer.
+ // We need to update the order transaction ID, so that the `payment_intent.succeeded` webhook is able to process the order.
+ case 'payment_intent.processing':
+ $charge = $this->get_latest_charge_from_intent( $intent );
+ if ( $charge ) {
+ $order->set_transaction_id( $charge->id );
+ /* translators: transaction id */
+ $order->update_status( 'on-hold', sprintf( __( 'Stripe charge awaiting payment: %s.', 'woocommerce-gateway-stripe' ), $charge->id ) );
+ }
+ break;
case 'payment_intent.requires_action':
do_action( 'wc_gateway_stripe_process_payment_intent_requires_action', $order, $notification->data->object );
@@ -1274,6 +1284,7 @@ public function process_webhook( $request_body ) {
$this->process_review_closed( $notification );
break;
+ case 'payment_intent.processing':
case 'payment_intent.succeeded':
case 'payment_intent.payment_failed':
case 'payment_intent.amount_capturable_updated':
diff --git a/includes/constants/class-wc-stripe-payment-methods.php b/includes/constants/class-wc-stripe-payment-methods.php
index 936ba44b2..06540fd65 100644
--- a/includes/constants/class-wc-stripe-payment-methods.php
+++ b/includes/constants/class-wc-stripe-payment-methods.php
@@ -26,6 +26,7 @@ class WC_Stripe_Payment_Methods {
const SOFORT = 'sofort';
const WECHAT_PAY = 'wechat_pay';
const CARD_PRESENT = 'card_present';
+ const ACSS_DEBIT = 'acss_debit';
const BACS_DEBIT = 'bacs_debit';
const AMAZON_PAY = 'amazon_pay';
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 f9f23aa44..b8ec21d56 100644
--- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php
@@ -40,6 +40,7 @@ class WC_Stripe_UPE_Payment_Gateway extends WC_Gateway_Stripe {
WC_Stripe_UPE_Payment_Method_Link::class,
WC_Stripe_UPE_Payment_Method_Wechat_Pay::class,
WC_Stripe_UPE_Payment_Method_Cash_App_Pay::class,
+ WC_Stripe_UPE_Payment_Method_ACSS::class,
];
/**
@@ -156,11 +157,6 @@ public function __construct() {
$this->payment_methods = [];
- if ( WC_Stripe_Feature_Flags::is_bacs_lpm_enabled() ) {
- // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
- $this->UPE_AVAILABLE_METHODS[] = WC_Stripe_UPE_Payment_Method_Bacs::class;
- }
-
foreach ( self::UPE_AVAILABLE_METHODS as $payment_method_class ) {
// Show ACH only if feature is enabled.
@@ -168,6 +164,16 @@ public function __construct() {
continue;
}
+ // Show ACSS only if feature is enabled.
+ if ( WC_Stripe_UPE_Payment_Method_ACSS::class === $payment_method_class && ! WC_Stripe_Feature_Flags::is_acss_lpm_enabled() ) {
+ continue;
+ }
+
+ // Show BACS only if feature is enabled.
+ if ( WC_Stripe_UPE_Payment_Method_Bacs::class === $payment_method_class && ! WC_Stripe_Feature_Flags::is_bacs_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.
*/
@@ -531,12 +537,13 @@ private function get_enabled_payment_method_config() {
$payment_method = $this->payment_methods[ $payment_method_id ];
$settings[ $payment_method_id ] = [
- 'isReusable' => $payment_method->is_reusable(),
- 'title' => $payment_method->get_title(),
- 'description' => $payment_method->get_description(),
- 'testingInstructions' => $payment_method->get_testing_instructions(),
- 'showSaveOption' => $this->should_upe_payment_method_show_save_option( $payment_method ),
- 'countries' => $payment_method->get_available_billing_countries(),
+ 'isReusable' => $payment_method->is_reusable(),
+ 'title' => $payment_method->get_title(),
+ 'description' => $payment_method->get_description(),
+ 'testingInstructions' => $payment_method->get_testing_instructions(),
+ 'showSaveOption' => $this->should_upe_payment_method_show_save_option( $payment_method ),
+ 'supportsDeferredIntent' => $payment_method->supports_deferred_intent(),
+ 'countries' => $payment_method->get_available_billing_countries(),
];
}
@@ -671,16 +678,27 @@ public function payment_fields() {
/**
* Process the payment for a given order.
*
- * @param int $order_id Reference.
- * @param bool $retry Should we retry on fail.
- * @param bool $force_save_source Force save the payment source.
- * @param mix $previous_error Any error message from previous request.
- * @param bool $use_order_source Whether to use the source, which should already be attached to the order.
+ * @param int $order_id Reference.
+ * @param bool $retry Should we retry on fail.
+ * @param bool $force_save_source Force save the payment source.
+ * @param mixed $previous_error Any error message from previous request.
+ * @param bool $use_order_source Whether to use the source, which should already be attached to the order.
*
* @return array|null An array with result of payment and redirect URL, or nothing.
*/
public function process_payment( $order_id, $retry = true, $force_save_source = false, $previous_error = false, $use_order_source = false ) {
+ $payment_intent_id = isset( $_POST['wc_payment_intent_id'] ) ? wc_clean( wp_unslash( $_POST['wc_payment_intent_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ $order = wc_get_order( $order_id );
+ $selected_payment_type = $this->get_selected_payment_method_type_from_request();
+
+ if ( $payment_intent_id && ! $this->payment_methods[ $selected_payment_type ]->supports_deferred_intent() ) {
+ // Adds customer and metadata to PaymentIntent.
+ // These parameters cannot be added upon updating the intent via the `/confirm` API.
+ $this->intent_controller->update_payment_intent( $payment_intent_id, $order_id );
+ }
+
// Flag for using a deferred intent. To be removed.
+ // https://github.com/woocommerce/woocommerce-gateway-stripe/issues/3868
if ( ! empty( $_POST['wc-stripe-is-deferred-intent'] ) ) {
return $this->process_payment_with_deferred_intent( $order_id );
}
@@ -693,8 +711,6 @@ public function process_payment( $order_id, $retry = true, $force_save_source =
return $this->process_payment_with_saved_payment_method( $order_id );
}
- $payment_intent_id = isset( $_POST['wc_payment_intent_id'] ) ? wc_clean( wp_unslash( $_POST['wc_payment_intent_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
- $order = wc_get_order( $order_id );
$payment_needed = $this->is_payment_needed( $order_id );
$save_payment_method = $this->has_subscription( $order_id ) || ! empty( $_POST[ 'wc-' . self::ID . '-new-payment-method' ] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
$selected_upe_payment_type = ! empty( $_POST['wc_stripe_selected_upe_payment_type'] ) ? wc_clean( wp_unslash( $_POST['wc_stripe_selected_upe_payment_type'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php
new file mode 100644
index 000000000..2ab20fb90
--- /dev/null
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php
@@ -0,0 +1,38 @@
+stripe_id = self::STRIPE_ID;
+ $this->title = __( 'Pre-Authorized Debit', 'woocommerce-gateway-stripe' );
+ $this->is_reusable = true;
+ $this->supported_currencies = [ WC_Stripe_Currency_Code::CANADIAN_DOLLAR ]; // The US dollar is supported, but has a high risk of failure since only a few Canadian bank accounts support it.
+ $this->supported_countries = [ 'CA' ];
+ $this->label = __( 'Pre-Authorized Debit', 'woocommerce-gateway-stripe' );
+ $this->description = __(
+ 'Canadian Pre-Authorized Debit is a payment method that allows customers to pay using their Canadian bank account.',
+ 'woocommerce-gateway-stripe'
+ );
+ $this->supports_deferred_intent = false;
+ }
+
+ /**
+ * 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/includes/payment-methods/class-wc-stripe-upe-payment-method.php b/includes/payment-methods/class-wc-stripe-upe-payment-method.php
index a3ba9a7cc..0bfc53607 100644
--- a/includes/payment-methods/class-wc-stripe-upe-payment-method.php
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-method.php
@@ -105,6 +105,13 @@ abstract class WC_Stripe_UPE_Payment_Method extends WC_Payment_Gateway {
*/
public $testmode;
+ /**
+ * Wether this payment method supports deferred intent creation.
+ *
+ * @var bool
+ */
+ protected $supports_deferred_intent;
+
/**
* Create instance of payment method
*/
@@ -112,11 +119,12 @@ public function __construct() {
$main_settings = WC_Stripe_Helper::get_stripe_settings();
$is_stripe_enabled = ! empty( $main_settings['enabled'] ) && 'yes' === $main_settings['enabled'];
- $this->enabled = $is_stripe_enabled && in_array( static::STRIPE_ID, $this->get_option( 'upe_checkout_experience_accepted_payments', [ WC_Stripe_Payment_Methods::CARD ] ), true ) ? 'yes' : 'no'; // @phpstan-ignore-line (STRIPE_ID is defined in classes using this class)
- $this->id = WC_Gateway_Stripe::ID . '_' . static::STRIPE_ID; // @phpstan-ignore-line (STRIPE_ID is defined in classes using this class)
- $this->has_fields = true;
- $this->testmode = WC_Stripe_Mode::is_test();
- $this->supports = [ 'products', 'refunds' ];
+ $this->enabled = $is_stripe_enabled && in_array( static::STRIPE_ID, $this->get_option( 'upe_checkout_experience_accepted_payments', [ WC_Stripe_Payment_Methods::CARD ] ), true ) ? 'yes' : 'no'; // @phpstan-ignore-line (STRIPE_ID is defined in classes using this class)
+ $this->id = WC_Gateway_Stripe::ID . '_' . static::STRIPE_ID; // @phpstan-ignore-line (STRIPE_ID is defined in classes using this class)
+ $this->has_fields = true;
+ $this->testmode = WC_Stripe_Mode::is_test();
+ $this->supports = [ 'products', 'refunds' ];
+ $this->supports_deferred_intent = true;
}
/**
@@ -726,4 +734,13 @@ public function get_transaction_url( $order ) {
return parent::get_transaction_url( $order );
}
+
+ /**
+ * Whether this payment method supports deferred intent creation.
+ *
+ * @return bool
+ */
+ public function supports_deferred_intent() {
+ return $this->supports_deferred_intent;
+ }
}
diff --git a/tests/phpunit/payment-methods/test-class-wc-stripe-upe-payment-gateway.php b/tests/phpunit/payment-methods/test-class-wc-stripe-upe-payment-gateway.php
index 3cfc11fd7..57d64c0f7 100644
--- a/tests/phpunit/payment-methods/test-class-wc-stripe-upe-payment-gateway.php
+++ b/tests/phpunit/payment-methods/test-class-wc-stripe-upe-payment-gateway.php
@@ -118,6 +118,7 @@ public function set_up() {
parent::set_up();
update_option( WC_Stripe_Feature_Flags::LPM_ACH_FEATURE_FLAG_NAME, 'yes' );
+ update_option( WC_Stripe_Feature_Flags::LPM_ACSS_FEATURE_FLAG_NAME, 'yes' );
$stripe_settings = WC_Stripe_Helper::get_stripe_settings();
$stripe_settings['sepa_tokens_for_other_methods'] = 'yes';
@@ -190,6 +191,7 @@ public function set_up() {
public function tear_down() {
parent::tear_down();
delete_option( WC_Stripe_Feature_Flags::LPM_ACH_FEATURE_FLAG_NAME );
+ delete_option( WC_Stripe_Feature_Flags::LPM_ACSS_FEATURE_FLAG_NAME );
}
/**
@@ -285,6 +287,7 @@ public function get_upe_available_payment_methods_provider() {
WC_Stripe_UPE_Payment_Method_Link::STRIPE_ID,
WC_Stripe_UPE_Payment_Method_Wechat_Pay::STRIPE_ID,
WC_Stripe_UPE_Payment_Method_Cash_App_Pay::STRIPE_ID,
+ WC_Stripe_UPE_Payment_Method_ACSS::STRIPE_ID,
],
],
[
@@ -299,6 +302,7 @@ public function get_upe_available_payment_methods_provider() {
WC_Stripe_UPE_Payment_Method_Oxxo::STRIPE_ID,
WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID,
WC_Stripe_UPE_Payment_Method_P24::STRIPE_ID,
+ WC_Stripe_UPE_Payment_Method_ACSS::STRIPE_ID,
],
],
];
@@ -341,7 +345,10 @@ public function test_process_payment_returns_valid_response() {
'metadata' => $metadata,
];
- $_POST = [ 'wc_payment_intent_id' => $payment_intent_id ];
+ $_POST = [
+ 'payment_method' => 'stripe',
+ 'wc_payment_intent_id' => $payment_intent_id,
+ ];
$this->mock_gateway->expects( $this->any() )
->method( 'get_stripe_customer_from_order' )
@@ -1782,7 +1789,10 @@ public function test_if_order_has_subscription_payment_method_will_be_saved() {
'setup_future_usage' => 'off_session',
];
- $_POST = [ 'wc_payment_intent_id' => $payment_intent_id ];
+ $_POST = [
+ 'payment_method' => 'stripe',
+ 'wc_payment_intent_id' => $payment_intent_id,
+ ];
$this->mock_gateway->expects( $this->any() )
->method( 'is_subscriptions_enabled' )
@@ -1831,7 +1841,10 @@ public function test_if_free_trial_subscription_will_not_update_intent() {
$order->set_total( 0 );
$order->save();
- $_POST = [ 'wc_payment_intent_id' => $setup_intent_id ];
+ $_POST = [
+ 'payment_method' => 'stripe',
+ 'wc_payment_intent_id' => $setup_intent_id,
+ ];
$this->mock_gateway->expects( $this->any() )
->method( 'has_subscription' )
diff --git a/tests/phpunit/payment-methods/test-class-wc-stripe-upe-payment-method.php b/tests/phpunit/payment-methods/test-class-wc-stripe-upe-payment-method.php
index 9ca8bbd7a..5f0db569c 100644
--- a/tests/phpunit/payment-methods/test-class-wc-stripe-upe-payment-method.php
+++ b/tests/phpunit/payment-methods/test-class-wc-stripe-upe-payment-method.php
@@ -112,26 +112,27 @@ class WC_Stripe_UPE_Payment_Method_Test extends WP_UnitTestCase {
* 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',
- 'us_bank_account_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',
+ 'acss_debit_payments' => 'active',
+ 'us_bank_account_payments' => 'active',
];
/**
@@ -250,6 +251,9 @@ public function test_payment_methods_show_correct_default_outputs() {
$mock_wechat_pay_details = [
'type' => WC_Stripe_Payment_Methods::WECHAT_PAY,
];
+ $mock_acss_details = [
+ 'type' => WC_Stripe_Payment_Methods::ACSS_DEBIT,
+ ];
$card_method = $this->mock_payment_methods['card'];
$alipay_method = $this->mock_payment_methods['alipay'];
@@ -264,6 +268,7 @@ public function test_payment_methods_show_correct_default_outputs() {
$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'];
+ $acss_method = $this->mock_payment_methods['acss_debit'];
$this->assertEquals( WC_Stripe_Payment_Methods::CARD, $card_method->get_id() );
$this->assertEquals( 'Credit / Debit Card', $card_method->get_label() );
@@ -371,6 +376,14 @@ public function test_payment_methods_show_correct_default_outputs() {
$this->assertTrue( $ach_method->is_reusable() );
$this->assertEquals( WC_Stripe_Payment_Methods::ACH, $ach_method->get_retrievable_type() );
$this->assertEquals( '', $ach_method->get_testing_instructions() );
+
+ $this->assertEquals( WC_Stripe_Payment_Methods::ACSS_DEBIT, $acss_method->get_id() );
+ $this->assertEquals( 'Pre-Authorized Debit', $acss_method->get_label() );
+ $this->assertEquals( 'Pre-Authorized Debit', $acss_method->get_title() );
+ $this->assertEquals( 'Pre-Authorized Debit', $acss_method->get_title( $mock_acss_details ) );
+ $this->assertTrue( $acss_method->is_reusable() );
+ $this->assertEquals( WC_Stripe_Payment_Methods::ACSS_DEBIT, $acss_method->get_retrievable_type() );
+ $this->assertEquals( '', $acss_method->get_testing_instructions() );
}
/**
@@ -402,6 +415,7 @@ public function test_card_payment_method_capability_is_always_enabled() {
$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'];
+ $acss_method = $this->mock_payment_methods['acss_debit'];
$this->assertTrue( $card_method->is_enabled_at_checkout() );
$this->assertFalse( $klarna_method->is_enabled_at_checkout() );
@@ -418,6 +432,7 @@ public function test_card_payment_method_capability_is_always_enabled() {
$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() );
+ $this->assertFalse( $acss_method->is_enabled_at_checkout() );
}
/**
@@ -611,7 +626,8 @@ public function test_payment_methods_are_reusable_if_cart_contains_subscription(
$store_currency = in_array( $payment_method_id, [ WC_Stripe_UPE_Payment_Method_Link::STRIPE_ID, WC_Stripe_UPE_Payment_Method_ACH::STRIPE_ID ] ) ? WC_Stripe_Currency_Code::UNITED_STATES_DOLLAR : 'EUR';
$account_currency = null;
- if ( $payment_method->has_domestic_transactions_restrictions() ) {
+ // Use different currencies for ACSS or payment methods that have domestic transactions restrictions.
+ if ( $payment_method->has_domestic_transactions_restrictions() || WC_Stripe_UPE_Payment_Method_ACSS::STRIPE_ID === $payment_method_id ) {
$store_currency = $payment_method->get_supported_currencies()[0];
$account_currency = $store_currency;
}
@@ -634,6 +650,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::ACSS_DEBIT,
WC_Stripe_Payment_Methods::CARD,
WC_Stripe_Payment_Methods::KLARNA,
WC_Stripe_Payment_Methods::AFTERPAY_CLEARPAY,
diff --git a/tests/phpunit/test-wc-stripe-webhook-handler.php b/tests/phpunit/test-wc-stripe-webhook-handler.php
index fc850e8f6..857eaf050 100644
--- a/tests/phpunit/test-wc-stripe-webhook-handler.php
+++ b/tests/phpunit/test-wc-stripe-webhook-handler.php
@@ -473,6 +473,57 @@ public function test_process_payment_intent(
$this->assertEquals( $expected_process_payment_intent_incomplete_calls, $mock_action_process_payment_intent_incomplete->get_call_count() );
}
+ /**
+ * Test that when a PaymentIntent is in the `processing` status,
+ * the order is updated to on-hold and the transaction ID is set.
+ */
+ public function test_process_webhook_payment_intent_processing() {
+ $notification = (object) [
+ 'type' => 'payment_intent.processing',
+ 'data' => (object) [
+ 'object' => (object) [
+ 'id' => 'pi_mock',
+ 'charges' => (object) [
+ 'data' => [
+ (object) [
+ 'id' => 'ch_mock',
+ ],
+ ],
+ ],
+ ],
+ ],
+ ];
+
+ // Order must be previously set to pending and have at least the payment intent set.
+ $order = WC_Helper_Order::create_order();
+ WC_Stripe_Helper::add_payment_intent_to_order( $notification->data->object->id, $order );
+ $order->set_status( 'pending' );
+ $order->save();
+
+ $this->mock_webhook_handler = $this->getMockBuilder( WC_Stripe_Webhook_Handler::class )
+ ->setMethods( [ 'lock_order_payment' ] )
+ ->getMock();
+
+ $this->mock_webhook_handler->method( 'lock_order_payment' )->willReturn( false );
+
+ $this->mock_webhook_handler->process_payment_intent( $notification );
+
+ $updated_order = wc_get_order( $order->get_id() );
+ $this->assertEquals( 'on-hold', $updated_order->get_status() );
+ $this->assertEquals( 'ch_mock', $updated_order->get_transaction_id() );
+
+ // Grab the latest order note and verify the content.
+ $notes = wc_get_order_notes(
+ [
+ 'order_id' => $updated_order->get_id(),
+ 'limit' => 1,
+ ]
+ );
+ $this->assertCount( 1, $notes );
+ $this->assertStringContainsString( 'Stripe charge awaiting payment: ch_mock.', $notes[0]->content );
+ }
+
+
/**
* Provider for `test_process_payment_intent`.
*
diff --git a/woocommerce-gateway-stripe.php b/woocommerce-gateway-stripe.php
index b1c6272cc..be6844f55 100644
--- a/woocommerce-gateway-stripe.php
+++ b/woocommerce-gateway-stripe.php
@@ -242,6 +242,7 @@ public function init() {
require_once __DIR__ . '/includes/payment-methods/class-wc-stripe-upe-payment-method-link.php';
require_once __DIR__ . '/includes/payment-methods/class-wc-stripe-upe-payment-method-cash-app-pay.php';
require_once __DIR__ . '/includes/payment-methods/class-wc-stripe-upe-payment-method-wechat-pay.php';
+ require_once __DIR__ . '/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php';
require_once __DIR__ . '/includes/payment-methods/class-wc-gateway-stripe-bancontact.php';
require_once __DIR__ . '/includes/payment-methods/class-wc-gateway-stripe-sofort.php';
require_once __DIR__ . '/includes/payment-methods/class-wc-gateway-stripe-giropay.php';