Commit a5ddaafdd05 for woocommerce
commit a5ddaafdd05d14cce15e5578d91d5da20f225490
Author: Mike Jolley <mike.jolley@me.com>
Date: Fri Oct 23 12:08:11 2026 +0100
Stop one broken express payment button from hiding the others (#70136)
* Fix one broken express payment button hiding all the others
Give each express payment method its own error boundary. A broken
button now shows an inline error in the editor and is hidden on the
storefront, so the remaining buttons keep working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add changelog entry for express payment error boundary fix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/42256-express-payment-error-boundary b/plugins/woocommerce/changelog/42256-express-payment-error-boundary
new file mode 100644
index 00000000000..d1859925f8c
--- /dev/null
+++ b/plugins/woocommerce/changelog/42256-express-payment-error-boundary
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Keep other express payment buttons working when one fails to load in the Cart and Checkout blocks.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.tsx
index bb8318ca610..3e6a5a61400 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.tsx
@@ -1,7 +1,8 @@
/**
* External dependencies
*/
-import { __ } from '@wordpress/i18n';
+import { __, sprintf } from '@wordpress/i18n';
+import NoticeBanner from '@woocommerce/base-components/notice-banner';
import {
useExpressPaymentMethods,
usePaymentMethodInterface,
@@ -180,26 +181,54 @@ const ExpressPaymentMethods = () => {
const expressPaymentMethod = isEditor
? paymentMethod.edit
: paymentMethod.content;
- return isValidElement( expressPaymentMethod ) ? (
+ const itemId = `express-payment-method-${ id }`;
+ // Each method gets its own error boundary so one broken button doesn't take down the others. A
+ // broken button shows an inline error in the editor and is hidden on the storefront.
+ const errorFallback = isEditor ? (
<ExpressPayItem
- key={ id }
- id={ `express-payment-method-${ id }` }
- className={ clsx( {
- 'wc-block-components-express-payment__event-button--focused':
- focusedExpressPaymentMethod === id,
- } ) }
+ id={ itemId }
+ style={
+ buttonAttributes
+ ? {
+ minHeight: `${ buttonAttributes.height }px`,
+ }
+ : undefined
+ }
>
- { cloneElement( expressPaymentMethod, {
- ...paymentMethodInterface,
- onClick: onExpressPaymentClick( id ),
- onClose: onExpressPaymentClose,
- onError: onExpressPaymentError,
- setExpressPaymentError:
- deprecatedSetExpressPaymentError,
- buttonAttributes,
- } ) }
+ <NoticeBanner status="error" isDismissible={ false }>
+ { sprintf(
+ /* translators: %s: express payment method title, e.g. "Apple Pay". */
+ __( "%s couldn't load.", 'woocommerce' ),
+ paymentMethod.title
+ ) }
+ </NoticeBanner>
</ExpressPayItem>
) : null;
+ return isValidElement( expressPaymentMethod ) ? (
+ <PaymentMethodErrorBoundary
+ key={ id }
+ isEditor={ isEditor }
+ fallback={ errorFallback }
+ >
+ <ExpressPayItem
+ id={ itemId }
+ className={ clsx( {
+ 'wc-block-components-express-payment__event-button--focused':
+ focusedExpressPaymentMethod === id,
+ } ) }
+ >
+ { cloneElement( expressPaymentMethod, {
+ ...paymentMethodInterface,
+ onClick: onExpressPaymentClick( id ),
+ onClose: onExpressPaymentClose,
+ onError: onExpressPaymentError,
+ setExpressPaymentError:
+ deprecatedSetExpressPaymentError,
+ buttonAttributes,
+ } ) }
+ </ExpressPayItem>
+ </PaymentMethodErrorBoundary>
+ ) : null;
} )
) : (
<div key="noneRegistered">
@@ -208,14 +237,12 @@ const ExpressPaymentMethods = () => {
);
return (
- <PaymentMethodErrorBoundary isEditor={ isEditor }>
- <ExpressPayWrapper
- className="wc-block-components-express-payment__event-buttons"
- ref={ expressPaymentWrapperRef }
- >
- { content }
- </ExpressPayWrapper>
- </PaymentMethodErrorBoundary>
+ <ExpressPayWrapper
+ className="wc-block-components-express-payment__event-buttons"
+ ref={ expressPaymentWrapperRef }
+ >
+ { content }
+ </ExpressPayWrapper>
);
};
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss
index b0cdb3ce7a8..216aa23c145 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss
@@ -41,6 +41,21 @@ $border-width: 1px;
width: 100%;
height: 48px;
}
+
+ // Inline error shown in the editor in place of a broken button. It inherits the item's min-height, which is
+ // set to the button height, and uses a fixed font size because the cart sets the item's font size to 0.
+ > .wc-block-components-notice-banner {
+ @include font-small-locked;
+ align-items: center;
+ box-sizing: border-box;
+ margin: 0;
+ min-height: inherit;
+ padding-block: 0 !important;
+
+ > svg {
+ height: 24px;
+ }
+ }
}
}
}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.tsx
index 08bf40d6753..cd195a562d3 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.tsx
@@ -3,6 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
+import type { ReactNode } from 'react';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import { noticeContexts } from '@woocommerce/base-context';
@@ -14,7 +15,11 @@ import {
interface PaymentMethodErrorBoundaryProps {
isEditor: boolean;
- children: React.ReactNode;
+ children: ReactNode;
+ /**
+ * Rendered instead of the default error notice when a child throws. Pass `null` to render nothing.
+ */
+ fallback?: ReactNode;
}
class PaymentMethodErrorBoundary extends Component< PaymentMethodErrorBoundaryProps > {
@@ -28,9 +33,12 @@ class PaymentMethodErrorBoundary extends Component< PaymentMethodErrorBoundaryPr
render() {
const { hasError, errorMessage } = this.state;
- const { isEditor } = this.props;
+ const { isEditor, fallback } = this.props;
if ( hasError ) {
+ if ( fallback !== undefined ) {
+ return fallback;
+ }
let errorText = __(
'We are experiencing difficulties with this payment method. Please contact us for assistance.',
'woocommerce'
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/express-payment-methods.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/express-payment-methods.tsx
index d84d22f93b3..2e2eadd804b 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/express-payment-methods.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/express-payment-methods.tsx
@@ -22,6 +22,7 @@ import mockEditorContext from './__mocks__/editor-context';
import { getExpectedExpressPaymentProps } from './__mocks__/express-payment-props';
import ExpressPaymentMethods from '../express-payment-methods';
jest.mock( '@woocommerce/base-context', () => ( {
+ ...jest.requireActual( '@woocommerce/base-context' ),
useEditorContext: mockEditorContext,
} ) );
@@ -384,4 +385,82 @@ describe( 'Express payment methods', () => {
} );
} );
} );
+
+ describe( 'One payment method throws an error', () => {
+ const BrokenExpressButton = () => {
+ throw new Error( 'Broken express payment method' );
+ };
+
+ beforeAll( async () => {
+ registerExpressPaymentMethod( {
+ name: 'paypal',
+ title: 'PayPal',
+ gatewayId: 'test-express-payment-method',
+ content: <MockExpressButton name="paypal" />,
+ edit: <MockEditorExpressButton name="paypal" />,
+ canMakePayment: () => true,
+ supports: { features: [ 'products' ] },
+ } );
+ registerExpressPaymentMethod( {
+ name: 'apple-pay',
+ title: 'Apple Pay',
+ gatewayId: 'test-express-payment-method',
+ content: <BrokenExpressButton />,
+ edit: <BrokenExpressButton />,
+ canMakePayment: () => true,
+ supports: { features: [ 'products' ] },
+ } );
+ await dispatch(
+ paymentStore
+ ).__internalUpdateAvailablePaymentMethods();
+ } );
+ afterAll( () => {
+ deregisterMockExpressPaymentMethods();
+ } );
+
+ it( 'should hide the broken method on the storefront and keep the others', () => {
+ mockEditorContext.mockImplementation( () => ( {
+ isEditor: false,
+ } ) );
+ render( <ExpressPaymentMethods /> );
+
+ expect( screen.getByText( 'paypal button' ) ).toBeVisible();
+ expect(
+ document.querySelector( '#express-payment-method-apple-pay' )
+ ).toBeNull();
+ expect(
+ document.querySelector(
+ '.wc-block-components-express-payment__event-buttons'
+ )?.children
+ ).toHaveLength( 1 );
+ expect(
+ document.querySelector( '.wc-block-components-notice-banner' )
+ ).toBeNull();
+ expect( screen.queryByText( /couldn't load/ ) ).toBeNull();
+ // React logs errors caught by the error boundary.
+ expect( console ).toHaveErrored();
+ } );
+
+ it( 'should show an inline error for the broken method in the editor and keep the others', () => {
+ mockEditorContext.mockImplementation( () => ( {
+ isEditor: true,
+ } ) );
+ render( <ExpressPaymentMethods /> );
+
+ expect( screen.getByText( 'paypal preview' ) ).toBeVisible();
+ const brokenItem = document.querySelector(
+ '#express-payment-method-apple-pay'
+ );
+ expect( brokenItem ).toHaveProperty( 'tagName', 'LI' );
+ expect( brokenItem ).toHaveTextContent(
+ "Apple Pay couldn't load."
+ );
+ expect(
+ brokenItem?.querySelector(
+ '.wc-block-components-notice-banner.is-error'
+ )
+ ).toBeInTheDocument();
+ expect( console ).toHaveErrored();
+ } );
+ } );
} );