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