Commit dfdeec8d1d7 for woocommerce

commit dfdeec8d1d78c7cbb9f4a71583af96b72d652114
Author: Tom Cafferkey <tjcafferkey@gmail.com>
Date:   Mon Jun 29 11:04:21 2026 +0100

    Fix extension cart update shipping mode sync (#66014)

    * Fix extension cart update shipping mode sync

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/66014-fix-extension-cart-update-shipping-mode-sync b/plugins/woocommerce/changelog/66014-fix-extension-cart-update-shipping-mode-sync
new file mode 100644
index 00000000000..9c8e636d575
--- /dev/null
+++ b/plugins/woocommerce/changelog/66014-fix-extension-cart-update-shipping-mode-sync
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix Checkout block shipping method UI after extension cart updates change the selected shipping rate.
\ No newline at end of file
diff --git a/plugins/woocommerce/client/blocks/assets/js/data/cart/test/thunks.ts b/plugins/woocommerce/client/blocks/assets/js/data/cart/test/thunks.ts
index 0eec805217f..59a92989540 100644
--- a/plugins/woocommerce/client/blocks/assets/js/data/cart/test/thunks.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/data/cart/test/thunks.ts
@@ -13,11 +13,22 @@ import {
 } from '../thunks';
 import { apiFetchWithHeaders } from '../../shared-controls';
 import { getIsCustomerDataDirty } from '../utils';
+import { store as checkoutStore } from '../../checkout';

 jest.mock( '../../shared-controls', () => ( {
 	apiFetchWithHeaders: jest.fn(),
 } ) );

+jest.mock( '@woocommerce/base-utils', () => ( {
+	...jest.requireActual( '@woocommerce/base-utils' ),
+	hasCollectableRate: jest.fn( ( chosenRates ) => {
+		if ( Array.isArray( chosenRates ) ) {
+			return chosenRates.includes( 'pickup_location' );
+		}
+		return chosenRates === 'pickup_location';
+	} ),
+} ) );
+
 jest.mock( '../notify-quantity-changes', () => ( {
 	notifyQuantityChanges: jest.fn(),
 } ) );
@@ -28,6 +39,7 @@ jest.mock( '../notify-errors', () => ( {

 jest.mock( '../utils', () => ( {
 	getIsCustomerDataDirty: jest.fn( () => false ),
+	getTriggerStoreSyncEvent: jest.fn( () => false ),
 	setIsCustomerDataDirty: jest.fn(),
 	setTriggerStoreSyncEvent: jest.fn(),
 } ) );
@@ -397,6 +409,15 @@ describe( 'applyExtensionCartUpdate', () => {
 		receiveCart: jest.fn(),
 		receiveError: jest.fn(),
 	} );
+	const createMockRegistry = () => {
+		const setPrefersCollection = jest.fn();
+		return {
+			registry: {
+				dispatch: jest.fn( () => ( { setPrefersCollection } ) ),
+			},
+			setPrefersCollection,
+		};
+	};

 	beforeEach( () => {
 		jest.clearAllMocks();
@@ -417,6 +438,64 @@ describe( 'applyExtensionCartUpdate', () => {
 		expect( dispatch.receiveCart ).toHaveBeenCalledWith( mockResponse );
 	} );

+	it( 'should set prefersCollection true when the extension response selects local pickup', async () => {
+		const dispatch = createMockDispatch();
+		const { registry, setPrefersCollection } = createMockRegistry();
+		mockApiFetchWithHeaders.mockResolvedValue( {
+			response: {
+				...mockResponse,
+				shipping_rates: [
+					{
+						package_id: 0,
+						shipping_rates: [
+							{
+								method_id: 'pickup_location',
+								selected: true,
+							},
+						],
+					},
+				],
+			},
+		} );
+
+		await applyExtensionCartUpdate( {
+			namespace: 'test',
+			data: {},
+		} )( { dispatch, registry } as never );
+
+		expect( registry.dispatch ).toHaveBeenCalledWith( checkoutStore );
+		expect( setPrefersCollection ).toHaveBeenCalledWith( true );
+	} );
+
+	it( 'should set prefersCollection false when the extension response selects shipping', async () => {
+		const dispatch = createMockDispatch();
+		const { registry, setPrefersCollection } = createMockRegistry();
+		mockApiFetchWithHeaders.mockResolvedValue( {
+			response: {
+				...mockResponse,
+				shipping_rates: [
+					{
+						package_id: 0,
+						shipping_rates: [
+							{
+								method_id: 'flat_rate',
+								selected: true,
+							},
+						],
+					},
+				],
+			},
+		} );
+
+		await applyExtensionCartUpdate( {
+			namespace: 'test',
+			data: {},
+		} )( { dispatch, registry } as never );
+
+		expect( registry.dispatch ).toHaveBeenCalledWith( checkoutStore );
+		expect( setPrefersCollection ).toHaveBeenCalledWith( false );
+	} );
+
 	it( 'should strip both addresses when customer data is dirty and no overwrite specified', async () => {
 		mockGetIsCustomerDataDirty.mockReturnValue( true );
 		const dispatch = createMockDispatch();
diff --git a/plugins/woocommerce/client/blocks/assets/js/data/cart/thunks.ts b/plugins/woocommerce/client/blocks/assets/js/data/cart/thunks.ts
index a1277513d2f..0b455a53ba7 100644
--- a/plugins/woocommerce/client/blocks/assets/js/data/cart/thunks.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/data/cart/thunks.ts
@@ -13,6 +13,7 @@ import {
 } from '@woocommerce/types';
 import {
 	camelCaseKeys,
+	hasCollectableRate,
 	triggerAddedToCartEvent,
 	triggerAddingToCartEvent,
 } from '@woocommerce/base-utils';
@@ -20,6 +21,7 @@ import {
 	type CurriedSelectorsOf,
 	type ConfigOf,
 	type ActionCreatorsOf,
+	type DispatchFunction,
 } from '@wordpress/data/build-types/types';
 import { __ } from '@wordpress/i18n';
 import { cartStore } from '@woocommerce/block-data';
@@ -40,9 +42,12 @@ import {
 	setTriggerStoreSyncEvent,
 } from './utils';
 import { isEditor } from '../utils';
+import { store as checkoutStore } from '../checkout';
+
 interface CartThunkArgs {
 	select: CurriedSelectorsOf< typeof cartStore >;
 	dispatch: ActionCreatorsOf< ConfigOf< typeof cartStore > >;
+	registry?: { dispatch: DispatchFunction };
 }

 /**
@@ -124,6 +129,33 @@ export const receiveError =
 		dispatch.setErrorData( response );
 	};

+/**
+ * Updates the checkout store with the shopper's collection preference based on
+ * the selected shipping rates in the cart.
+ *
+ * @param {CartResponse} response
+ * @param {CartThunkArgs['registry']} registry
+ */
+const syncPrefersCollectionFromSelectedShippingRates = (
+	response: CartResponse,
+	registry?: CartThunkArgs[ 'registry' ]
+) => {
+	if ( ! registry ) {
+		return;
+	}
+
+	const selectedMethodIds = response.shipping_rates
+		?.flatMap( ( shippingPackage ) => shippingPackage.shipping_rates )
+		.filter( ( rate ) => rate.selected )
+		.map( ( rate ) => rate.method_id );
+
+	if ( selectedMethodIds?.length ) {
+		registry
+			.dispatch( checkoutStore )
+			.setPrefersCollection( hasCollectableRate( selectedMethodIds ) );
+	}
+};
+
 /**
  * POSTs to the /cart/extensions endpoint with the data supplied by the extension.
  *
@@ -131,7 +163,7 @@ export const receiveError =
  */
 export const applyExtensionCartUpdate =
 	( args: ExtensionCartUpdateArgs ) =>
-	async ( { dispatch }: CartThunkArgs ) => {
+	async ( { dispatch, registry }: CartThunkArgs ) => {
 		try {
 			const { response } = await apiFetchWithHeaders< {
 				response: CartResponse;
@@ -178,9 +210,17 @@ export const applyExtensionCartUpdate =
 				}

 				dispatch.receiveCart( cartToReceive );
+				syncPrefersCollectionFromSelectedShippingRates(
+					response,
+					registry
+				);
 				return response;
 			}
 			dispatch.receiveCart( response );
+			syncPrefersCollectionFromSelectedShippingRates(
+				response,
+				registry
+			);
 			return response;
 		} catch ( error ) {
 			dispatch.receiveError( isApiErrorResponse( error ) ? error : null );