Skip to content

Commit

Permalink
ACSS: Handle errors and edge cases (#3870)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

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

Co-authored-by: César Costa <[email protected]>

* Fix restrict payment method to country

* Conditional payment intent creation based on supports deferred intent flag

* Submit PI id to checkout

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

* Fix PE fonts in blocks checkout

* Implement loading mask to PM while PI ID isn't available

* Add issue about error handling when creating an intent

* Display error message when the intent creation fails in the classic checkout

* Display error when PI fails to be created

* Prevent retrying payments for specific error codes

* Add changelog entries

* Enhance error handling for payment methods and improve user feedback

* Add error handling for payment processor load issues in blocks

* Refactor payment processor error handling to display error messages conditionally

* Move container check to beginning of function

---------

Co-authored-by: Wesley Rosa <[email protected]>
Co-authored-by: César Costa <[email protected]>
  • Loading branch information
3 people authored Mar 3, 2025
1 parent 0c51fcc commit df8249c
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 38 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ module.exports = {
// This helps the `import/no-extraneous-dependencies` and
//`import/no-unresolved` rules account for them.
'import/core-modules': [
'@woocommerce/blocks-checkout',
'@woocommerce/blocks-registry',
'@woocommerce/settings',
'@wordpress/i18n',
Expand Down
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
* Add - Use idempotency keys when creating payment intents, to help prevent duplicate charges for a single order.
* Fix - Allow to save card during checkout with account creation.
* Add - Add BLIK LPM feature flag.
* Fix - ACSS: Handle errors and edge cases.

= 9.2.0 - 2025-02-13 =
* Fix - Fix missing product_id parameter for the express checkout add-to-cart operation.
Expand Down
67 changes: 53 additions & 14 deletions client/blocks/upe/upe-deferred-intent-creation/payment-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
* External dependencies
*/
import { useEffect, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { StoreNotice } from '@woocommerce/blocks-checkout';
import { Elements } from '@stripe/react-stripe-js';
/**
* Internal dependencies
Expand All @@ -21,23 +22,29 @@ import { getFontRulesFromPage } from 'wcstripe/styles/upe';
*
* @param {*} props Additional props for payment processing.
* @param {WCStripeAPI} props.api Object containing methods for interacting with Stripe.
* @param {Object} props.components Object containing components for rendering.
* @param {string} props.paymentMethodId The ID of the payment method.
* @param {boolean} props.supportsDeferredIntent Whether the payment method supports deferred intent creation.
* @param {Object} props.components Object containing components for rendering.
*
* @return {JSX.Element} Rendered Payment elements.
*/
const PaymentElements = ( {
api,
components: { LoadingMask },
paymentMethodId,
supportsDeferredIntent,
components: { LoadingMask },
...props
} ) => {
const [ clientSecret, setClientSecret ] = useState( null );
const [ paymentIntentId, setPaymentIntentId ] = useState( null );
const [ hasRequestedIntent, setHasRequestedIntent ] = useState( false );

const [ errorMessage, setErrorMessage ] = useState( null );
const [
paymentProcessorLoadErrorMessage,
setPaymentProcessorLoadErrorMessage,
] = useState( null );

useEffect( () => {
if ( supportsDeferredIntent || hasRequestedIntent ) {
return;
Expand All @@ -53,9 +60,21 @@ const PaymentElements = ( {
setClientSecret( response.client_secret );
setPaymentIntentId( response.id );
} catch ( error ) {
// TODO: Gracefully handle errors.
// https://github.com/woocommerce/woocommerce-gateway-stripe/issues/3830
console.log( 'error', error ); // eslint-disable-line no-console
const paymentMethodTitle =
getBlocksConfiguration()?.paymentMethodsConfig?.[
paymentMethodId
]?.title ?? '';
setErrorMessage(
error?.message ??
sprintf(
// translators: %s is the payment method title.
__(
'Failed to load %s payment method. Please refresh the page and try again.',
'woocommerce-gateway-stripe'
),
paymentMethodTitle
)
);
}
}

Expand All @@ -69,6 +88,16 @@ const PaymentElements = ( {
supportsDeferredIntent,
] );

if ( errorMessage ) {
return (
<div className="wc-block-components-notices">
<StoreNotice status="error" isDismissible={ false }>
{ errorMessage }
</StoreNotice>
</div>
);
}

// If a client secret is required, wait until it is available.
if ( ! supportsDeferredIntent && ! clientSecret ) {
return (
Expand Down Expand Up @@ -112,14 +141,24 @@ const PaymentElements = ( {
};

return (
<Elements stripe={ stripe } options={ options }>
<PaymentProcessor
api={ api }
paymentIntentId={ paymentIntentId }
paymentMethodId={ paymentMethodId }
{ ...props }
/>
</Elements>
<>
{ paymentProcessorLoadErrorMessage?.error?.message && (
<div className="wc-block-components-notices">
<StoreNotice status="error" isDismissible={ false }>
{ paymentProcessorLoadErrorMessage.error.message }
</StoreNotice>
</div>
) }
<Elements stripe={ stripe } options={ options }>
<PaymentProcessor
api={ api }
paymentIntentId={ paymentIntentId }
paymentMethodId={ paymentMethodId }
onLoadError={ setPaymentProcessorLoadErrorMessage }
{ ...props }
/>
</Elements>
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
useStripe,
Elements,
} from '@stripe/react-stripe-js';
import { useEffect, useState } from 'react';
import { useEffect, useState, useRef } from 'react';
/**
* Internal dependencies
*/
Expand All @@ -23,6 +23,7 @@ import {
import { isLinkEnabled } from 'wcstripe/stripe-utils';
import { PAYMENT_METHOD_CASHAPP } from 'wcstripe/stripe-utils/constants';

const noop = () => null;
/**
* Gets the Stripe element options.
*
Expand Down Expand Up @@ -124,6 +125,7 @@ const PaymentProcessor = ( {
shouldSavePayment,
fingerprint,
billing,
onLoadError = noop,
} ) => {
const stripe = useStripe();
const elements = useElements();
Expand All @@ -146,6 +148,13 @@ const PaymentProcessor = ( {
shouldSavePayment =
shouldSavePayment || getBlocksConfiguration()?.cartContainsSubscription;

const hasLoadErrorRef = useRef( false );

const setHasLoadError = ( event ) => {
hasLoadErrorRef.current = true;
onLoadError( event );
};

useEffect(
() =>
onPaymentSetup( () => {
Expand All @@ -156,6 +165,16 @@ const PaymentProcessor = ( {
return;
}

if ( hasLoadErrorRef.current ) {
return {
type: 'error',
message: __(
'Invalid or missing payment details. Please ensure the provided payment method is correctly entered.',
'woocommerce-gateway-stripe'
),
};
}

if ( ! isPaymentElementComplete ) {
return {
type: 'error',
Expand All @@ -173,6 +192,7 @@ const PaymentProcessor = ( {
};
}

// Check if user tried to save a method that isn’t reusable.
if (
gatewayConfig.supports.showSaveOption &&
shouldSavePayment &&
Expand Down Expand Up @@ -314,6 +334,7 @@ const PaymentProcessor = ( {
<PaymentElement
options={ getStripeElementOptions() }
onChange={ onSelectedPaymentMethodChange }
onLoadError={ setHasLoadError }
className="wcstripe-payment-element"
/>
</>
Expand Down
54 changes: 46 additions & 8 deletions client/classic/upe/payment-processing.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getStripeServerData,
getUpeSettings,
showErrorCheckout,
showErrorPaymentMethod,
appendSetupIntentToForm,
unblockBlockCheckout,
resetBlockCheckoutPaymentState,
Expand All @@ -32,6 +33,7 @@ for ( const paymentMethodType in paymentMethodsConfig ) {
intentId: null,
elements: null,
upeElement: null,
hasLoadError: false,
};
}

Expand Down Expand Up @@ -79,17 +81,32 @@ export function validateElements( elements ) {
* @return {Object} A promise that resolves with the created Stripe payment element.
*/
async function createStripePaymentElement( api, paymentMethodType ) {
const amount = Number( getStripeServerData()?.cartTotal );
const paymentMethodTypes = getPaymentMethodTypes( paymentMethodType );
const { supportsDeferredIntent } =
paymentMethodsConfig[ paymentMethodType ] || {};
let options;
let intent, options;

// If the payment method doesn't support deferred intent, the intent must be created here.
if ( ! supportsDeferredIntent ) {
// TODO: Gracefully handle errors related to the intent creation.
// https://github.com/woocommerce/woocommerce-gateway-stripe/issues/3830
const intent = await api.createIntent( null, paymentMethodType );
try {
intent = await api.createIntent( null, paymentMethodType );
} catch ( error ) {
showErrorPaymentMethod(
error?.message ??
sprintf(
// translators: %s is the payment method title.
__(
'Failed to load %s payment method. Please refresh the page and try again.',
'woocommerce-gateway-stripe'
),
paymentMethodsConfig?.[ paymentMethodType ]?.title ?? ''
),
'.payment_box.payment_method_stripe_' + paymentMethodType
);
// Setting the flag to true to prevent the form from being submitted.
gatewayUPEComponents[ paymentMethodType ].hasLoadError = true;
return;
}

gatewayUPEComponents[ paymentMethodType ].intentId = intent.id;

options = {
Expand All @@ -99,6 +116,9 @@ async function createStripePaymentElement( api, paymentMethodType ) {
clientSecret: intent.client_secret,
};
} else {
const amount = Number( getStripeServerData()?.cartTotal );
const paymentMethodTypes = getPaymentMethodTypes( paymentMethodType );

options = {
mode: amount < 1 ? 'setup' : 'payment',
currency: getStripeServerData()?.currency.toLowerCase(),
Expand Down Expand Up @@ -263,7 +283,13 @@ export async function mountStripePaymentElement( api, domElement ) {
const upeElement =
gatewayUPEComponents[ paymentMethodType ].upeElement ||
( await createStripePaymentElement( api, paymentMethodType ) );

upeElement.mount( domElement );
upeElement.on( 'loaderror', ( e ) => {
showErrorPaymentMethod( e.error.message, domElement );
// Setting the flag to true to prevent the form from being submitted.
gatewayUPEComponents[ paymentMethodType ].hasLoadError = true;
} );

return gatewayUPEComponents[ paymentMethodType ];
}
Expand All @@ -278,6 +304,7 @@ export async function mountStripePaymentElement( api, domElement ) {
* @param {Object} jQueryForm The jQuery object for the form being submitted.
* @param {string} paymentMethodType The type of Stripe payment method being used.
* @return {boolean} return false to prevent the default form submission from WC Core.
* @throws {Error} If there is an error creating the Stripe payment method.
*/
let hasCheckoutCompleted;
export const processPayment = (
Expand All @@ -297,8 +324,6 @@ export const processPayment = (

blockUI( jQueryForm );

const elements = gatewayUPEComponents[ paymentMethodType ].elements;

const getErrorMessage = ( err ) => {
const genericErrorMessage = __(
'Payment failed. Please try again.',
Expand Down Expand Up @@ -342,6 +367,19 @@ export const processPayment = (

( async () => {
try {
const { elements, hasLoadError } = gatewayUPEComponents[
paymentMethodType
];

if ( hasLoadError ) {
throw new Error(
__(
'Invalid or missing payment details. Please ensure the provided payment method is correctly entered.',
'woocommerce-gateway-stripe'
)
);
}

await validateElements( elements );

const paymentMethodObject = await createStripePaymentMethod(
Expand Down
Loading

0 comments on commit df8249c

Please sign in to comment.