Commit 921702407b5 for woocommerce

commit 921702407b509703f9e885cef22ac4a17afc5980
Author: Soroush Ahmadi <mrsoroushahmadi@gmail.com>
Date:   Wed Sep 23 13:50:13 2026 +0330

    Fix cart blocks crash on corrupt localStorage cart cache (#68372)

    * Fix cart blocks crash on corrupt localStorage cart cache

    * Fix Cart return contract on persistence layer get

    * Wrap the whole persistence get in try/catch

    * fix TS errors from Cart|null return contract

    * Fix linting in test file

    ---------

    Co-authored-by: soroush5 <soroush5@users.noreply.github.com>
    Co-authored-by: Tom Cafferkey <tjcafferkey@gmail.com>

diff --git a/plugins/woocommerce/changelog/68370-fix-cart-persistence-corrupt-cache b/plugins/woocommerce/changelog/68370-fix-cart-persistence-corrupt-cache
new file mode 100644
index 00000000000..6af17980066
--- /dev/null
+++ b/plugins/woocommerce/changelog/68370-fix-cart-persistence-corrupt-cache
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent the cart, checkout and mini-cart blocks from crashing when the cached cart data in localStorage is corrupt. The persistence layer now drops the bad cache and re-fetches instead of throwing.
diff --git a/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/index.ts b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/index.ts
index 1ab93bf2dfb..331a6e04d41 100644
--- a/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/index.ts
+++ b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/index.ts
@@ -62,7 +62,7 @@ window.addEventListener( 'load', () => {
 	const cachedCart = persistenceLayer.get();
 	// On login, if a customer had a cart session, the cached cart is equal to the default cart data, with no items.
 	// We need to check if the cached cart has items, otherwise we will wrongly skip the API request.
-	const hasItemsInCachedCart = cachedCart?.itemsCount > 0;
+	const hasItemsInCachedCart = ( cachedCart?.itemsCount ?? 0 ) > 0;

 	if (
 		( ! hasCartSession() || hasItemsInCachedCart ) &&
diff --git a/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/persistence-layer.ts b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/persistence-layer.ts
index 4f12fb435e1..c0a1c88e6dc 100644
--- a/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/persistence-layer.ts
+++ b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/persistence-layer.ts
@@ -33,24 +33,34 @@ export const isAddingToCart = () => {
 };

 export const persistenceLayer = {
-	get: () => {
-		if ( ! hasCartSession() || ! hasValidHash() ) {
-			return null;
-		}
+	get: (): Cart | null => {
+		try {
+			if ( ! hasCartSession() || ! hasValidHash() ) {
+				return null;
+			}

-		const cached = window.localStorage?.getItem( 'storeApiCartData' );
+			const cached = window.localStorage?.getItem( 'storeApiCartData' );

-		if ( ! cached ) {
-			return null;
-		}
+			if ( ! cached ) {
+				return null;
+			}
+
+			const parsed: unknown = JSON.parse( cached );

-		const parsed = JSON.parse( cached );
+			if ( ! parsed || typeof parsed !== 'object' ) {
+				return null;
+			}

-		if ( ! parsed || typeof parsed !== 'object' ) {
+			return parsed as Cart;
+		} catch {
+			// Best-effort read: `get` runs at store creation, so any throw
+			// here crashes cart, checkout, and mini-cart alike. Covered:
+			// corrupt JSON, and `window.localStorage` access itself throwing
+			// (private-mode Safari, disabled storage, jsdom teardown in
+			// Jest) — the same cases `set` below guards against. Fall back
+			// to null so the store re-fetches.
 			return null;
 		}
-
-		return parsed;
 	},
 	set: ( cartData: Cart ) => {
 		// Wrap in try/catch for two reasons:
diff --git a/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/index.ts b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/index.ts
index 24e5b6ac78a..f1e13858fd4 100644
--- a/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/index.ts
+++ b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/index.ts
@@ -2,6 +2,7 @@
  * External dependencies
  */
 import { dispatch as wpDispatch } from '@wordpress/data';
+import type { Cart } from '@woocommerce/types';

 /**
  * Internal dependencies
@@ -27,13 +28,16 @@ const mockWpDispatch = jest.mocked( wpDispatch );
 describe( 'Window load event handler', () => {
 	let mockFinishResolution: jest.Mock;
 	let originalAddEventListener: typeof window.addEventListener;
-	let loadHandler: EventListener;
+	let loadHandler: ( event: Event ) => void;

 	beforeAll( () => {
 		// Capture the addEventListener calls to extract the load handler
 		originalAddEventListener = window.addEventListener;
 		window.addEventListener = jest.fn(
-			( event: string, handler: EventListenerOrEventListenerObject ) => {
+			(
+				event: string,
+				handler: Parameters< typeof window.addEventListener >[ 1 ]
+			) => {
 				if ( event === 'load' && typeof handler === 'function' ) {
 					loadHandler = handler;
 				}
@@ -42,7 +46,7 @@ describe( 'Window load event handler', () => {
 		);

 		// Now import the module to register the event listener
-		require( '../index' );
+		jest.requireActual( '../index' );
 	} );

 	beforeEach( () => {
@@ -69,7 +73,9 @@ describe( 'Window load event handler', () => {
 	it( 'should skip API request when cached cart has items and not adding to cart with /?add-to-cart=', () => {
 		mockHasCartSession.mockReturnValue( true );
 		mockIsAddingToCart.mockReturnValue( false );
-		mockPersistenceLayerGet.mockReturnValue( { itemsCount: 2 } );
+		mockPersistenceLayerGet.mockReturnValue( {
+			itemsCount: 2,
+		} as unknown as Cart );

 		loadHandler( new Event( 'load' ) );

@@ -79,7 +85,9 @@ describe( 'Window load event handler', () => {
 	it( 'should make API request when has cart session but cached cart is empty', () => {
 		mockHasCartSession.mockReturnValue( true );
 		mockIsAddingToCart.mockReturnValue( false );
-		mockPersistenceLayerGet.mockReturnValue( { itemsCount: 0 } );
+		mockPersistenceLayerGet.mockReturnValue( {
+			itemsCount: 0,
+		} as unknown as Cart );

 		loadHandler( new Event( 'load' ) );

@@ -109,7 +117,9 @@ describe( 'Window load event handler', () => {
 	it( 'should make API request when has cart session, cached cart has items, but adding to cart with /?add-to-cart=', () => {
 		mockHasCartSession.mockReturnValue( true );
 		mockIsAddingToCart.mockReturnValue( true );
-		mockPersistenceLayerGet.mockReturnValue( { itemsCount: 2 } );
+		mockPersistenceLayerGet.mockReturnValue( {
+			itemsCount: 2,
+		} as unknown as Cart );

 		loadHandler( new Event( 'load' ) );

diff --git a/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/persistence-layer.js b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/persistence-layer.js
new file mode 100644
index 00000000000..d6cb1ba5a30
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/persistence-layer.js
@@ -0,0 +1,46 @@
+/**
+ * Internal dependencies
+ */
+import { persistenceLayer } from '../persistence-layer';
+
+describe( 'persistenceLayer', () => {
+	beforeEach( () => {
+		window.localStorage.clear();
+		document.cookie = 'woocommerce_items_in_cart=1';
+		document.cookie = 'woocommerce_cart_hash=abc';
+		window.localStorage.setItem( 'storeApiCartHash', 'abc' );
+	} );
+
+	it( 'returns null instead of throwing when the cached cart is corrupt', () => {
+		window.localStorage.setItem( 'storeApiCartData', '{corrupt-json' );
+		expect( () => persistenceLayer.get() ).not.toThrow();
+		expect( persistenceLayer.get() ).toBeNull();
+	} );
+
+	it( 'returns the parsed cart when the cached cart is valid', () => {
+		window.localStorage.setItem(
+			'storeApiCartData',
+			JSON.stringify( { itemsCount: 2 } )
+		);
+		expect( persistenceLayer.get() ).toEqual( { itemsCount: 2 } );
+	} );
+
+	it( 'returns null instead of throwing when localStorage access throws', () => {
+		const storage = window.localStorage;
+		Object.defineProperty( window, 'localStorage', {
+			configurable: true,
+			get() {
+				throw new Error( 'denied' );
+			},
+		} );
+		try {
+			expect( () => persistenceLayer.get() ).not.toThrow();
+			expect( persistenceLayer.get() ).toBeNull();
+		} finally {
+			Object.defineProperty( window, 'localStorage', {
+				configurable: true,
+				value: storage,
+			} );
+		}
+	} );
+} );