Commit ac02ebe210e for woocommerce
commit ac02ebe210e49be25e9bf7879996be9c7db7ae9b
Author: Raluca Stan <ralucastn@gmail.com>
Date: Thu Oct 15 15:14:23 2026 +0200
Fix "no payment methods" notice flashing while Checkout loads (#69853)
* Wait for the cart before checking payment methods on checkout
* Add changelog entry for checkout payment methods flash fix
diff --git a/plugins/woocommerce/changelog/fix-64796-checkout-no-payment-methods-flash b/plugins/woocommerce/changelog/fix-64796-checkout-no-payment-methods-flash
new file mode 100644
index 00000000000..abcd8021974
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-64796-checkout-no-payment-methods-flash
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Stop the Checkout block from briefly showing "no payment methods available" while the cart loads.
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx b/plugins/woocommerce/client/blocks/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx
index 57095108fa3..c8b241fd9d5 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx
@@ -13,8 +13,9 @@ import {
} from '@wordpress/element';
import { usePrevious } from '@woocommerce/base-hooks';
import deprecated from '@wordpress/deprecated';
-import { useDispatch, useSelect } from '@wordpress/data';
+import { useDispatch, useRegistry, useSelect } from '@wordpress/data';
import {
+ cartStore,
checkoutStore,
paymentStore,
validationStore,
@@ -122,6 +123,7 @@ export const CheckoutEventsProvider = ( {
const registeredMethods = getExpressPaymentMethods();
const { isEditor } = useEditorContext();
+ const registry = useRegistry();
const {
__internalUpdateAvailablePaymentMethods,
@@ -138,12 +140,17 @@ export const CheckoutEventsProvider = ( {
// Update the payment method store when paymentMethods or expressPaymentMethods changes.
// Ensure this happens in the editor even if paymentMethods is empty. This won't happen instantly when the objects
// are updated, but on the next re-render.
+ // On the storefront, skip the check until the cart has loaded: the cart store runs it as soon as the cart arrives,
+ // and checking against the empty default cart would show "no payment methods" until that run finishes.
useEffect( () => {
- if (
- ! isEditor &&
- Object.keys( paymentMethods ).length === 0 &&
- Object.keys( expressPaymentMethods ).length === 0
- ) {
+ const hasRegisteredMethods =
+ Object.keys( paymentMethods ).length > 0 ||
+ Object.keys( expressPaymentMethods ).length > 0;
+ const hasCartLoaded = registry
+ .select( cartStore )
+ .hasFinishedResolution( 'getCartData' );
+
+ if ( ! isEditor && ( ! hasRegisteredMethods || ! hasCartLoaded ) ) {
return;
}
void __internalUpdateAvailablePaymentMethods();
@@ -151,6 +158,7 @@ export const CheckoutEventsProvider = ( {
isEditor,
paymentMethods,
expressPaymentMethods,
+ registry,
__internalUpdateAvailablePaymentMethods,
] );
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/context/providers/cart-checkout/checkout-events/test/index.tsx b/plugins/woocommerce/client/blocks/assets/js/base/context/providers/cart-checkout/checkout-events/test/index.tsx
index 748e7bb2d20..57a5eb2a8ee 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/context/providers/cart-checkout/checkout-events/test/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/base/context/providers/cart-checkout/checkout-events/test/index.tsx
@@ -1,20 +1,28 @@
/**
* External dependencies
*/
-import { act, render } from '@testing-library/react';
+import { act, render, waitFor } from '@testing-library/react';
import { dispatch } from '@wordpress/data';
-import { checkoutStore, paymentStore } from '@woocommerce/block-data';
+import {
+ cartStore,
+ checkoutStore,
+ paymentStore,
+} from '@woocommerce/block-data';
import { checkoutEvents } from '@woocommerce/blocks-checkout-events';
+import { previewCart } from '@woocommerce/resource-previews';
+import { server, http, HttpResponse } from '@woocommerce/test-utils/msw';
/**
* Internal dependencies
*/
import { CheckoutEventsProvider } from '../index';
+import { EditorProvider } from '../../../editor-context';
-// Mock the registry functions
-jest.mock( '@woocommerce/blocks-registry', () => ( {
- getPaymentMethods: jest.fn( () => ( {} ) ),
- getExpressPaymentMethods: jest.fn( () => ( {
+// Mock the registry functions. Like the real registry, they return the same objects on every call, so a re-render does
+// not re-run the provider's payment method check.
+jest.mock( '@woocommerce/blocks-registry', () => {
+ const paymentMethods = {};
+ const expressPaymentMethods = {
stripe: {
name: 'stripe',
title: 'Stripe',
@@ -33,8 +41,12 @@ jest.mock( '@woocommerce/blocks-registry', () => ( {
style: [],
},
},
- } ) ),
-} ) );
+ };
+ return {
+ getPaymentMethods: jest.fn( () => paymentMethods ),
+ getExpressPaymentMethods: jest.fn( () => expressPaymentMethods ),
+ };
+} );
describe( 'CheckoutEventsContext', () => {
let mockSetRegisteredExpressPaymentMethods: jest.Mock;
@@ -147,3 +159,104 @@ describe( 'CheckoutEventsContext', () => {
expect( failCallback ).toHaveBeenCalled();
} );
} );
+
+describe( 'CheckoutEventsProvider payment method checks', () => {
+ let updateAvailablePaymentMethods: jest.SpyInstance;
+ let releaseCart: () => void;
+
+ /**
+ * Holds the cart request until releaseCart() is called. The cart store requests the cart as soon as its
+ * resolution is invalidated, so the handler is in place before that.
+ */
+ const holdCartRequest = () => {
+ const cartReleased = new Promise< void >( ( resolve ) => {
+ releaseCart = resolve;
+ } );
+ server.use(
+ http.get( '/wc/store/v1/cart', async () => {
+ await cartReleased;
+ return HttpResponse.json( previewCart );
+ } )
+ );
+ act( () => {
+ dispatch( cartStore ).invalidateResolutionForStore();
+ } );
+ };
+
+ beforeAll( () => {
+ // Tell the cart store the shopper has items in their cart, so it waits for the cart request instead of
+ // treating the cart as empty and marking it loaded when the window load event fires.
+ document.cookie = 'woocommerce_items_in_cart=1';
+ } );
+
+ afterAll( () => {
+ document.cookie =
+ 'woocommerce_items_in_cart=; expires=Thu, 01 Jan 1970 00:00:00 GMT';
+ } );
+
+ beforeEach( () => {
+ releaseCart = () => undefined;
+ // The cart store also runs this action once the cart loads, so tests count calls from the moment they render.
+ updateAvailablePaymentMethods = jest
+ .spyOn(
+ dispatch( paymentStore ),
+ '__internalUpdateAvailablePaymentMethods'
+ )
+ .mockImplementation( jest.fn() );
+ } );
+
+ afterEach( () => {
+ releaseCart();
+ updateAvailablePaymentMethods.mockRestore();
+ } );
+
+ it( 'waits for the cart before checking which payment methods the shopper can use', async () => {
+ holdCartRequest();
+
+ render(
+ <CheckoutEventsProvider redirectUrl="local">
+ <div />
+ </CheckoutEventsProvider>
+ );
+
+ expect( updateAvailablePaymentMethods ).not.toHaveBeenCalled();
+
+ releaseCart();
+
+ // The cart store runs the first check once the cart arrives. It may be debounced by a second.
+ await waitFor(
+ () => expect( updateAvailablePaymentMethods ).toHaveBeenCalled(),
+ { timeout: 2000 }
+ );
+ } );
+
+ it( 'checks payment methods on mount when the cart has already loaded', () => {
+ act( () => {
+ dispatch( cartStore ).receiveCart( previewCart );
+ dispatch( cartStore ).finishResolution( 'getCartData' );
+ } );
+ updateAvailablePaymentMethods.mockClear();
+
+ render(
+ <CheckoutEventsProvider redirectUrl="local">
+ <div />
+ </CheckoutEventsProvider>
+ );
+
+ expect( updateAvailablePaymentMethods ).toHaveBeenCalledTimes( 1 );
+ } );
+
+ it( 'checks payment methods in the editor without waiting for the cart', () => {
+ holdCartRequest();
+
+ render(
+ <EditorProvider currentPostId={ 1 }>
+ <CheckoutEventsProvider redirectUrl="local">
+ <div />
+ </CheckoutEventsProvider>
+ </EditorProvider>
+ );
+
+ expect( updateAvailablePaymentMethods ).toHaveBeenCalledTimes( 1 );
+ } );
+} );