Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ACSS payment method and refactor payment processing with deferred intent #3805

Open
wants to merge 27 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3831008
PoC: Add ACSS Debit payment method
ricardo Jan 22, 2025
5c7b815
Work around deferred intents
ricardo Jan 23, 2025
b65e7d2
Changes required for ACSS
ricardo Jan 25, 2025
838f9b3
Merge branch 'develop' into poc/3708-enable-pre-auth-pm
ricardo Jan 30, 2025
b2d42b5
Some code cleanup for ACSS
ricardo Jan 30, 2025
09fbf6e
Only mount UPE when the payment element is selected on the classic ch…
ricardo Jan 31, 2025
4471721
Refactor create intent and mandate options
ricardo Jan 31, 2025
c6b920c
Refactor payment processing
ricardo Feb 1, 2025
c409cc1
Cleanup
ricardo Feb 3, 2025
4984b33
Merge branch 'develop' into fix/3804-refactor-deferred-intent
ricardo Feb 3, 2025
ac22800
Fix PHP lint issue
ricardo Feb 3, 2025
b536123
Fix JS lint errors
ricardo Feb 3, 2025
d14ca72
Fix PHP unit tests
ricardo Feb 3, 2025
531d2f5
Fix missing customer and metadata from PI
ricardo Feb 3, 2025
fac59c4
Standardizing feature flag condition in direct debit PMs
ricardo Feb 4, 2025
fae34f6
Fix icon
ricardo Feb 4, 2025
3788184
Add tests / Fix tests
ricardo Feb 4, 2025
fe861c3
Merge branch 'develop' into fix/3804-refactor-deferred-intent
ricardo Feb 4, 2025
9542421
Handle "payment_intent.processing" webhooks.
ricardo Feb 5, 2025
af3d876
Merge branch 'develop' into fix/3804-refactor-deferred-intent
ricardo Feb 5, 2025
e166763
Add unit test to payment_intent.processing
ricardo Feb 6, 2025
e066659
Merge branch 'develop' into fix/3804-refactor-deferred-intent
wjrosa Feb 6, 2025
825ecea
Add ifs for better coverage
ricardo Feb 6, 2025
c67e0fb
Merge branch 'develop' into fix/3804-refactor-deferred-intent
ricardo Feb 6, 2025
a0ad301
Merge branch 'fix/3804-refactor-deferred-intent' of github.com:woocom…
ricardo Feb 6, 2025
f539b4c
Fix PHP lint issue
ricardo Feb 6, 2025
35dd03b
Update client/stripe-utils/utils.js
ricardo Feb 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
42 changes: 29 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,38 @@ 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 );
}
}

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;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is similar to how it used to work before #2935, but instead of saving the PI ID to a local variable, we save the intent ID to each specific UPE component object (gatewayUPEComponents), so we can render the PE only when the PM is selected, and have a different PI for each split UPE that doesn't support deferred intents (In this case it will be only BLIK and ACSS).


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" />',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Thanks for fixing this

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 @@ -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',
Expand Down
Loading
Loading