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 );
+	} );
+} );