From 383100851ddb1686519c11a280af38cd964ccf70 Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Wed, 22 Jan 2025 15:40:23 -0300 Subject: [PATCH 01/24] PoC: Add ACSS Debit payment method --- client/payment-method-icons/index.js | 1 + client/payment-methods-map.js | 13 ++++++ client/stripe-utils/constants.js | 3 ++ .../class-wc-stripe-intent-controller.php | 13 +++++- .../class-wc-stripe-payment-methods.php | 1 + .../class-wc-stripe-upe-payment-gateway.php | 1 + ...lass-wc-stripe-upe-payment-method-acss.php | 40 +++++++++++++++++++ woocommerce-gateway-stripe.php | 1 + 8 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php diff --git a/client/payment-method-icons/index.js b/client/payment-method-icons/index.js index c0c9c57060..9c09b3b9d5 100644 --- a/client/payment-method-icons/index.js +++ b/client/payment-method-icons/index.js @@ -36,4 +36,5 @@ export default { oxxo: OxxoIcon, wechat_pay: WechatPayIcon, cashapp: CashAppIcon, + acss_debit: CreditCardIcon, }; diff --git a/client/payment-methods-map.js b/client/payment-methods-map.js index 3547955b42..c37c6b79f3 100644 --- a/client/payment-methods-map.js +++ b/client/payment-methods-map.js @@ -239,4 +239,17 @@ export default { currencies: [ 'USD' ], capability: 'cashapp_payments', }, + acss_debit: { + id: 'acss_debit', + label: __( + 'Canadian 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.card, + currencies: [ 'CAD' ], + }, }; diff --git a/client/stripe-utils/constants.js b/client/stripe-utils/constants.js index 62c7ca2e6d..2425acd03d 100644 --- a/client/stripe-utils/constants.js +++ b/client/stripe-utils/constants.js @@ -21,6 +21,7 @@ export const PAYMENT_METHOD_CLEARPAY = 'clearpay'; export const PAYMENT_METHOD_WECHAT_PAY = 'wechat_pay'; export const PAYMENT_METHOD_CASHAPP = 'cashapp'; export const PAYMENT_METHOD_LINK = 'link'; +export const PAYMENT_METHOD_ACSS_DEBIT = 'acss_debit'; /** * Payment method names constants with the `stripe` prefix @@ -43,6 +44,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_DEBIT = 'stripe_acss_debit'; export function getPaymentMethodsConstants() { return { @@ -63,6 +65,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_DEBIT, }; } diff --git a/includes/class-wc-stripe-intent-controller.php b/includes/class-wc-stripe-intent-controller.php index ccd967cfc9..be36399737 100644 --- a/includes/class-wc-stripe-intent-controller.php +++ b/includes/class-wc-stripe-intent-controller.php @@ -950,7 +950,18 @@ 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 ) { - if ( in_array( $selected_payment_type, [ WC_Stripe_Payment_Methods::SEPA_DEBIT, WC_Stripe_Payment_Methods::BANCONTACT, WC_Stripe_Payment_Methods::IDEAL, WC_Stripe_Payment_Methods::SOFORT, WC_Stripe_Payment_Methods::LINK ], true ) ) { + if ( in_array( + $selected_payment_type, + [ + WC_Stripe_Payment_Methods::SEPA_DEBIT, + WC_Stripe_Payment_Methods::BANCONTACT, + WC_Stripe_Payment_Methods::IDEAL, + WC_Stripe_Payment_Methods::SOFORT, + WC_Stripe_Payment_Methods::LINK, + WC_Stripe_Payment_Methods::ACSS_DEBIT, + ], + true + ) ) { return true; } diff --git a/includes/constants/class-wc-stripe-payment-methods.php b/includes/constants/class-wc-stripe-payment-methods.php index 65d2de25cc..08fd6a1d37 100644 --- a/includes/constants/class-wc-stripe-payment-methods.php +++ b/includes/constants/class-wc-stripe-payment-methods.php @@ -24,6 +24,7 @@ class WC_Stripe_Payment_Methods { const SOFORT = 'sofort'; const WECHAT_PAY = 'wechat_pay'; const CARD_PRESENT = 'card_present'; + const ACSS_DEBIT = 'acss_debit'; /** * Payment methods that are considered as voucher payment methods. 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 3aa3369d5d..eccacf8174 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -39,6 +39,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, ]; /** 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 0000000000..71ae12a418 --- /dev/null +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php @@ -0,0 +1,40 @@ +stripe_id = self::STRIPE_ID; + $this->title = __( 'Canadian Pre-Autorized Debit', 'woocommerce-gateway-stripe' ); + $this->is_reusable = true; + $this->supported_currencies = [ WC_Stripe_Currency_Code::CANADIAN_DOLLAR, WC_Stripe_Currency_Code::UNITED_STATES_DOLLAR ]; + $this->supported_countries = [ 'CA' ]; + $this->label = __( 'Canadian 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' + ); + } + + /** + * Returns whether the payment method is available for the Stripe account's country. + * + * Canadian Pre-Authorized Debit is only available for domestic transactions in the United States or Canada. + * + * @return bool True if the payment method is available for the account's country, false otherwise. + */ + // public function is_available_for_account_country() { + // return in_array( WC_Stripe::get_instance()->account->get_account_country(), $this->supported_countries, true ); + // } +} diff --git a/woocommerce-gateway-stripe.php b/woocommerce-gateway-stripe.php index ccaaad0fdc..b7b80b3676 100644 --- a/woocommerce-gateway-stripe.php +++ b/woocommerce-gateway-stripe.php @@ -239,6 +239,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'; From 5c7b8154dca0fafe990b8505f5e00d051178e963 Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Thu, 23 Jan 2025 16:26:45 -0300 Subject: [PATCH 02/24] Work around deferred intents --- client/classic/upe/payment-processing.js | 68 ++++++++++++++++++- .../class-wc-stripe-intent-controller.php | 48 ++++++++++--- ...lass-wc-stripe-upe-payment-method-acss.php | 7 ++ .../class-wc-stripe-upe-payment-method.php | 15 +++- 4 files changed, 126 insertions(+), 12 deletions(-) diff --git a/client/classic/upe/payment-processing.js b/client/classic/upe/payment-processing.js index 24242f7a6a..8709866c77 100644 --- a/client/classic/upe/payment-processing.js +++ b/client/classic/upe/payment-processing.js @@ -20,6 +20,7 @@ import { PAYMENT_METHOD_CASHAPP, PAYMENT_METHOD_MULTIBANCO, PAYMENT_METHOD_WECHAT_PAY, + PAYMENT_METHOD_ACSS_DEBIT, } from 'wcstripe/stripe-utils/constants'; const gatewayUPEComponents = {}; @@ -57,7 +58,7 @@ function blockUI( jQueryForm ) { * @return {Promise} Promise for the checkout submission. */ export function validateElements( elements ) { - return elements.submit().then( ( result ) => { + return elements?.submit().then( ( result ) => { if ( result.error ) { throw new Error( result.error.message ); } @@ -273,6 +274,13 @@ export const processPayment = ( blockUI( jQueryForm ); + // ACSS Debit requires different handling without the Payment Element. + // TODO: We should probably refactor this into a separate method. + if ( paymentMethodType === PAYMENT_METHOD_ACSS_DEBIT ) { + confirmAcssDebitPayment( api, jQueryForm ); + return false; + } + const elements = gatewayUPEComponents[ paymentMethodType ].elements; const getErrorMessage = ( err ) => { @@ -613,3 +621,61 @@ export const confirmWalletPayment = async ( api, jQueryForm ) => { resetBlockCheckoutPaymentState(); } }; + +/** + * Handles displaying the ACSS Debit authorization modal/flows to the customer and then redirecting + * them to the order received page once they authenticate the payment or once micro-deposit verification is triggered. + * + * @param {Object} api The API object used to make requests (createIntent, confirmAcssDebitPayment, etc). + * @param {Object} jQueryForm The jQuery form object representing your checkout/order form. + */ +export const confirmAcssDebitPayment = async ( api, jQueryForm ) => { + const isOrderPay = getStripeServerData()?.isOrderPay; + + // The Order Pay page does a hard refresh when the hash changes, so we need to block the UI again. + if ( isOrderPay ) { + blockUI( jQueryForm ); + } + + try { + const acssIntent = await api.createIntent(); + + const firstName = document.querySelector( '#billing_first_name' )?.value || ''; + const lastName = document.querySelector( '#billing_last_name' )?.value || ''; + const email = document.querySelector( '#billing_email' )?.value || ''; + const billingDetails = { + name: ( firstName + ' ' + lastName ).trim(), + email, + }; + + const { paymentIntent, error } = await api.getStripe().confirmAcssDebitPayment( + acssIntent.client_secret, + { + payment_method: { + billing_details: billingDetails, + }, + } + ); + + if ( error ) { + // TODO: Handle errors more gracefully. + throw new Error( error.message ); + } + + // Similar to `appendSetupIntentToForm`, we need to pass the intent ID back to the order. + const hiddenInput = document.createElement( 'input' ); + hiddenInput.type = 'hidden'; + hiddenInput.name = 'wc_stripe_payment_intent'; + hiddenInput.value = paymentIntent.id; + jQueryForm[0].appendChild( hiddenInput ); + hasCheckoutCompleted = true; + submitForm( jQueryForm ); + } catch ( err ) { + jQueryForm.removeClass( 'processing' ).unblock(); + hasCheckoutCompleted = false; + showErrorCheckout( err?.message || __( 'ACSS Debit payment failed.', 'woocommerce-gateway-stripe' ) ); + } finally { + unblockBlockCheckout(); + resetBlockCheckoutPaymentState(); + } +}; diff --git a/includes/class-wc-stripe-intent-controller.php b/includes/class-wc-stripe-intent-controller.php index be36399737..444f6fd24d 100644 --- a/includes/class-wc-stripe-intent-controller.php +++ b/includes/class-wc-stripe-intent-controller.php @@ -349,15 +349,17 @@ public function create_payment_intent( $order_id = null ) { $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', + ]; + + // Conditionally add ACSS Debit mandate options if 'acss_debit' is present. + $request = $this->maybe_add_acss_mandate_options( $request, $enabled_payment_methods ); + + $payment_intent = WC_Stripe_API::request( $request, 'payment_intents' ); if ( ! empty( $payment_intent->error ) ) { throw new Exception( $payment_intent->error->message ); @@ -369,6 +371,34 @@ public function create_payment_intent( $order_id = null ) { ]; } + /** + * Conditionally adds ACSS Debit mandate options to the Stripe payment_intent request array. + * + * @param array $request The Stripe request body that will be sent to the /payment_intents endpoint. + * @param array $enabled_payment_methods The payment method types that will be used for this PaymentIntent. + * + * @return array The updated $request with ACSS Debit mandate options, if applicable. + */ + private function maybe_add_acss_mandate_options( $request, $enabled_payment_methods ) { + if ( ! in_array( 'acss_debit', $enabled_payment_methods, true ) ) { + return $request; + } + + if ( ! isset( $request['payment_method_options'] ) ) { + $request['payment_method_options'] = []; + } + + $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; + } + /** * Handle AJAX request for updating a payment intent for Stripe UPE. * 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 index 71ae12a418..4bf258e4d1 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php @@ -25,6 +25,13 @@ public function __construct() { 'Canadian Pre-Authorized Debit is a payment method that allows customers to pay using their Canadian bank account.', 'woocommerce-gateway-stripe' ); + + $this->is_deferred_intent = false; + } + + public function get_testing_instructions() { + return __( 'Use the following test account details:', 'woocommerce-gateway-stripe' ) . '
' . + __( 'Account number: 000123456789', 'woocommerce-gateway-stripe' ); } /** 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 78d49acdff..e4ce783fbc 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 UPE method uses deferred intent. + * + * @var bool + */ + protected $is_deferred_intent; + /** * Create instance of payment method */ @@ -117,6 +124,8 @@ public function __construct() { $this->has_fields = true; $this->testmode = WC_Stripe_Mode::is_test(); $this->supports = [ 'products', 'refunds' ]; + + $this->is_deferred_intent = true; } /** @@ -577,9 +586,11 @@ public function payment_fields() {

get_description() ); ?>

-
+ is_deferred_intent ) : ?> +
+ + -
Date: Sat, 25 Jan 2025 04:41:24 -0300 Subject: [PATCH 03/24] Changes required for ACSS --- client/api/index.js | 4 +- client/classic/upe/payment-processing.js | 118 ++++++------------ client/stripe-utils/utils.js | 6 + .../class-wc-stripe-intent-controller.php | 51 +++----- .../class-wc-stripe-upe-payment-gateway.php | 13 +- ...lass-wc-stripe-upe-payment-method-acss.php | 16 --- .../class-wc-stripe-upe-payment-method.php | 6 +- 7 files changed, 79 insertions(+), 135 deletions(-) diff --git a/client/api/index.js b/client/api/index.js index f0bb94701d..a70a30716b 100644 --- a/client/api/index.js +++ b/client/api/index.js @@ -133,12 +133,14 @@ 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 {boolean} isAcssDebit Whether the payment method is ACSS Debit. * * @return {Promise} The final promise for the request to the server. */ - createIntent( orderId ) { + createIntent( orderId, isAcssDebit = false ) { return this.request( this.getAjaxUrl( 'create_payment_intent' ), { stripe_order_id: orderId, + is_acss_debit: isAcssDebit, _ajax_nonce: this.options?.createPaymentIntentNonce, } ) .then( ( response ) => { diff --git a/client/classic/upe/payment-processing.js b/client/classic/upe/payment-processing.js index 8709866c77..279be77359 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, @@ -23,6 +24,8 @@ import { PAYMENT_METHOD_ACSS_DEBIT, } from 'wcstripe/stripe-utils/constants'; +let paymentIntentId, clientSecret; + const gatewayUPEComponents = {}; const paymentMethodsConfig = getStripeServerData()?.paymentMethodsConfig; @@ -66,28 +69,44 @@ 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. * - * @todo Make paymentMethodType required when Split is implemented. + * If the user is NOT deferring (e.g. ACSS Debit), we call our server to create a PaymentIntent + * and then pass its client_secret to the Payment Element on initialization. * * @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 = null ) { 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(), - }; + let options; + + // ACSS doesn't support deferred intent, so we need to create a PaymentIntent first. + if ( paymentMethodType === PAYMENT_METHOD_ACSS_DEBIT ) { + const response = await api.createIntent( paymentMethodType, true ); + + clientSecret = response.client_secret; + paymentIntentId = response.id; + + options = { + appearance: initializeUPEAppearance( api ), + paymentMethodCreation: 'manual', + fonts: getFontRulesFromPage(), + clientSecret: clientSecret, + }; + } 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 ); @@ -274,13 +293,6 @@ export const processPayment = ( blockUI( jQueryForm ); - // ACSS Debit requires different handling without the Payment Element. - // TODO: We should probably refactor this into a separate method. - if ( paymentMethodType === PAYMENT_METHOD_ACSS_DEBIT ) { - confirmAcssDebitPayment( api, jQueryForm ); - return false; - } - const elements = gatewayUPEComponents[ paymentMethodType ].elements; const getErrorMessage = ( err ) => { @@ -328,6 +340,13 @@ export const processPayment = ( try { await validateElements( elements ); + // TODO: Refactor this in the right place. + // ACSS Debit requires different handling. + if ( paymentMethodType === PAYMENT_METHOD_ACSS_DEBIT ) { + // Attach payment intent ID to form. + appendPaymentIntentIdToForm( jQueryForm, paymentIntentId ); + } + const paymentMethodObject = await createStripePaymentMethod( api, elements, @@ -622,60 +641,3 @@ export const confirmWalletPayment = async ( api, jQueryForm ) => { } }; -/** - * Handles displaying the ACSS Debit authorization modal/flows to the customer and then redirecting - * them to the order received page once they authenticate the payment or once micro-deposit verification is triggered. - * - * @param {Object} api The API object used to make requests (createIntent, confirmAcssDebitPayment, etc). - * @param {Object} jQueryForm The jQuery form object representing your checkout/order form. - */ -export const confirmAcssDebitPayment = async ( api, jQueryForm ) => { - const isOrderPay = getStripeServerData()?.isOrderPay; - - // The Order Pay page does a hard refresh when the hash changes, so we need to block the UI again. - if ( isOrderPay ) { - blockUI( jQueryForm ); - } - - try { - const acssIntent = await api.createIntent(); - - const firstName = document.querySelector( '#billing_first_name' )?.value || ''; - const lastName = document.querySelector( '#billing_last_name' )?.value || ''; - const email = document.querySelector( '#billing_email' )?.value || ''; - const billingDetails = { - name: ( firstName + ' ' + lastName ).trim(), - email, - }; - - const { paymentIntent, error } = await api.getStripe().confirmAcssDebitPayment( - acssIntent.client_secret, - { - payment_method: { - billing_details: billingDetails, - }, - } - ); - - if ( error ) { - // TODO: Handle errors more gracefully. - throw new Error( error.message ); - } - - // Similar to `appendSetupIntentToForm`, we need to pass the intent ID back to the order. - const hiddenInput = document.createElement( 'input' ); - hiddenInput.type = 'hidden'; - hiddenInput.name = 'wc_stripe_payment_intent'; - hiddenInput.value = paymentIntent.id; - jQueryForm[0].appendChild( hiddenInput ); - hasCheckoutCompleted = true; - submitForm( jQueryForm ); - } catch ( err ) { - jQueryForm.removeClass( 'processing' ).unblock(); - hasCheckoutCompleted = false; - showErrorCheckout( err?.message || __( 'ACSS Debit payment failed.', 'woocommerce-gateway-stripe' ) ); - } finally { - unblockBlockCheckout(); - resetBlockCheckoutPaymentState(); - } -}; diff --git a/client/stripe-utils/utils.js b/client/stripe-utils/utils.js index 0783c1a3d2..35bd648583 100644 --- a/client/stripe-utils/utils.js +++ b/client/stripe-utils/utils.js @@ -299,6 +299,12 @@ export const appendPaymentMethodIdToForm = ( form, paymentMethodId ) => { ); }; +export const appendPaymentIntentIdToForm = ( form, paymentIntentId ) => { + form.append( + `` + ); +}; + export const appendSetupIntentToForm = ( form, setupIntent ) => { form.append( `` diff --git a/includes/class-wc-stripe-intent-controller.php b/includes/class-wc-stripe-intent-controller.php index 444f6fd24d..a24683bde1 100644 --- a/includes/class-wc-stripe-intent-controller.php +++ b/includes/class-wc-stripe-intent-controller.php @@ -307,7 +307,9 @@ 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; + // Check if is_acss_debit is set and true. + $is_acss_debit = isset( $_POST['is_acss_debit'] ) && filter_var( $_POST['is_acss_debit'], FILTER_VALIDATE_BOOLEAN ); if ( $order_id ) { $order = wc_get_order( $order_id ); @@ -316,7 +318,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, $is_acss_debit ), 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. @@ -337,7 +339,7 @@ public function create_payment_intent_ajax() { * @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, $is_acss_debit = false ) { $amount = WC()->cart->get_total( false ); $order = wc_get_order( $order_id ); if ( is_a( $order, 'WC_Order' ) ) { @@ -345,7 +347,7 @@ 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 = $is_acss_debit ? [ 'acss_debit' ] : $gateway->get_upe_enabled_at_checkout_payment_method_ids( $order_id ); $currency = get_woocommerce_currency(); $capture = $gateway->is_automatic_capture_enabled(); @@ -356,8 +358,17 @@ public function create_payment_intent( $order_id = null ) { 'capture_method' => $capture ? 'automatic' : 'manual', ]; - // Conditionally add ACSS Debit mandate options if 'acss_debit' is present. - $request = $this->maybe_add_acss_mandate_options( $request, $enabled_payment_methods ); + if ( $is_acss_debit ) { + $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', + ], + ], + ]; + } $payment_intent = WC_Stripe_API::request( $request, 'payment_intents' ); @@ -371,34 +382,6 @@ public function create_payment_intent( $order_id = null ) { ]; } - /** - * Conditionally adds ACSS Debit mandate options to the Stripe payment_intent request array. - * - * @param array $request The Stripe request body that will be sent to the /payment_intents endpoint. - * @param array $enabled_payment_methods The payment method types that will be used for this PaymentIntent. - * - * @return array The updated $request with ACSS Debit mandate options, if applicable. - */ - private function maybe_add_acss_mandate_options( $request, $enabled_payment_methods ) { - if ( ! in_array( 'acss_debit', $enabled_payment_methods, true ) ) { - return $request; - } - - if ( ! isset( $request['payment_method_options'] ) ) { - $request['payment_method_options'] = []; - } - - $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; - } - /** * Handle AJAX request for updating a payment intent for Stripe UPE. * 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 eccacf8174..e361645114 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -638,6 +638,17 @@ public function payment_fields() { * @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 ); + + // TODO: Refactor this into the right place. + // If payment method is ACSS debit, handle accordingly. + $selected_payment_type = $this->get_selected_payment_method_type_from_request(); + if ( $selected_payment_type === WC_Stripe_UPE_Payment_Method_ACSS::STRIPE_ID ) { + // Attach payment intent to order. + WC_Stripe_Helper::add_payment_intent_to_order( $payment_intent_id, $order ); + } + // Flag for using a deferred intent. To be removed. if ( ! empty( $_POST['wc-stripe-is-deferred-intent'] ) ) { return $this->process_payment_with_deferred_intent( $order_id ); @@ -651,8 +662,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 index 4bf258e4d1..12444c2964 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php @@ -28,20 +28,4 @@ public function __construct() { $this->is_deferred_intent = false; } - - public function get_testing_instructions() { - return __( 'Use the following test account details:', 'woocommerce-gateway-stripe' ) . '
' . - __( 'Account number: 000123456789', 'woocommerce-gateway-stripe' ); - } - - /** - * Returns whether the payment method is available for the Stripe account's country. - * - * Canadian Pre-Authorized Debit is only available for domestic transactions in the United States or Canada. - * - * @return bool True if the payment method is available for the account's country, false otherwise. - */ - // public function is_available_for_account_country() { - // return in_array( WC_Stripe::get_instance()->account->get_account_country(), $this->supported_countries, true ); - // } } 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 e4ce783fbc..3cd97602d3 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method.php @@ -586,11 +586,9 @@ public function payment_fields() {

get_description() ); ?>

- is_deferred_intent ) : ?> -
- - +
+
Date: Wed, 29 Jan 2025 23:43:48 -0300 Subject: [PATCH 04/24] Some code cleanup for ACSS --- client/classic/upe/payment-processing.js | 4 ++-- client/payment-methods-map.js | 2 +- .../class-wc-stripe-upe-payment-method-acss.php | 7 +++---- .../class-wc-stripe-upe-payment-method.php | 17 ++++++++--------- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/client/classic/upe/payment-processing.js b/client/classic/upe/payment-processing.js index 315d7a41e8..2512d446fa 100644 --- a/client/classic/upe/payment-processing.js +++ b/client/classic/upe/payment-processing.js @@ -62,7 +62,7 @@ function blockUI( jQueryForm ) { * @return {Promise} Promise for the checkout submission. */ export function validateElements( elements ) { - return elements?.submit().then( ( result ) => { + return elements.submit().then( ( result ) => { if ( result.error ) { throw new Error( result.error.message ); } @@ -95,7 +95,7 @@ async function createStripePaymentElement( api, paymentMethodType = null ) { appearance: initializeUPEAppearance( api ), paymentMethodCreation: 'manual', fonts: getFontRulesFromPage(), - clientSecret: clientSecret, + clientSecret, }; } else { options = { diff --git a/client/payment-methods-map.js b/client/payment-methods-map.js index 0fff3e947b..8780f07519 100644 --- a/client/payment-methods-map.js +++ b/client/payment-methods-map.js @@ -242,7 +242,7 @@ const paymentMethodsMap = { acss_debit: { id: 'acss_debit', label: __( - 'Canadian Pre-Authorized Debit', + 'Pre-Authorized Debit', 'woocommerce-gateway-stripe' ), description: __( 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 index 12444c2964..9a32a62cbd 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php @@ -18,14 +18,13 @@ public function __construct() { $this->stripe_id = self::STRIPE_ID; $this->title = __( 'Canadian Pre-Autorized Debit', 'woocommerce-gateway-stripe' ); $this->is_reusable = true; - $this->supported_currencies = [ WC_Stripe_Currency_Code::CANADIAN_DOLLAR, WC_Stripe_Currency_Code::UNITED_STATES_DOLLAR ]; + $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 = __( 'Canadian Pre-Authorized Debit', 'woocommerce-gateway-stripe' ); + $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->is_deferred_intent = false; + $this->supports_deferred_intent = false; } } 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 3cd97602d3..ba63e9c600 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method.php @@ -106,11 +106,11 @@ abstract class WC_Stripe_UPE_Payment_Method extends WC_Payment_Gateway { public $testmode; /** - * Wether this UPE method uses deferred intent. + * Wether this payment method supports deferred intent creation. * * @var bool */ - protected $is_deferred_intent; + protected $supports_deferred_intent; /** * Create instance of payment method @@ -119,13 +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->is_deferred_intent = true; + $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; } /** From 09fbf6eef4535737f7436bf6bcd052ba42771d40 Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Fri, 31 Jan 2025 19:36:56 -0300 Subject: [PATCH 05/24] Only mount UPE when the payment element is selected on the classic checkout --- client/classic/upe/deferred-intent.js | 43 +++++++++++++------ client/stripe-utils/utils.js | 27 +++++++++++- .../class-wc-stripe-upe-payment-gateway.php | 13 +++--- .../class-wc-stripe-upe-payment-method.php | 9 ++++ 4 files changed, 71 insertions(+), 21 deletions(-) diff --git a/client/classic/upe/deferred-intent.js b/client/classic/upe/deferred-intent.js index c222796e55..1ae11ecc62 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,12 @@ jQuery( function ( $ ) { maybeMountStripePaymentElement(); } + // Maybe mount the Payment Element when selecting the payment method. + // This is needed for payment methods that don't support deferred intents. + $( '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 +83,30 @@ 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() ) { + // 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 ); + restrictPaymentMethodToLocation( upeElement ); } } @@ -97,7 +114,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/stripe-utils/utils.js b/client/stripe-utils/utils.js index 35bd648583..791e39c6a1 100644 --- a/client/stripe-utils/utils.js +++ b/client/stripe-utils/utils.js @@ -550,7 +550,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 || {}; @@ -558,9 +558,22 @@ export const isPaymentMethodRestrictedToLocation = ( upeElement ) => { return !! paymentMethodsConfig[ paymentMethodType ]?.countries.length; }; +/** + * 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 || {}; @@ -581,6 +594,16 @@ 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 + '"]' + ); + + console.log( radioButton ); + + if ( radioButton ) { + radioButton.checked = false; + } } }; 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 5c104b81dc..69ab0c3f38 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -502,12 +502,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(), ]; } 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 ba63e9c600..d5f5d63ba9 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method.php @@ -725,4 +725,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; + } } From 4471721b88f925817202fd5ff2839f505df5ccb4 Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Fri, 31 Jan 2025 20:48:57 -0300 Subject: [PATCH 06/24] Refactor create intent and mandate options --- client/api/index.js | 8 +-- client/classic/upe/payment-processing.js | 29 +++++----- .../class-wc-stripe-intent-controller.php | 54 ++++++++++++------- 3 files changed, 51 insertions(+), 40 deletions(-) diff --git a/client/api/index.js b/client/api/index.js index 1aa2890705..7bb973a1f1 100644 --- a/client/api/index.js +++ b/client/api/index.js @@ -136,15 +136,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 {boolean} isAcssDebit Whether the payment method is ACSS Debit. + * @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, isAcssDebit = false ) { + createIntent( orderId = null, paymentMethodType = null ) { return this.request( this.getAjaxUrl( 'create_payment_intent' ), { stripe_order_id: orderId, - is_acss_debit: isAcssDebit, + payment_method_type: paymentMethodType, _ajax_nonce: this.options?.createPaymentIntentNonce, } ) .then( ( response ) => { diff --git a/client/classic/upe/payment-processing.js b/client/classic/upe/payment-processing.js index 2512d446fa..07d685cc2f 100644 --- a/client/classic/upe/payment-processing.js +++ b/client/classic/upe/payment-processing.js @@ -25,14 +25,12 @@ import { PAYMENT_METHOD_ACSS_DEBIT, } from 'wcstripe/stripe-utils/constants'; -let paymentIntentId, clientSecret; - const gatewayUPEComponents = {}; - const paymentMethodsConfig = getStripeServerData()?.paymentMethodsConfig; for ( const paymentMethodType in paymentMethodsConfig ) { gatewayUPEComponents[ paymentMethodType ] = { + intentId: null, elements: null, upeElement: null, }; @@ -84,18 +82,17 @@ async function createStripePaymentElement( api, paymentMethodType = null ) { const paymentMethodTypes = getPaymentMethodTypes( paymentMethodType ); let options; - // ACSS doesn't support deferred intent, so we need to create a PaymentIntent first. - if ( paymentMethodType === PAYMENT_METHOD_ACSS_DEBIT ) { - const response = await api.createIntent( paymentMethodType, true ); - - clientSecret = response.client_secret; - paymentIntentId = response.id; + // If the payment method doesn't support deferred intent, the intent must be created here. + const { supportsDeferredIntent } = paymentMethodsConfig[ paymentMethodType ] || {}; + if ( ! supportsDeferredIntent ) { + const intent = await api.createIntent( null, paymentMethodType ); + gatewayUPEComponents[ paymentMethodType ].intentId = intent.id; options = { appearance: initializeUPEAppearance( api ), paymentMethodCreation: 'manual', fonts: getFontRulesFromPage(), - clientSecret, + clientSecret: intent.client_secret, }; } else { options = { @@ -341,13 +338,6 @@ export const processPayment = ( try { await validateElements( elements ); - // TODO: Refactor this in the right place. - // ACSS Debit requires different handling. - if ( paymentMethodType === PAYMENT_METHOD_ACSS_DEBIT ) { - // Attach payment intent ID to form. - appendPaymentIntentIdToForm( jQueryForm, paymentIntentId ); - } - const paymentMethodObject = await createStripePaymentMethod( api, elements, @@ -360,6 +350,11 @@ export const processPayment = ( paymentMethodObject.paymentMethod.id ); + appendPaymentIntentIdToForm( + jQueryForm, + gatewayUPEComponents[ paymentMethodType ].intentId + ); + let stopFormSubmission = false; await additionalActionsHandler( paymentMethodObject.paymentMethod, diff --git a/includes/class-wc-stripe-intent-controller.php b/includes/class-wc-stripe-intent-controller.php index da35bcc6a7..7d3adfabf6 100644 --- a/includes/class-wc-stripe-intent-controller.php +++ b/includes/class-wc-stripe-intent-controller.php @@ -307,9 +307,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; - // Check if is_acss_debit is set and true. - $is_acss_debit = isset( $_POST['is_acss_debit'] ) && filter_var( $_POST['is_acss_debit'], FILTER_VALIDATE_BOOLEAN ); + $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 ); @@ -318,7 +317,7 @@ public function create_payment_intent_ajax() { } } - wp_send_json_success( $this->create_payment_intent( $order_id, $is_acss_debit ), 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. @@ -335,11 +334,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, $is_acss_debit = false ) { + 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' ) ) { @@ -347,7 +348,7 @@ public function create_payment_intent( $order_id = null, $is_acss_debit = false } $gateway = $this->get_upe_gateway(); - $enabled_payment_methods = $is_acss_debit ? [ 'acss_debit' ] : $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(); @@ -358,17 +359,7 @@ public function create_payment_intent( $order_id = null, $is_acss_debit = false 'capture_method' => $capture ? 'automatic' : 'manual', ]; - if ( $is_acss_debit ) { - $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', - ], - ], - ]; - } + $request = $this->maybe_add_mandate_options( $request, $payment_method_type ); $payment_intent = WC_Stripe_API::request( $request, 'payment_intents' ); @@ -816,6 +807,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 ( $payment_method_type === WC_Stripe_UPE_Payment_Method_ACSS::STRIPE_ID ) { + $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. @@ -970,7 +986,7 @@ 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 and ACSS Debit. * https://docs.stripe.com/payments/finalize-payments-on-the-server * * @param string $selected_payment_type The name of the selected UPE payment type. From c6b920ca02ed2c69e7b831cba97bcd744b2da3da Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Sat, 1 Feb 2025 02:06:15 -0300 Subject: [PATCH 07/24] Refactor payment processing --- client/classic/upe/deferred-intent.js | 4 ++-- .../class-wc-stripe-upe-payment-gateway.php | 11 ++++------- .../class-wc-stripe-upe-payment-method-acss.php | 2 +- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/client/classic/upe/deferred-intent.js b/client/classic/upe/deferred-intent.js index 1ae11ecc62..5020dfbb6c 100644 --- a/client/classic/upe/deferred-intent.js +++ b/client/classic/upe/deferred-intent.js @@ -55,8 +55,8 @@ jQuery( function ( $ ) { maybeMountStripePaymentElement(); } - // Maybe mount the Payment Element when selecting the payment method. - // This is needed for payment methods that don't support deferred intents. + + // 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(); } ); 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 69ab0c3f38..adc1788d7e 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -652,14 +652,11 @@ public function payment_fields() { * @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 ); - - // TODO: Refactor this into the right place. - // If payment method is ACSS debit, handle accordingly. + $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 ( $selected_payment_type === WC_Stripe_UPE_Payment_Method_ACSS::STRIPE_ID ) { - // Attach payment intent to order. + + if ( $payment_intent_id && ! $this->payment_methods[ $selected_payment_type ]->supports_deferred_intent() ) { WC_Stripe_Helper::add_payment_intent_to_order( $payment_intent_id, $order ); } 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 index 9a32a62cbd..3cf72c14ae 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php @@ -16,7 +16,7 @@ class WC_Stripe_UPE_Payment_Method_ACSS extends WC_Stripe_UPE_Payment_Method { public function __construct() { parent::__construct(); $this->stripe_id = self::STRIPE_ID; - $this->title = __( 'Canadian Pre-Autorized Debit', 'woocommerce-gateway-stripe' ); + $this->title = __( 'Pre-Autorized 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' ]; From c409cc19bc7b1137bbd58cfbf2a0713fbb0abe5d Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Mon, 3 Feb 2025 14:09:29 -0300 Subject: [PATCH 08/24] Cleanup --- client/classic/upe/payment-processing.js | 10 ++++++---- client/stripe-utils/constants.js | 4 ++-- client/stripe-utils/utils.js | 2 -- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/classic/upe/payment-processing.js b/client/classic/upe/payment-processing.js index 07d685cc2f..ba5e3230ce 100644 --- a/client/classic/upe/payment-processing.js +++ b/client/classic/upe/payment-processing.js @@ -70,20 +70,22 @@ export function validateElements( elements ) { /** * Creates a Stripe payment element with the specified payment method type and options. * - * If the user is NOT deferring (e.g. ACSS Debit), we call our server to create a PaymentIntent - * and then pass its client_secret to the Payment Element on initialization. + * 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. + * + * 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. */ -async function createStripePaymentElement( api, paymentMethodType = null ) { +async function createStripePaymentElement( api, paymentMethodType ) { const amount = Number( getStripeServerData()?.cartTotal ); const paymentMethodTypes = getPaymentMethodTypes( paymentMethodType ); + const { supportsDeferredIntent } = paymentMethodsConfig[ paymentMethodType ] || {}; let options; // If the payment method doesn't support deferred intent, the intent must be created here. - const { supportsDeferredIntent } = paymentMethodsConfig[ paymentMethodType ] || {}; if ( ! supportsDeferredIntent ) { const intent = await api.createIntent( null, paymentMethodType ); gatewayUPEComponents[ paymentMethodType ].intentId = intent.id; diff --git a/client/stripe-utils/constants.js b/client/stripe-utils/constants.js index 6e46892538..2d05a9cd6d 100644 --- a/client/stripe-utils/constants.js +++ b/client/stripe-utils/constants.js @@ -44,7 +44,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_DEBIT = 'stripe_acss_debit'; +export const PAYMENT_METHOD_STRIPE_ACSS = 'stripe_acss_debit'; export const PAYMENT_METHOD_STRIPE_BACS = 'stripe_bacs_debit'; export function getPaymentMethodsConstants() { @@ -66,7 +66,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_DEBIT, + 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 791e39c6a1..778e886e32 100644 --- a/client/stripe-utils/utils.js +++ b/client/stripe-utils/utils.js @@ -599,8 +599,6 @@ export const togglePaymentMethodForCountry = ( upeElement ) => { 'input[name="payment_method"][value="stripe_' + paymentMethodType + '"]' ); - console.log( radioButton ); - if ( radioButton ) { radioButton.checked = false; } From ac22800015bd9bf26402308fffb5441efb8eba7e Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Mon, 3 Feb 2025 14:20:52 -0300 Subject: [PATCH 09/24] Fix PHP lint issue --- includes/class-wc-stripe-intent-controller.php | 2 +- .../class-wc-stripe-upe-payment-gateway.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/class-wc-stripe-intent-controller.php b/includes/class-wc-stripe-intent-controller.php index 849510feea..67fdcff539 100644 --- a/includes/class-wc-stripe-intent-controller.php +++ b/includes/class-wc-stripe-intent-controller.php @@ -832,7 +832,7 @@ private function add_mandate_data( $request ) { */ private function maybe_add_mandate_options( $request, $payment_method_type ) { // Add required mandate options for ACSS. - if ( $payment_method_type === WC_Stripe_UPE_Payment_Method_ACSS::STRIPE_ID ) { + if ( WC_Stripe_UPE_Payment_Method_ACSS::STRIPE_ID === $payment_method_type ) { $request['payment_method_options'] = [ 'acss_debit' => [ 'mandate_options' => [ 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 fde227b595..25b37fef84 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -653,11 +653,11 @@ 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. */ From b536123931f661d08f41f294fa7bb743b0d13d2f Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Mon, 3 Feb 2025 14:35:17 -0300 Subject: [PATCH 10/24] Fix JS lint errors --- client/classic/upe/deferred-intent.js | 3 +-- client/classic/upe/payment-processing.js | 5 ++--- client/payment-methods-map.js | 5 +---- client/stripe-utils/utils.js | 6 ++++-- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/client/classic/upe/deferred-intent.js b/client/classic/upe/deferred-intent.js index 5020dfbb6c..bcaffb6d46 100644 --- a/client/classic/upe/deferred-intent.js +++ b/client/classic/upe/deferred-intent.js @@ -55,7 +55,6 @@ 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(); @@ -91,7 +90,7 @@ jQuery( function ( $ ) { } const selectedMethod = getSelectedUPEGatewayPaymentMethod(); - for ( const upeElement of $( '.wc-stripe-upe-element').toArray() ) { + for ( const upeElement of $( '.wc-stripe-upe-element' ).toArray() ) { // Don't mount if it's already mounted. if ( $( upeElement ).children().length ) { continue; diff --git a/client/classic/upe/payment-processing.js b/client/classic/upe/payment-processing.js index ba5e3230ce..e1c2e736cc 100644 --- a/client/classic/upe/payment-processing.js +++ b/client/classic/upe/payment-processing.js @@ -22,7 +22,6 @@ import { PAYMENT_METHOD_CASHAPP, PAYMENT_METHOD_MULTIBANCO, PAYMENT_METHOD_WECHAT_PAY, - PAYMENT_METHOD_ACSS_DEBIT, } from 'wcstripe/stripe-utils/constants'; const gatewayUPEComponents = {}; @@ -82,7 +81,8 @@ export function validateElements( elements ) { async function createStripePaymentElement( api, paymentMethodType ) { const amount = Number( getStripeServerData()?.cartTotal ); const paymentMethodTypes = getPaymentMethodTypes( paymentMethodType ); - const { supportsDeferredIntent } = paymentMethodsConfig[ paymentMethodType ] || {}; + const { supportsDeferredIntent } = + paymentMethodsConfig[ paymentMethodType ] || {}; let options; // If the payment method doesn't support deferred intent, the intent must be created here. @@ -638,4 +638,3 @@ export const confirmWalletPayment = async ( api, jQueryForm ) => { resetBlockCheckoutPaymentState(); } }; - diff --git a/client/payment-methods-map.js b/client/payment-methods-map.js index bf49f6f5ac..2ada8bbc0a 100644 --- a/client/payment-methods-map.js +++ b/client/payment-methods-map.js @@ -242,10 +242,7 @@ const paymentMethodsMap = { }, acss_debit: { id: 'acss_debit', - label: __( - 'Pre-Authorized Debit', - 'woocommerce-gateway-stripe' - ), + 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' diff --git a/client/stripe-utils/utils.js b/client/stripe-utils/utils.js index 364630da87..5cfddf51c4 100644 --- a/client/stripe-utils/utils.js +++ b/client/stripe-utils/utils.js @@ -581,7 +581,7 @@ export const paymentMethodSupportsDeferredIntent = ( upeElement ) => { 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. @@ -608,7 +608,9 @@ export const togglePaymentMethodForCountry = ( upeElement ) => { upeContainer.style.display = 'none'; // Also uncheck the radio button if it's selected. const radioButton = document.querySelector( - 'input[name="payment_method"][value="stripe_' + paymentMethodType + '"]' + 'input[name="payment_method"][value="stripe_' + + paymentMethodType + + '"]' ); if ( radioButton ) { From d14ca72aa3bf4df4be23e70dc20897b34ccc899e Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Mon, 3 Feb 2025 15:15:24 -0300 Subject: [PATCH 11/24] Fix PHP unit tests --- ...test-class-wc-stripe-upe-payment-gateway.php | 17 ++++++++++++++--- .../test-class-wc-stripe-upe-payment-method.php | 3 ++- 2 files changed, 16 insertions(+), 4 deletions(-) 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 c0e9223cad..a3bd2c4a76 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 @@ -281,6 +281,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, ], ], [ @@ -295,6 +296,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, ], ], ]; @@ -337,7 +339,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' ) @@ -1778,7 +1783,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' ) @@ -1827,7 +1835,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 a8f2818fed..1c3ecb8f47 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 @@ -118,6 +118,7 @@ class WC_Stripe_UPE_Payment_Method_Test extends WP_UnitTestCase { 'cashapp_payments' => 'active', 'wechat_pay_payments' => 'active', 'us_bank_account_ach_payments' => 'active', + 'acss_debit_payments' => 'active', ]; /** @@ -595,7 +596,7 @@ public function test_payment_methods_are_reusable_if_cart_contains_subscription( $store_currency = WC_Stripe_UPE_Payment_Method_Link::STRIPE_ID === $payment_method_id ? WC_Stripe_Currency_Code::UNITED_STATES_DOLLAR : 'EUR'; $account_currency = null; - if ( $payment_method->has_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; } From 531d2f5302d7e8ac34a03f3b61c3fdbc76916d6d Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Mon, 3 Feb 2025 18:39:07 -0300 Subject: [PATCH 12/24] Fix missing customer and metadata from PI --- .../payment-methods/class-wc-stripe-upe-payment-gateway.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 25b37fef84..68715a3fe3 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -667,7 +667,9 @@ public function process_payment( $order_id, $retry = true, $force_save_source = $selected_payment_type = $this->get_selected_payment_method_type_from_request(); if ( $payment_intent_id && ! $this->payment_methods[ $selected_payment_type ]->supports_deferred_intent() ) { - WC_Stripe_Helper::add_payment_intent_to_order( $payment_intent_id, $order ); + // 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. From fac59c41abfd9ef68e3cb9da3528c6cc7d4343a5 Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Mon, 3 Feb 2025 21:47:56 -0300 Subject: [PATCH 13/24] Standardizing feature flag condition in direct debit PMs --- client/payment-methods-map.js | 31 ++++++++++++------- .../class-wc-stripe-upe-payment-gateway.php | 15 ++++++--- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/client/payment-methods-map.js b/client/payment-methods-map.js index 2ada8bbc0a..0e8bf7f916 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: { @@ -240,18 +242,9 @@ const paymentMethodsMap = { currencies: [ 'USD' ], capability: 'cashapp_payments', }, - 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.card, - currencies: [ 'CAD' ], - }, }; +// Enable ACH according to feature flag value. if ( isAchEnabled ) { paymentMethodsMap.us_bank_account = { id: 'us_bank_account', @@ -265,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.card, + currencies: [ 'CAD' ], + }; +} + +// Enable Bacs according to feature flag value. +if ( isBacsEnabled ) { paymentMethodsMap.bacs_debit = { id: 'bacs_debit', label: 'Bacs Direct Debit', 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 68715a3fe3..b25c2dd734 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -150,11 +150,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. @@ -162,6 +157,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. */ From fae34f649ae0e86eaa3735a1eeba2a67759fd2ea Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Mon, 3 Feb 2025 22:10:16 -0300 Subject: [PATCH 14/24] Fix icon --- client/payment-methods-map.js | 2 +- includes/abstracts/abstract-wc-stripe-payment-gateway.php | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/client/payment-methods-map.js b/client/payment-methods-map.js index 0e8bf7f916..d21b812575 100644 --- a/client/payment-methods-map.js +++ b/client/payment-methods-map.js @@ -267,7 +267,7 @@ if ( isAcssEnabled ) { 'Canadian Pre-Authorized Debit is a payment method that allows customers to pay using their Canadian bank account.', 'woocommerce-gateway-stripe' ), - Icon: icons.card, + Icon: icons.acss_debit, currencies: [ 'CAD' ], }; } diff --git a/includes/abstracts/abstract-wc-stripe-payment-gateway.php b/includes/abstracts/abstract-wc-stripe-payment-gateway.php index bca889a540..5548f7b797 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 => 'ACH', + WC_Stripe_Payment_Methods::ACSS_DEBIT => '' . __( 'Pre-Authorized Debit', 'woocommerce-gateway-stripe' ) . '', WC_Stripe_Payment_Methods::ALIPAY => 'Alipay', WC_Stripe_Payment_Methods::WECHAT_PAY => 'Wechat Pay', WC_Stripe_Payment_Methods::BANCONTACT => 'Bancontact', WC_Stripe_Payment_Methods::IDEAL => 'iDEAL', WC_Stripe_Payment_Methods::P24 => 'P24', WC_Stripe_Payment_Methods::GIROPAY => 'giropay', - WC_Stripe_Payment_Methods::KLARNA => 'klarna', - WC_Stripe_Payment_Methods::AFFIRM => 'affirm', + WC_Stripe_Payment_Methods::KLARNA => 'Klarna', + WC_Stripe_Payment_Methods::AFFIRM => 'Affirm', WC_Stripe_Payment_Methods::EPS => 'EPS', WC_Stripe_Payment_Methods::MULTIBANCO => 'Multibanco', WC_Stripe_Payment_Methods::SOFORT => 'Sofort', WC_Stripe_Payment_Methods::SEPA => 'SEPA', WC_Stripe_Payment_Methods::BOLETO => 'Boleto', WC_Stripe_Payment_Methods::OXXO => 'OXXO', - 'cards' => 'credit / debit card', + 'cards' => '' . __( 'Credit / Debit Card', 'woocommerce-gateway-stripe' ) . '', WC_Stripe_Payment_Methods::CASHAPP_PAY => 'Cash App Pay', ] ); From 3788184c55ac6de77dcea0bcfbb130a9c8d44b90 Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Mon, 3 Feb 2025 22:29:21 -0300 Subject: [PATCH 15/24] Add tests / Fix tests --- .../class-wc-stripe-upe-payment-method-acss.php | 10 +++++++++- .../test-class-wc-stripe-upe-payment-gateway.php | 2 ++ .../test-class-wc-stripe-upe-payment-method.php | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) 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 index 3cf72c14ae..4825026a8b 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php @@ -16,7 +16,7 @@ class WC_Stripe_UPE_Payment_Method_ACSS extends WC_Stripe_UPE_Payment_Method { public function __construct() { parent::__construct(); $this->stripe_id = self::STRIPE_ID; - $this->title = __( 'Pre-Autorized Debit', 'woocommerce-gateway-stripe' ); + $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' ]; @@ -27,4 +27,12 @@ public function __construct() { ); $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/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 a3bd2c4a76..79353a33a1 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' ); $mock_account = $this->getMockBuilder( 'WC_Stripe_Account' ) ->disableOriginalConstructor() @@ -186,6 +187,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 ); } /** 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 1c3ecb8f47..b17bad231e 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 @@ -235,6 +235,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']; @@ -249,6 +252,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() ); @@ -356,6 +360,14 @@ public function test_payment_methods_show_correct_default_outputs() { $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() ); + + $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() ); } /** @@ -387,6 +399,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() ); @@ -403,6 +416,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() ); } /** @@ -596,6 +610,7 @@ public function test_payment_methods_are_reusable_if_cart_contains_subscription( $store_currency = WC_Stripe_UPE_Payment_Method_Link::STRIPE_ID === $payment_method_id ? WC_Stripe_Currency_Code::UNITED_STATES_DOLLAR : 'EUR'; $account_currency = null; + // 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; @@ -619,6 +634,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, From 954242132e622eec5b070d9ecb6d977d50b77ba2 Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Wed, 5 Feb 2025 14:15:59 -0300 Subject: [PATCH 16/24] Handle "payment_intent.processing" webhooks. --- includes/class-wc-stripe-account.php | 1 + includes/class-wc-stripe-webhook-handler.php | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/includes/class-wc-stripe-account.php b/includes/class-wc-stripe-account.php index b27e18ecbb..b227b461ea 100644 --- a/includes/class-wc-stripe-account.php +++ b/includes/class-wc-stripe-account.php @@ -278,6 +278,7 @@ public function configure_webhooks( $mode = 'live', $secret_key = '' ) { '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-webhook-handler.php b/includes/class-wc-stripe-webhook-handler.php index 1a2cce6c58..87f4712bf2 100644 --- a/includes/class-wc-stripe-webhook-handler.php +++ b/includes/class-wc-stripe-webhook-handler.php @@ -991,6 +991,14 @@ 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 ); + $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 ); @@ -1258,6 +1266,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': From e166763c6e4bff8ba3c8f802123390c8977443ad Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Thu, 6 Feb 2025 15:23:56 -0300 Subject: [PATCH 17/24] Add unit test to payment_intent.processing --- .../test-wc-stripe-webhook-handler.php | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/phpunit/test-wc-stripe-webhook-handler.php b/tests/phpunit/test-wc-stripe-webhook-handler.php index fc850e8f67..df20472b33 100644 --- a/tests/phpunit/test-wc-stripe-webhook-handler.php +++ b/tests/phpunit/test-wc-stripe-webhook-handler.php @@ -473,6 +473,52 @@ 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`. * From 825ecea0e298f94101ceebfa33dac53c17e6e60b Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Thu, 6 Feb 2025 15:42:03 -0300 Subject: [PATCH 18/24] Add ifs for better coverage --- client/classic/upe/payment-processing.js | 11 +++++++---- includes/class-wc-stripe-webhook-handler.php | 8 +++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/client/classic/upe/payment-processing.js b/client/classic/upe/payment-processing.js index e1c2e736cc..b54b3a2e45 100644 --- a/client/classic/upe/payment-processing.js +++ b/client/classic/upe/payment-processing.js @@ -352,10 +352,13 @@ export const processPayment = ( paymentMethodObject.paymentMethod.id ); - appendPaymentIntentIdToForm( - jQueryForm, - gatewayUPEComponents[ paymentMethodType ].intentId - ); + // 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( diff --git a/includes/class-wc-stripe-webhook-handler.php b/includes/class-wc-stripe-webhook-handler.php index 87f4712bf2..35ec7b0b65 100644 --- a/includes/class-wc-stripe-webhook-handler.php +++ b/includes/class-wc-stripe-webhook-handler.php @@ -995,9 +995,11 @@ public function process_payment_intent( $notification ) { // 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 ); - $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 ) ); + 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 ); From f539b4c4e3203cd442f82e04a046e2b7ed830d02 Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Thu, 6 Feb 2025 15:57:52 -0300 Subject: [PATCH 19/24] Fix PHP lint issue --- tests/phpunit/test-wc-stripe-webhook-handler.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/test-wc-stripe-webhook-handler.php b/tests/phpunit/test-wc-stripe-webhook-handler.php index df20472b33..857eaf050d 100644 --- a/tests/phpunit/test-wc-stripe-webhook-handler.php +++ b/tests/phpunit/test-wc-stripe-webhook-handler.php @@ -513,7 +513,12 @@ public function test_process_webhook_payment_intent_processing() { $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 ] ); + $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 ); } From 35dd03b4b6ae996a95ebff574f4e9a2c7ccfd51a Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Fri, 7 Feb 2025 13:45:44 -0300 Subject: [PATCH 20/24] Update client/stripe-utils/utils.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: César Costa <10233985+cesarcosta99@users.noreply.github.com> --- client/stripe-utils/utils.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/stripe-utils/utils.js b/client/stripe-utils/utils.js index ed6a02977d..6d4cb36d2c 100644 --- a/client/stripe-utils/utils.js +++ b/client/stripe-utils/utils.js @@ -606,9 +606,7 @@ export const togglePaymentMethodForCountry = ( upeElement ) => { upeContainer.style.display = 'none'; // Also uncheck the radio button if it's selected. const radioButton = document.querySelector( - 'input[name="payment_method"][value="stripe_' + - paymentMethodType + - '"]' + `input[name="payment_method"][value="stripe_${ paymentMethodType }"]` ); if ( radioButton ) { From c97dec5fc6371da25d42e27cd0c88d5e46b55114 Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Mon, 10 Feb 2025 13:26:12 -0300 Subject: [PATCH 21/24] Update includes/class-wc-stripe-intent-controller.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: César Costa <10233985+cesarcosta99@users.noreply.github.com> --- includes/class-wc-stripe-intent-controller.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/class-wc-stripe-intent-controller.php b/includes/class-wc-stripe-intent-controller.php index 67fdcff539..d8c8528b8e 100644 --- a/includes/class-wc-stripe-intent-controller.php +++ b/includes/class-wc-stripe-intent-controller.php @@ -1001,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, Link payment methods and ACSS Debit. + * 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. From 87f4feb3dbfb029acc918b58729026045ecbcdbe Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Mon, 10 Feb 2025 13:58:24 -0300 Subject: [PATCH 22/24] Fix restrict payment method to country --- client/classic/upe/deferred-intent.js | 4 +++- .../class-wc-stripe-upe-payment-method-acss.php | 16 ++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/client/classic/upe/deferred-intent.js b/client/classic/upe/deferred-intent.js index bcaffb6d46..3ae3830f6d 100644 --- a/client/classic/upe/deferred-intent.js +++ b/client/classic/upe/deferred-intent.js @@ -91,6 +91,9 @@ jQuery( function ( $ ) { 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; @@ -105,7 +108,6 @@ jQuery( function ( $ ) { } await mountStripePaymentElement( api, upeElement ); - restrictPaymentMethodToLocation( upeElement ); } } 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 index 4825026a8b..2ab20fb901 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php @@ -15,17 +15,17 @@ class WC_Stripe_UPE_Payment_Method_ACSS extends WC_Stripe_UPE_Payment_Method { */ public function __construct() { parent::__construct(); - $this->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 = __( + $this->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; + $this->supports_deferred_intent = false; } /** From 106d8cd04cc0ce050be34a9b27f00dbd8bbd4c18 Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Tue, 11 Feb 2025 10:07:05 -0300 Subject: [PATCH 23/24] Add link to issue for "wc-stripe-is-deferred-intent" --- includes/payment-methods/class-wc-stripe-upe-payment-gateway.php | 1 + 1 file changed, 1 insertion(+) 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 160ed1f3a2..9e82ddd19f 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -694,6 +694,7 @@ public function process_payment( $order_id, $retry = true, $force_save_source = } // 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 ); } From 11f2925f20319d747306696ec10e33d3ea4e5c02 Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Thu, 13 Feb 2025 06:54:30 -0300 Subject: [PATCH 24/24] Fix lint error --- ...est-class-wc-stripe-upe-payment-method.php | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) 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 ea6bed6d4b..5ba2a225a3 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 @@ -98,27 +98,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_ach_payments' => 'active', - 'acss_debit_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', ]; /**