Skip to content

Commit

Permalink
Add ACSS payment method and refactor payment processing with deferred…
Browse files Browse the repository at this point in the history
… intent (#3805)

* PoC: Add ACSS Debit payment method

* Work around deferred intents

* Changes required for ACSS

* Some code cleanup for ACSS

* Only mount UPE when the payment element is selected on the classic checkout

* Refactor create intent and mandate options

* Refactor payment processing

* Cleanup

* Fix PHP lint issue

* Fix JS lint errors

* Fix PHP unit tests

* Fix missing customer and metadata from PI

* Standardizing feature flag condition in direct debit PMs

* Fix icon

* Add tests / Fix tests

* Handle "payment_intent.processing" webhooks.

* Add unit test to payment_intent.processing

* Add ifs for better coverage

* Fix PHP lint issue

* Update client/stripe-utils/utils.js

Co-authored-by: César Costa <10233985+cesarcosta99@users.noreply.github.com>

* Update includes/class-wc-stripe-intent-controller.php

Co-authored-by: César Costa <10233985+cesarcosta99@users.noreply.github.com>

* Fix restrict payment method to country

* Add link to issue for "wc-stripe-is-deferred-intent"

---------

Co-authored-by: Wesley Rosa <wesleyjrosa@gmail.com>
Co-authored-by: César Costa <10233985+cesarcosta99@users.noreply.github.com>
  • Loading branch information
3 people authored Feb 13, 2025
1 parent fd2a7bb commit 4a17680
Show file tree
Hide file tree
Showing 19 changed files with 391 additions and 99 deletions.
6 changes: 4 additions & 2 deletions client/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,15 @@ export default class WCStripeAPI {
/**
* Creates an intent based on a payment method.
*
* @param {number} orderId The id of the order if creating the intent on Order Pay page.
* @param {number|null} orderId The id of the order if creating the intent on Order Pay page.
* @param {string|null} paymentMethodType The type of payment method.
*
* @return {Promise} The final promise for the request to the server.
*/
createIntent( orderId ) {
createIntent( orderId = null, paymentMethodType = null ) {
return this.request( this.getAjaxUrl( 'create_payment_intent' ), {
stripe_order_id: orderId,
payment_method_type: paymentMethodType,
_ajax_nonce: this.options?.createPaymentIntentNonce,
} )
.then( ( response ) => {
Expand Down
44 changes: 31 additions & 13 deletions client/classic/upe/deferred-intent.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getStripeServerData,
isPaymentMethodRestrictedToLocation,
isUsingSavedPaymentMethod,
paymentMethodSupportsDeferredIntent,
togglePaymentMethodForCountry,
} from '../../stripe-utils';
import './style.scss';
Expand Down Expand Up @@ -54,6 +55,11 @@ jQuery( function ( $ ) {
maybeMountStripePaymentElement();
}

// For payment methods that don't support deferred intents, we mount the Payment Element only when it's selected.
$( 'form.checkout' ).on( 'change', 'input[name="payment_method"]', () => {
maybeMountStripePaymentElement();
} );

// My Account > Payment Methods page submit.
$( 'form#add_payment_method' ).on( 'submit', function () {
return processPayment(
Expand All @@ -76,28 +82,40 @@ jQuery( function ( $ ) {
}
} );

// If the card element selector doesn't exist, then do nothing.
// For example, when a 100% discount coupon is applied).
// We also don't re-mount if already mounted in DOM.
async function maybeMountStripePaymentElement() {
if (
$( '.wc-stripe-upe-element' ).length &&
! $( '.wc-stripe-upe-element' ).children().length
) {
for ( const upeElement of $(
'.wc-stripe-upe-element'
).toArray() ) {
await mountStripePaymentElement( api, upeElement );
restrictPaymentMethodToLocation( upeElement );
// If the card element selector doesn't exist, do nothing.
// For example, when a 100% discount coupon is applied.
if ( ! $( '.wc-stripe-upe-element' ).length ) {
return;
}

const selectedMethod = getSelectedUPEGatewayPaymentMethod();
for ( const upeElement of $( '.wc-stripe-upe-element' ).toArray() ) {
// Maybe hide the payment method based on the billing country.
restrictPaymentMethodToLocation( upeElement );

// Don't mount if it's already mounted.
if ( $( upeElement ).children().length ) {
continue;
}

// Payment methods that don't support deferred intents don't need to be mounted unless they are selected.
if (
upeElement.dataset.paymentMethodType !== selectedMethod &&
! paymentMethodSupportsDeferredIntent( upeElement )
) {
continue;
}

await mountStripePaymentElement( api, upeElement );
}
}

function restrictPaymentMethodToLocation( upeElement ) {
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 );
} );
Expand Down
57 changes: 42 additions & 15 deletions client/classic/upe/payment-processing.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { __, sprintf } from '@wordpress/i18n';
import {
appendPaymentMethodIdToForm,
appendPaymentIntentIdToForm,
getPaymentMethodTypes,
initializeUPEAppearance,
isLinkEnabled,
Expand All @@ -24,11 +25,11 @@ import {
} from 'wcstripe/stripe-utils/constants';

const gatewayUPEComponents = {};

const paymentMethodsConfig = getStripeServerData()?.paymentMethodsConfig;

for ( const paymentMethodType in paymentMethodsConfig ) {
gatewayUPEComponents[ paymentMethodType ] = {
intentId: null,
elements: null,
upeElement: null,
};
Expand Down Expand Up @@ -66,28 +67,46 @@ export function validateElements( elements ) {
}

/**
* Creates a Stripe payment element with the specified payment method type and options. The function
* retrieves the necessary data from the UPE configuration and initializes the appearance. It then creates the
* Stripe elements and the Stripe payment element, which is attached to the gatewayUPEComponents object afterward.
* Creates a Stripe payment element with the specified payment method type and options.
*
* If the payment method doesn't support deferred intent, the intent must be created first.
* Then, the payment element is created with the intent's client secret.
*
* @todo Make paymentMethodType required when Split is implemented.
* Finally, the payment element is mounted and attached to the gatewayUPEComponents object.
*
* @param {Object} api The API object used to create the Stripe payment element.
* @param {string} paymentMethodType The type of Stripe payment method to create.
* @return {Object} A promise that resolves with the created Stripe payment element.
*/
function createStripePaymentElement( api, paymentMethodType = null ) {
async function createStripePaymentElement( api, paymentMethodType ) {
const amount = Number( getStripeServerData()?.cartTotal );
const paymentMethodTypes = getPaymentMethodTypes( paymentMethodType );
const options = {
mode: amount < 1 ? 'setup' : 'payment',
currency: getStripeServerData()?.currency.toLowerCase(),
amount,
paymentMethodCreation: 'manual',
paymentMethodTypes,
appearance: initializeUPEAppearance( api ),
fonts: getFontRulesFromPage(),
};
const { supportsDeferredIntent } =
paymentMethodsConfig[ paymentMethodType ] || {};
let options;

// If the payment method doesn't support deferred intent, the intent must be created here.
if ( ! supportsDeferredIntent ) {
const intent = await api.createIntent( null, paymentMethodType );
gatewayUPEComponents[ paymentMethodType ].intentId = intent.id;

options = {
appearance: initializeUPEAppearance( api ),
paymentMethodCreation: 'manual',
fonts: getFontRulesFromPage(),
clientSecret: intent.client_secret,
};
} else {
options = {
mode: amount < 1 ? 'setup' : 'payment',
currency: getStripeServerData()?.currency.toLowerCase(),
amount,
paymentMethodCreation: 'manual',
paymentMethodTypes,
appearance: initializeUPEAppearance( api ),
fonts: getFontRulesFromPage(),
};
}

const elements = api.getStripe().elements( options );

Expand Down Expand Up @@ -333,6 +352,14 @@ export const processPayment = (
paymentMethodObject.paymentMethod.id
);

// Append the intent ID to the form if it was previously created through a non-deferred intent.
if ( gatewayUPEComponents[ paymentMethodType ].intentId ) {
appendPaymentIntentIdToForm(
jQueryForm,
gatewayUPEComponents[ paymentMethodType ].intentId
);
}

let stopFormSubmission = false;
await additionalActionsHandler(
paymentMethodObject.paymentMethod,
Expand Down
1 change: 1 addition & 0 deletions client/payment-method-icons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ export default {
cashapp: CashAppIcon,
us_bank_account: BankDebitIcon,
bacs_debit: BankDebitIcon,
acss_debit: BankDebitIcon,
};
21 changes: 19 additions & 2 deletions client/payment-methods-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -242,6 +244,7 @@ const paymentMethodsMap = {
},
};

// Enable ACH according to feature flag value.
if ( isAchEnabled ) {
paymentMethodsMap.us_bank_account = {
id: 'us_bank_account',
Expand All @@ -255,8 +258,22 @@ if ( isAchEnabled ) {
};
}

// Enable Bacs according to feature flag value
if ( window.wc_stripe_settings_params?.is_bacs_enabled ) {
// Enable ACSS according to feature flag value.
if ( isAcssEnabled ) {
paymentMethodsMap.acss_debit = {
id: 'acss_debit',
label: __( 'Pre-Authorized Debit', 'woocommerce-gateway-stripe' ),
description: __(
'Canadian Pre-Authorized Debit is a payment method that allows customers to pay using their Canadian bank account.',
'woocommerce-gateway-stripe'
),
Icon: icons.acss_debit,
currencies: [ 'CAD' ],
};
}

// Enable Bacs according to feature flag value.
if ( isBacsEnabled ) {
paymentMethodsMap.bacs_debit = {
id: 'bacs_debit',
label: 'Bacs Direct Debit',
Expand Down
2 changes: 2 additions & 0 deletions client/stripe-utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const PAYMENT_METHOD_STRIPE_AFTERPAY_CLEARPAY =
'stripe_afterpay_clearpay';
export const PAYMENT_METHOD_STRIPE_WECHAT_PAY = 'stripe_wechat_pay';
export const PAYMENT_METHOD_STRIPE_CASHAPP = 'stripe_cashapp';
export const PAYMENT_METHOD_STRIPE_ACSS = 'stripe_acss_debit';
export const PAYMENT_METHOD_STRIPE_BACS = 'stripe_bacs_debit';

export function getPaymentMethodsConstants() {
Expand All @@ -67,6 +68,7 @@ export function getPaymentMethodsConstants() {
afterpay_clearpay: PAYMENT_METHOD_STRIPE_AFTERPAY_CLEARPAY,
wechat_pay: PAYMENT_METHOD_STRIPE_WECHAT_PAY,
cashapp: PAYMENT_METHOD_STRIPE_CASHAPP,
acss_debit: PAYMENT_METHOD_STRIPE_ACSS,
bacs_debit: PAYMENT_METHOD_STRIPE_BACS,
};
}
Expand Down
31 changes: 29 additions & 2 deletions client/stripe-utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,12 @@ export const appendPaymentMethodIdToForm = ( form, paymentMethodId ) => {
);
};

export const appendPaymentIntentIdToForm = ( form, paymentIntentId ) => {
form.append(
`<input type="hidden" id="wc_payment_intent_id" name="wc_payment_intent_id" value="${ paymentIntentId }" />`
);
};

export const appendSetupIntentToForm = ( form, setupIntent ) => {
form.append(
`<input type="hidden" id="wc-stripe-setup-intent" name="wc-stripe-setup-intent" value="${ setupIntent.id }" />`
Expand Down Expand Up @@ -554,7 +560,7 @@ export const getPaymentMethodName = ( paymentMethodType ) => {
*
* @param {Object} upeElement The selector of the DOM element of particular payment method to mount the UPE element to.
* @return {boolean} Whether the payment method is restricted to selected billing country.
**/
*/
export const isPaymentMethodRestrictedToLocation = ( upeElement ) => {
const paymentMethodsConfig =
getStripeServerData()?.paymentMethodsConfig || {};
Expand All @@ -563,8 +569,21 @@ export const isPaymentMethodRestrictedToLocation = ( upeElement ) => {
};

/**
* Determines if the payment method supports deferred intent.
*
* @param {Object} upeElement The selector of the DOM element of particular payment method to mount the UPE element to.
**/
* @return {boolean} Whether the payment method supports deferred intent.
*/
export const paymentMethodSupportsDeferredIntent = ( upeElement ) => {
const paymentMethodsConfig =
getStripeServerData()?.paymentMethodsConfig || {};
const paymentMethodType = upeElement.dataset.paymentMethodType;
return !! paymentMethodsConfig[ paymentMethodType ]?.supportsDeferredIntent;
};

/**
* @param {Object} upeElement The selector of the DOM element of particular payment method to mount the UPE element to.
*/
export const togglePaymentMethodForCountry = ( upeElement ) => {
const paymentMethodsConfig =
getStripeServerData()?.paymentMethodsConfig || {};
Expand All @@ -585,6 +604,14 @@ export const togglePaymentMethodForCountry = ( upeElement ) => {
upeContainer.style.display = 'block';
} else {
upeContainer.style.display = 'none';
// Also uncheck the radio button if it's selected.
const radioButton = document.querySelector(
`input[name="payment_method"][value="stripe_${ paymentMethodType }"]`
);

if ( radioButton ) {
radioButton.checked = false;
}
}
};

Expand Down
7 changes: 4 additions & 3 deletions includes/abstracts/abstract-wc-stripe-payment-gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -338,21 +338,22 @@ public function payment_icons() {
'wc_stripe_payment_icons',
[
WC_Stripe_Payment_Methods::ACH => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/bank-debit.svg" class="stripe-ach-icon stripe-icon" alt="ACH" />',
WC_Stripe_Payment_Methods::ACSS_DEBIT => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/bank-debit.svg" class="stripe-ach-icon stripe-icon" alt="' . __( 'Pre-Authorized Debit', 'woocommerce-gateway-stripe' ) . '" />',
WC_Stripe_Payment_Methods::ALIPAY => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/alipay.svg" class="stripe-alipay-icon stripe-icon" alt="Alipay" />',
WC_Stripe_Payment_Methods::WECHAT_PAY => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/wechat.svg" class="stripe-wechat-icon stripe-icon" alt="Wechat Pay" />',
WC_Stripe_Payment_Methods::BANCONTACT => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/bancontact.svg" class="stripe-bancontact-icon stripe-icon" alt="Bancontact" />',
WC_Stripe_Payment_Methods::IDEAL => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/ideal.svg" class="stripe-ideal-icon stripe-icon" alt="iDEAL" />',
WC_Stripe_Payment_Methods::P24 => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/p24.svg" class="stripe-p24-icon stripe-icon" alt="P24" />',
WC_Stripe_Payment_Methods::GIROPAY => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/giropay.svg" class="stripe-giropay-icon stripe-icon" alt="giropay" />',
WC_Stripe_Payment_Methods::KLARNA => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/klarna.svg" class="stripe-klarna-icon stripe-icon" alt="klarna" />',
WC_Stripe_Payment_Methods::AFFIRM => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/affirm.svg" class="stripe-affirm-icon stripe-icon" alt="affirm" />',
WC_Stripe_Payment_Methods::KLARNA => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/klarna.svg" class="stripe-klarna-icon stripe-icon" alt="Klarna" />',
WC_Stripe_Payment_Methods::AFFIRM => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/affirm.svg" class="stripe-affirm-icon stripe-icon" alt="Affirm" />',
WC_Stripe_Payment_Methods::EPS => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/eps.svg" class="stripe-eps-icon stripe-icon" alt="EPS" />',
WC_Stripe_Payment_Methods::MULTIBANCO => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/multibanco.svg" class="stripe-multibanco-icon stripe-icon" alt="Multibanco" />',
WC_Stripe_Payment_Methods::SOFORT => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/sofort.svg" class="stripe-sofort-icon stripe-icon" alt="Sofort" />',
WC_Stripe_Payment_Methods::SEPA => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/sepa.svg" class="stripe-sepa-icon stripe-icon" alt="SEPA" />',
WC_Stripe_Payment_Methods::BOLETO => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/boleto.svg" class="stripe-boleto-icon stripe-icon" alt="Boleto" />',
WC_Stripe_Payment_Methods::OXXO => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/oxxo.svg" class="stripe-oxxo-icon stripe-icon" alt="OXXO" />',
'cards' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/cards.svg" class="stripe-cards-icon stripe-icon" alt="credit / debit card" />',
'cards' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/cards.svg" class="stripe-cards-icon stripe-icon" alt="' . __( 'Credit / Debit Card', 'woocommerce-gateway-stripe' ) . '" />',
WC_Stripe_Payment_Methods::CASHAPP_PAY => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/cashapp.svg" class="stripe-cashapp-icon stripe-icon" alt="Cash App Pay" />',
]
);
Expand Down
1 change: 1 addition & 0 deletions includes/class-wc-stripe-account.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class WC_Stripe_Account {
'charge.refund.updated',
'review.opened',
'review.closed',
'payment_intent.processing',
'payment_intent.succeeded',
'payment_intent.payment_failed',
'payment_intent.amount_capturable_updated',
Expand Down
Loading

0 comments on commit 4a17680

Please sign in to comment.