Commit a4f7cd77e2d for woocommerce

commit a4f7cd77e2dfc0d5b31d5b9b30bcc003f8b4c2fd
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date:   Mon Jul 6 19:40:05 2026 +0200

    Update Product Image block to use larger thumbnails (#66191)

    * Add changelog

    * Update Product Image block to use larger thumbnails

    * Replace imageSizing attribute with aspectRatio

    * Fix Single Product previews not having the correct aspect ratio

    * Types fixes

    * Fix unit test

    * Move away from cropped sizing

    * Rename test

    * Update resolveAspectRatio() signature

    * Make sure PHP tests reset options

    * Fix imageSizing value in tests

    * Use render_product_image_block() util when possible

    * Minor cleanups

    * Remove unnecessary test

    * Make sure All Products images keep the correct aspect ratio

    * Update Product Collection 3 Columns pattern to not use imageSizing

diff --git a/plugins/woocommerce/changelog/update-product-image-use-larger-thumbnails b/plugins/woocommerce/changelog/update-product-image-use-larger-thumbnails
new file mode 100644
index 00000000000..ebfbf3e83d8
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-product-image-use-larger-thumbnails
@@ -0,0 +1,4 @@
+Significance: patch
+Type: enhancement
+
+Updated Product Image block to use larger thumbnails
diff --git a/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/block.tsx b/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/block.tsx
index 29297785852..8c64f57f678 100644
--- a/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/block.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/block.tsx
@@ -14,12 +14,7 @@ import { withProductDataContext } from '@woocommerce/shared-hocs';
 import { useStoreEvents } from '@woocommerce/base-context/hooks';
 import type { HTMLAttributes } from 'react';
 import { decodeEntities } from '@wordpress/html-entities';
-import {
-	isString,
-	objectHasProp,
-	isEmpty,
-	ProductResponseItem,
-} from '@woocommerce/types';
+import { isEmpty, ProductResponseItem } from '@woocommerce/types';
 import { ProductEntityResponse } from '@woocommerce/entities';

 /**
@@ -27,8 +22,8 @@ import { ProductEntityResponse } from '@woocommerce/entities';
  */
 import ProductSaleBadge from '../sale-badge/block';
 import './style.scss';
-import { BlockAttributes, ImageSizing, ProductImageContext } from './types';
-import { isTryingToDisplayLegacySaleBadge } from './utils';
+import { BlockAttributes, ProductImageContext } from './types';
+import { isTryingToDisplayLegacySaleBadge, resolveAspectRatio } from './utils';

 const buildStyles = ( props: Partial< ImageProps > ) => {
 	const { aspectRatio, height, width, scale } = props;
@@ -58,16 +53,12 @@ const chooseImage = ( product: ProductResponseItem, imageId?: number ) => {

 const ImagePlaceholder = ( props: {
 	style?: Record< string, unknown >;
-	showFullSize: boolean;
 } ): JSX.Element => {
-	const { showFullSize, ...restProps } = props;
-	const src = showFullSize
-		? getSetting( 'placeholderImgSrcFullSize', PLACEHOLDER_IMG_SRC )
-		: PLACEHOLDER_IMG_SRC;
+	const src = getSetting( 'placeholderImgSrcFullSize', PLACEHOLDER_IMG_SRC );

 	return (
 		<img
-			{ ...restProps }
+			{ ...props }
 			src={ src }
 			// Decorative image with no value, so alt should be empty.
 			alt=""
@@ -88,7 +79,6 @@ interface ImageProps {
 		thumbnail?: string | undefined;
 	};
 	loaded: boolean;
-	showFullSize: boolean;
 	fallbackAlt: string;
 	scale: 'cover' | 'contain' | 'fill';
 	width?: string | undefined;
@@ -99,19 +89,19 @@ interface ImageProps {
 const Image = ( {
 	image,
 	loaded,
-	showFullSize,
 	fallbackAlt,
 	width,
 	scale,
 	height,
 	aspectRatio,
 }: ImageProps ): JSX.Element => {
-	const { thumbnail, src, srcset, sizes, alt } = image || {};
+	const { src, srcset, sizes, alt } = image || {};
 	const imageProps = {
 		alt: alt || fallbackAlt,
 		hidden: ! loaded,
-		src: showFullSize ? src : thumbnail,
-		...( showFullSize && { srcSet: srcset, sizes } ),
+		src,
+		srcSet: srcset,
+		sizes,
 	};

 	const imageStyles = buildStyles( {
@@ -122,12 +112,7 @@ const Image = ( {
 	} );

 	if ( ! image ) {
-		return (
-			<ImagePlaceholder
-				showFullSize={ showFullSize }
-				style={ imageStyles }
-			/>
-		);
+		return <ImagePlaceholder style={ imageStyles } />;
 	}

 	return (
@@ -142,7 +127,7 @@ const Image = ( {

 type Props = BlockAttributes &
 	Pick< ProductImageContext, 'imageId' > &
-	HTMLAttributes< HTMLDivElement > & {
+	Omit< HTMLAttributes< HTMLDivElement >, 'style' > & {
 		isAdmin?: boolean;
 		product?: ProductResponseItem | ProductEntityResponse;
 		isResolving?: boolean;
@@ -173,7 +158,7 @@ export const Block = ( props: Props ): JSX.Element | null => {
 		className,
 		height,
 		imageId,
-		imageSizing = ImageSizing.SINGLE,
+		imageSizing,
 		scale,
 		showProductLink = true,
 		style,
@@ -193,13 +178,16 @@ export const Block = ( props: Props ): JSX.Element | null => {
 	} );
 	const { dispatchStoreEvent } = useStoreEvents();

-	const showFullSize = imageSizing !== ImageSizing.THUMBNAIL;
-	const finalAspectRatio =
-		objectHasProp( style, 'dimensions' ) &&
-		objectHasProp( style.dimensions, 'aspectRatio' ) &&
-		isString( style.dimensions.aspectRatio )
-			? style.dimensions.aspectRatio
-			: aspectRatio;
+	const storeAspectRatio = getSetting< string | null >(
+		'thumbnailAspectRatio',
+		null
+	);
+	const finalAspectRatio = resolveAspectRatio(
+		style,
+		aspectRatio,
+		storeAspectRatio,
+		imageSizing
+	);
 	const aspectRatioClass = `wc-block-components-product-image--aspect-ratio-${
 		finalAspectRatio ? finalAspectRatio.replace( '/', '-' ) : 'auto'
 	}`;
@@ -227,10 +215,7 @@ export const Block = ( props: Props ): JSX.Element | null => {
 					) }
 					style={ styleProps.style }
 				>
-					<ImagePlaceholder
-						showFullSize={ showFullSize }
-						style={ imageStyles }
-					/>
+					<ImagePlaceholder style={ imageStyles } />
 				</div>
 				{ children }
 			</>
@@ -289,7 +274,6 @@ export const Block = ( props: Props ): JSX.Element | null => {
 						fallbackAlt={ decodeEntities( product.name ) }
 						image={ image }
 						loaded={ ! isLoading }
-						showFullSize={ showFullSize }
 						width={ width }
 						height={ height }
 						scale={ scale }
diff --git a/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/edit.tsx
index b734ff41465..52450e466c9 100644
--- a/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/edit.tsx
@@ -9,26 +9,12 @@ import {
 	useInnerBlocksProps,
 	store as blockEditorStore,
 } from '@wordpress/block-editor';
-import {
-	createInterpolateElement,
-	useEffect,
-	useRef,
-} from '@wordpress/element';
-import { getAdminLink, getSettingWithCoercion } from '@woocommerce/settings';
+import { useEffect, useRef } from '@wordpress/element';
 import { useProduct } from '@woocommerce/entities';
-import { isBoolean } from '@woocommerce/types';
 import type { BlockEditProps } from '@wordpress/blocks';
 import { ProductQueryContext as Context } from '@woocommerce/blocks/product-query/types';
 import {
 	ToggleControl,
-	// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-	// @ts-ignore - Ignoring because `__experimentalToggleGroupControl` is not yet in the type definitions.
-	// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
-	__experimentalToggleGroupControl as ToggleGroupControl,
-	// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-	// @ts-ignore - Ignoring because `__experimentalToggleGroupControl` is not yet in the type definitions.
-	// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
-	__experimentalToggleGroupControlOption as ToggleGroupControlOption,
 	// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
 	__experimentalToolsPanel as ToolsPanel,
 	// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
@@ -40,7 +26,7 @@ import {
  */
 import Block from './block';
 import { useIsDescendentOfSingleProductBlock } from '../shared/use-is-descendent-of-single-product-block';
-import { BlockAttributes, ImageSizing } from './types';
+import { BlockAttributes } from './types';
 import { ImageSizeSettings } from './image-size-settings';

 const TEMPLATE = [
@@ -54,7 +40,6 @@ const TEMPLATE = [

 const DEFAULT_ATTRIBUTES = {
 	showProductLink: true,
-	imageSizing: ImageSizing.SINGLE,
 };

 const Edit = ( {
@@ -63,7 +48,7 @@ const Edit = ( {
 	context,
 	clientId,
 }: BlockEditProps< BlockAttributes > & { context: Context } ): JSX.Element => {
-	const { showProductLink, imageSizing, width, height, scale } = attributes;
+	const { showProductLink, width, height, scale } = attributes;

 	const ref = useRef< HTMLDivElement >( null );

@@ -123,12 +108,6 @@ const Edit = ( {
 		}
 	);

-	const isBlockTheme = getSettingWithCoercion(
-		'isBlockTheme',
-		false,
-		isBoolean
-	);
-
 	const { product, isResolving } = useProduct( context.postId );

 	return (
@@ -149,7 +128,6 @@ const Edit = ( {
 							setAttributes( {
 								showProductLink:
 									DEFAULT_ATTRIBUTES.showProductLink,
-								imageSizing: DEFAULT_ATTRIBUTES.imageSizing,
 							} )
 						}
 					>
@@ -188,60 +166,6 @@ const Edit = ( {
 								}
 							/>
 						</ToolsPanelItem>
-						<ToolsPanelItem
-							label={ __( 'Resolution', 'woocommerce' ) }
-							hasValue={ () =>
-								imageSizing !== DEFAULT_ATTRIBUTES.imageSizing
-							}
-							onDeselect={ () =>
-								setAttributes( {
-									imageSizing: DEFAULT_ATTRIBUTES.imageSizing,
-								} )
-							}
-							isShownByDefault
-						>
-							<ToggleGroupControl
-								__next40pxDefaultSize
-								__nextHasNoMarginBottom
-								label={ __( 'Resolution', 'woocommerce' ) }
-								isBlock
-								help={
-									! isBlockTheme
-										? createInterpolateElement(
-												__(
-													'Product image cropping can be modified in the <a>Customizer</a>.',
-													'woocommerce'
-												),
-												{
-													a: (
-														// eslint-disable-next-line jsx-a11y/anchor-has-content
-														<a
-															href={ `${ getAdminLink(
-																'customize.php'
-															) }?autofocus[panel]=woocommerce&autofocus[section]=woocommerce_product_images` }
-															target="_blank"
-															rel="noopener noreferrer"
-														/>
-													),
-												}
-										  )
-										: null
-								}
-								value={ imageSizing }
-								onChange={ ( value: ImageSizing ) =>
-									setAttributes( { imageSizing: value } )
-								}
-							>
-								<ToggleGroupControlOption
-									value={ ImageSizing.SINGLE }
-									label={ __( 'Full Size', 'woocommerce' ) }
-								/>
-								<ToggleGroupControlOption
-									value={ ImageSizing.THUMBNAIL }
-									label={ __( 'Thumbnail', 'woocommerce' ) }
-								/>
-							</ToggleGroupControl>
-						</ToolsPanelItem>
 					</ToolsPanel>
 				</InspectorControls>
 			) }
diff --git a/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/test/block.test.tsx b/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/test/block.test.tsx
index 9711bef9d7a..404df4ed43b 100644
--- a/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/test/block.test.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/test/block.test.tsx
@@ -3,6 +3,7 @@
  */
 import { render, fireEvent } from '@testing-library/react';
 import { ProductDataContextProvider } from '@woocommerce/shared-context';
+import { getSetting } from '@woocommerce/settings';
 import { ProductResponseItem } from '@woocommerce/types';

 /**
@@ -27,6 +28,9 @@ jest.mock( '@woocommerce/settings', () => {
 			if ( key === 'placeholderImgSrcFullSize' ) {
 				return 'placeholder-full-size.jpg';
 			}
+			if ( key === 'thumbnailAspectRatio' ) {
+				return '1/1';
+			}
 			// Use the original getSetting for other keys
 			return originalModule.getSetting( key, defaultValue );
 		} ),
@@ -300,4 +304,120 @@ describe( 'Product Image Block', () => {
 			expect( placeholderImage.getAttribute( 'height' ) ).toBe( null );
 		} );
 	} );
+
+	describe( 'aspect ratio', () => {
+		test( 'uses full-size image src even when imageSizing is thumbnail', () => {
+			const component = render(
+				<ProductDataContextProvider
+					product={ productWithImages }
+					isLoading={ false }
+				>
+					<Block
+						showProductLink={ true }
+						productId={ productWithImages.id }
+						showSaleBadge={ false }
+						saleBadgeAlign={ 'left' }
+						imageSizing={ ImageSizing.THUMBNAIL }
+						isDescendentOfQueryLoop={ false }
+					/>
+				</ProductDataContextProvider>
+			);
+
+			const image = component.getByTestId( 'product-image' );
+			fireEvent.load( image );
+
+			expect( image.getAttribute( 'src' ) ).toBe(
+				productWithImages.images[ 0 ].src
+			);
+			expect( image.getAttribute( 'src' ) ).not.toBe(
+				productWithImages.images[ 0 ].thumbnail
+			);
+		} );
+
+		test( 'applies store thumbnail aspect ratio when imageSizing is thumbnail', () => {
+			const component = render(
+				<ProductDataContextProvider
+					product={ productWithImages }
+					isLoading={ false }
+				>
+					<Block
+						showProductLink={ true }
+						productId={ productWithImages.id }
+						showSaleBadge={ false }
+						saleBadgeAlign={ 'left' }
+						imageSizing={ ImageSizing.THUMBNAIL }
+						isDescendentOfQueryLoop={ false }
+					/>
+				</ProductDataContextProvider>
+			);
+
+			const image = component.getByTestId( 'product-image' );
+			expect( image.style.aspectRatio ).toBe( '1/1' );
+			expect(
+				component.container.querySelector(
+					'.wc-block-components-product-image--aspect-ratio-1-1'
+				)
+			).not.toBeNull();
+		} );
+
+		test( 'block aspect ratio overrides store thumbnail aspect ratio', () => {
+			const component = render(
+				<ProductDataContextProvider
+					product={ productWithImages }
+					isLoading={ false }
+				>
+					<Block
+						showProductLink={ true }
+						productId={ productWithImages.id }
+						showSaleBadge={ false }
+						saleBadgeAlign={ 'left' }
+						imageSizing={ ImageSizing.THUMBNAIL }
+						isDescendentOfQueryLoop={ false }
+						aspectRatio="3/5"
+					/>
+				</ProductDataContextProvider>
+			);
+
+			const image = component.getByTestId( 'product-image' );
+			expect( image.style.aspectRatio ).toBe( '3/5' );
+		} );
+
+		test( 'uses auto aspect ratio class when store cropping is uncropped', () => {
+			( getSetting as jest.Mock ).mockImplementation(
+				( key, defaultValue ) => {
+					if ( key === 'placeholderImgSrcFullSize' ) {
+						return 'placeholder-full-size.jpg';
+					}
+					if ( key === 'thumbnailAspectRatio' ) {
+						return null;
+					}
+					return defaultValue;
+				}
+			);
+
+			const component = render(
+				<ProductDataContextProvider
+					product={ productWithImages }
+					isLoading={ false }
+				>
+					<Block
+						showProductLink={ true }
+						productId={ productWithImages.id }
+						showSaleBadge={ false }
+						saleBadgeAlign={ 'left' }
+						imageSizing={ ImageSizing.THUMBNAIL }
+						isDescendentOfQueryLoop={ false }
+					/>
+				</ProductDataContextProvider>
+			);
+
+			const image = component.getByTestId( 'product-image' );
+			expect( image.style.aspectRatio ).toBe( '' );
+			expect(
+				component.container.querySelector(
+					'.wc-block-components-product-image--aspect-ratio-auto'
+				)
+			).not.toBeNull();
+		} );
+	} );
 } );
diff --git a/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/test/utils.test.ts b/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/test/utils.test.ts
new file mode 100644
index 00000000000..a401c14e5c7
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/test/utils.test.ts
@@ -0,0 +1,57 @@
+/**
+ * Internal dependencies
+ */
+import { resolveAspectRatio } from '../utils';
+import { ImageSizing } from '../types';
+
+describe( 'resolveAspectRatio', () => {
+	it( 'uses style.dimensions.aspectRatio when set', () => {
+		expect(
+			resolveAspectRatio(
+				{ dimensions: { aspectRatio: '16/9' } },
+				'1/1',
+				'4/3',
+				ImageSizing.THUMBNAIL
+			)
+		).toBe( '16/9' );
+	} );
+
+	it( 'uses aspectRatio attribute when dimensions are not set', () => {
+		expect(
+			resolveAspectRatio( undefined, '3/5', '1/1', ImageSizing.THUMBNAIL )
+		).toBe( '3/5' );
+	} );
+
+	it( 'falls back to store aspect ratio when no block override is set', () => {
+		expect(
+			resolveAspectRatio(
+				undefined,
+				undefined,
+				'4/3',
+				ImageSizing.THUMBNAIL
+			)
+		).toBe( '4/3' );
+	} );
+
+	it( 'returns undefined when store aspect ratio is null (uncropped)', () => {
+		expect(
+			resolveAspectRatio(
+				undefined,
+				undefined,
+				null,
+				ImageSizing.THUMBNAIL
+			)
+		).toBeUndefined();
+	} );
+
+	it( 'returns undefined when imageSizing is not thumbnail', () => {
+		expect(
+			resolveAspectRatio(
+				undefined,
+				undefined,
+				'4/3',
+				ImageSizing.SINGLE
+			)
+		).toBeUndefined();
+	} );
+} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/types.ts b/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/types.ts
index 22d5d99d3a5..2e318fde042 100644
--- a/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/types.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/types.ts
@@ -8,6 +8,12 @@ export enum ImageSizing {
 	THUMBNAIL = 'thumbnail',
 }

+export type AspectRatioStyle = {
+	dimensions?: {
+		aspectRatio?: string;
+	};
+};
+
 export interface BlockAttributes {
 	// The product ID.
 	productId: number;
@@ -33,6 +39,8 @@ export interface BlockAttributes {
 	scale: 'cover' | 'contain' | 'fill';
 	// Aspect ratio of the image.
 	aspectRatio: string;
+	// Block style from dimensions support (not React CSSProperties).
+	style?: AspectRatioStyle;
 }

 export interface ProductImageContext extends ProductQueryContext {
diff --git a/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/utils.ts b/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/utils.ts
index eca4523de44..2fa825d4c5f 100644
--- a/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/utils.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/image/utils.ts
@@ -1,3 +1,8 @@
+/**
+ * Internal dependencies
+ */
+import { AspectRatioStyle, ImageSizing } from './types';
+
 export const isTryingToDisplayLegacySaleBadge = ( showSaleBadge?: boolean ) => {
 	// If the block is pristine, it doesn't have a showSaleBadge attribute
 	// but it is supposed to be `true` by default.
@@ -9,3 +14,34 @@ export const isTryingToDisplayLegacySaleBadge = ( showSaleBadge?: boolean ) => {
 	// that we should respect.
 	return showSaleBadge;
 };
+
+/**
+ * Resolve the aspect ratio for a product image.
+ *
+ * Block-level overrides take priority over store thumbnail cropping settings.
+ */
+export const resolveAspectRatio = (
+	style: AspectRatioStyle | undefined,
+	aspectRatio: string | undefined,
+	storeAspectRatio: string | null | undefined,
+	imageSizing: ImageSizing | undefined
+): string | undefined => {
+	if (
+		style &&
+		style.dimensions &&
+		style.dimensions.aspectRatio &&
+		typeof style.dimensions.aspectRatio === 'string'
+	) {
+		return style.dimensions.aspectRatio;
+	}
+
+	if ( aspectRatio && typeof aspectRatio === 'string' ) {
+		return aspectRatio;
+	}
+
+	if ( imageSizing && imageSizing === ImageSizing.THUMBNAIL ) {
+		return storeAspectRatio ?? undefined;
+	}
+
+	return undefined;
+};
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/constants.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/constants.ts
index 88a71288cff..6826b519786 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/constants.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/constants.ts
@@ -23,7 +23,6 @@ import {
 	LayoutOptions,
 	WidthOptions,
 } from './types';
-import { ImageSizing } from '../../atomic/blocks/product-elements/image/types';

 export const PRODUCT_COLLECTION_BLOCK_NAME = blockJson.name;
 const PRODUCT_TITLE_NAME = `${ PRODUCT_COLLECTION_BLOCK_NAME }/product-title`;
@@ -131,8 +130,12 @@ export const INNER_BLOCKS_PRODUCT_TEMPLATE: InnerBlockTemplate = [
 		[
 			'woocommerce/product-image',
 			{
-				imageSizing: ImageSizing.THUMBNAIL,
 				showSaleBadge: false,
+				style: {
+					dimensions: {
+						aspectRatio: '1/1',
+					},
+				},
 			},
 			[
 				[
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-query/constants.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-query/constants.ts
index a5d0fc5c8d4..c579c76fedc 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-query/constants.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-query/constants.ts
@@ -11,7 +11,6 @@ import { isBoolean } from '@woocommerce/types';
 import { QueryBlockAttributes } from './types';
 import { VARIATION_NAME as PRODUCT_TITLE_ID } from './variations/elements/product-title';
 import { VARIATION_NAME as PRODUCT_TEMPLATE_ID } from './variations/elements/product-template';
-import { ImageSizing } from '../../atomic/blocks/product-elements/image/types';

 export const PRODUCT_QUERY_VARIATION_NAME = 'woocommerce/product-query';

@@ -95,7 +94,11 @@ export const INNER_BLOCKS_TEMPLATE: InnerBlockTemplate[] = [
 			[
 				'woocommerce/product-image',
 				{
-					imageSizing: ImageSizing.THUMBNAIL,
+					style: {
+						dimensions: {
+							aspectRatio: '1/1',
+						},
+					},
 				},
 			],
 			[
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-query/variations/related-products.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-query/variations/related-products.tsx
index ed9114f16c3..2a117ac18e0 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-query/variations/related-products.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-query/variations/related-products.tsx
@@ -73,7 +73,11 @@ export const INNER_BLOCKS_TEMPLATE: InnerBlockTemplate[] = [
 				'woocommerce/product-image',
 				{
 					productId: 0,
-					imageSizing: 'cropped',
+					style: {
+						dimensions: {
+							aspectRatio: '1/1',
+						},
+					},
 				},
 			],
 			[
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/products/base-utils.js b/plugins/woocommerce/client/blocks/assets/js/blocks/products/base-utils.js
index 137e70e4f26..84b2bf23fff 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/products/base-utils.js
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/products/base-utils.js
@@ -7,13 +7,21 @@ import clsx from 'clsx';
  * Internal dependencies
  */
 import addToCartButtonMetadata from '../../atomic/blocks/product-elements/button/block.json';
-import { ImageSizing } from '../../atomic/blocks/product-elements/image/types';

 /**
  * The default layout built from the default template.
  */
 export const DEFAULT_PRODUCT_LIST_LAYOUT = [
-	[ 'woocommerce/product-image', { imageSizing: ImageSizing.THUMBNAIL } ],
+	[
+		'woocommerce/product-image',
+		{
+			style: {
+				dimensions: {
+					aspectRatio: '1/1',
+				},
+			},
+		},
+	],
 	[ 'woocommerce/product-title' ],
 	[ 'woocommerce/product-price' ],
 	[ 'woocommerce/product-rating' ],
diff --git a/plugins/woocommerce/patterns/product-collection-3-columns.php b/plugins/woocommerce/patterns/product-collection-3-columns.php
index ccc20d5a3b2..10069655a85 100644
--- a/plugins/woocommerce/patterns/product-collection-3-columns.php
+++ b/plugins/woocommerce/patterns/product-collection-3-columns.php
@@ -11,7 +11,7 @@
 	<!-- wp:woocommerce/product-collection {"query":{"perPage":3,"pages":0,"offset":0,"postType":"product","order":"asc","orderBy":"title","search":"","exclude":[],"inherit":false,"taxQuery":{},"isProductCollectionBlock":true,"woocommerceOnSale":false,"woocommerceStockStatus":["instock","outofstock","onbackorder"],"woocommerceAttributes":[],"woocommerceHandPickedProducts":[]},"tagName":"div","dimensions":{"widthType":"fill","fixedWidth":""},"displayLayout":{"type":"flex","columns":3},"align":"wide"} -->
 	<div class="wp-block-woocommerce-product-collection alignwide">
 		<!-- wp:woocommerce/product-template -->
-		<!-- wp:woocommerce/product-image {"showSaleBadge":false,"aspectRatio":"3/5","imageSizing":"single","isDescendentOfQueryLoop":true} -->
+		<!-- wp:woocommerce/product-image {"showSaleBadge":false,"aspectRatio":"3/5","isDescendentOfQueryLoop":true} -->
 			<!-- wp:woocommerce/product-sale-badge {"isDescendentOfQueryLoop":true,"align":"right"} /-->
 		<!-- /wp:woocommerce/product-image -->

diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AllProducts.php b/plugins/woocommerce/src/Blocks/BlockTypes/AllProducts.php
index f33142b14d0..a86664180bb 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AllProducts.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AllProducts.php
@@ -29,6 +29,7 @@ class AllProducts extends AbstractBlock {
 		$this->asset_data_registry->add( 'minRows', wc_get_theme_support( 'product_blocks::min_rows', 1 ) );
 		$this->asset_data_registry->add( 'maxRows', wc_get_theme_support( 'product_blocks::max_rows', 6 ) );
 		$this->asset_data_registry->add( 'defaultRows', wc_get_theme_support( 'product_blocks::default_rows', 3 ) );
+		$this->asset_data_registry->add( 'thumbnailAspectRatio', $this->get_store_thumbnail_aspect_ratio() );

 		// Hydrate the All Product block with data from the API. This is for the add to cart buttons which show current quantity in cart, and events.
 		if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
@@ -43,4 +44,27 @@ class AllProducts extends AbstractBlock {
 		parent::register_block_type_assets();
 		$this->register_chunk_translations( [ $this->block_name ] );
 	}
+
+	/**
+	 * Get the store thumbnail aspect ratio from WooCommerce Customizer settings.
+	 * This method is a copy from the one in ProductImage.php.
+	 *
+	 * @return string|null CSS aspect ratio value (e.g. "1/1", "4/3"), or null when uncropped.
+	 */
+	private function get_store_thumbnail_aspect_ratio() {
+		$cropping = get_option( 'woocommerce_thumbnail_cropping', '1:1' );
+
+		if ( 'uncropped' === $cropping ) {
+			return null;
+		}
+
+		if ( 'custom' === $cropping ) {
+			$width  = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_width', '4' ) );
+			$height = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_height', '3' ) );
+
+			return $width . '/' . $height;
+		}
+
+		return str_replace( ':', '/', $cropping );
+	}
 }
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductImage.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductImage.php
index 0263d38edca..4c75a87b1d6 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductImage.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductImage.php
@@ -123,6 +123,54 @@ class ProductImage extends AbstractBlock {
 		);
 	}

+	/**
+	 * Get the store thumbnail aspect ratio from WooCommerce Customizer settings.
+	 *
+	 * @return string|null CSS aspect ratio value (e.g. "1/1", "4/3"), or null when uncropped.
+	 */
+	private function get_store_thumbnail_aspect_ratio() {
+		$cropping = get_option( 'woocommerce_thumbnail_cropping', '1:1' );
+
+		if ( 'uncropped' === $cropping ) {
+			return null;
+		}
+
+		if ( 'custom' === $cropping ) {
+			$width  = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_width', '4' ) );
+			$height = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_height', '3' ) );
+
+			return $width . '/' . $height;
+		}
+
+		return str_replace( ':', '/', $cropping );
+	}
+
+	/**
+	 * Resolve the aspect ratio for a product image.
+	 *
+	 * Block-level overrides take priority over store thumbnail cropping settings.
+	 *
+	 * @param array $attributes Parsed block attributes.
+	 * @return string|null CSS aspect ratio value, or null when no ratio should be applied.
+	 */
+	private function resolve_aspect_ratio( $attributes ) {
+		if ( ! empty( $attributes['style']['dimensions']['aspectRatio'] ) ) {
+			return $attributes['style']['dimensions']['aspectRatio'];
+		}
+
+		if ( ! empty( $attributes['aspectRatio'] ) ) {
+			return $attributes['aspectRatio'];
+		}
+
+		// For backwards compatibility, we interpret "thumbnail" as following
+		// the store thumbnail cropping settings.
+		if ( 'thumbnail' === $attributes['imageSizing'] ) {
+			return $this->get_store_thumbnail_aspect_ratio();
+		}
+
+		return null;
+	}
+
 	/**
 	 * Render Image.
 	 *
@@ -132,8 +180,7 @@ class ProductImage extends AbstractBlock {
 	 * @return string
 	 */
 	private function render_image( $product, $attributes, $image_id = null ) {
-		$image_size = 'single' === $attributes['imageSizing'] ? 'woocommerce_single' : 'woocommerce_thumbnail';
-
+		$image_size  = 'large';
 		$image_style = '';

 		if ( ! empty( $attributes['height'] ) ) {
@@ -146,13 +193,9 @@ class ProductImage extends AbstractBlock {
 			$image_style .= sprintf( 'object-fit:%s;', $attributes['scale'] );
 		}

-		// Keep this aspect ratio for backward compatibility.
-		if ( ! empty( $attributes['aspectRatio'] ) ) {
-			$image_style .= sprintf( 'aspect-ratio:%s;', $attributes['aspectRatio'] );
-		}
-
-		if ( ! empty( $attributes['style']['dimensions']['aspectRatio'] ) ) {
-			$image_style .= sprintf( 'aspect-ratio:%s;', $attributes['style']['dimensions']['aspectRatio'] );
+		$aspect_ratio = $this->resolve_aspect_ratio( $attributes );
+		if ( $aspect_ratio ) {
+			$image_style .= sprintf( 'aspect-ratio:%s;', $aspect_ratio );
 		}

 		if ( ! empty( $attributes['style']['dimensions']['minHeight'] ) ) {
@@ -227,8 +270,8 @@ class ProductImage extends AbstractBlock {
 	 *                           not in the post content on editor load.
 	 */
 	protected function enqueue_data( array $attributes = [] ) {
-		$this->asset_data_registry->add( 'isBlockTheme', wp_is_block_theme() );
 		$this->asset_data_registry->add( 'placeholderImgSrcFullSize', wc_placeholder_img_src( 'woocommerce_single' ) );
+		$this->asset_data_registry->add( 'thumbnailAspectRatio', $this->get_store_thumbnail_aspect_ratio() );
 	}

 	/**
@@ -240,13 +283,13 @@ class ProductImage extends AbstractBlock {
 	 * @return string Rendered block type output.
 	 */
 	protected function render( $attributes, $content, $block ) {
-		$parsed_attributes  = $this->parse_attributes( $attributes );
-		$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'extra_classes' ) );
-		$post_id            = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
-		$image_id           = isset( $block->context['imageId'] ) ? (int) $block->context['imageId'] : null;
-		$product            = wc_get_product( $post_id );
-		$aspect_ratio       = $parsed_attributes['aspectRatio'] ?? $parsed_attributes['style']['dimensions']['aspectRatio'] ?? 'auto';
-		$aspect_ratio_class = 'wc-block-components-product-image--aspect-ratio-' . str_replace( '/', '-', $aspect_ratio );
+		$parsed_attributes     = $this->parse_attributes( $attributes );
+		$classes_and_styles    = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'extra_classes' ) );
+		$post_id               = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
+		$image_id              = isset( $block->context['imageId'] ) ? (int) $block->context['imageId'] : null;
+		$product               = wc_get_product( $post_id );
+		$resolved_aspect_ratio = $this->resolve_aspect_ratio( $parsed_attributes );
+		$aspect_ratio_class    = 'wc-block-components-product-image--aspect-ratio-' . ( $resolved_aspect_ratio ? str_replace( '/', '-', $resolved_aspect_ratio ) : 'auto' );

 		$classes = implode(
 			' ',
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductImage.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductImage.php
index 0c145a9c452..568449b6f0d 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductImage.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductImage.php
@@ -11,6 +11,45 @@ use WC_Helper_Product;
  */
 class ProductImage extends \WP_UnitTestCase {

+	/**
+	 * Previous thumbnail cropping option values to restore after tests.
+	 *
+	 * @var array<string, mixed>
+	 */
+	private $prev_thumbnail_cropping_options = array();
+
+	/**
+	 * Set up.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$option_names = array(
+			'woocommerce_thumbnail_cropping',
+			'woocommerce_thumbnail_cropping_custom_width',
+			'woocommerce_thumbnail_cropping_custom_height',
+		);
+
+		foreach ( $option_names as $option_name ) {
+			$this->prev_thumbnail_cropping_options[ $option_name ] = get_option( $option_name, null );
+		}
+	}
+
+	/**
+	 * Tear down.
+	 */
+	public function tearDown(): void {
+		foreach ( $this->prev_thumbnail_cropping_options as $option_name => $value ) {
+			if ( null === $value ) {
+				delete_option( $option_name );
+			} else {
+				update_option( $option_name, $value );
+			}
+		}
+
+		parent::tearDown();
+	}
+
 	/**
 	 * Helper method to create a simple product with an image.
 	 *
@@ -99,13 +138,30 @@ class ProductImage extends \WP_UnitTestCase {
 		);
 	}

+	/**
+	 * Helper to render a product image block for a product.
+	 *
+	 * @param \WC_Product $product Product object.
+	 * @param string      $block_attributes Optional JSON attributes for the block.
+	 * @return string Rendered block markup.
+	 */
+	private function render_product_image_block( $product, $block_attributes = '' ) {
+		$attrs = '' !== $block_attributes ? ' ' . $block_attributes : '';
+
+		return do_blocks(
+			'<!-- wp:woocommerce/single-product {"productId":' . $product->get_id() . '} -->' .
+			'<!-- wp:woocommerce/product-image' . $attrs . ' /-->' .
+			'<!-- /wp:woocommerce/single-product -->'
+		);
+	}
+
 	/**
 	 * Test that the ProductImage block renders correctly for a simple product.
 	 */
 	public function test_product_image_render_simple_product() {
 		$data = $this->create_product_with_image();

-		$markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image /--><!-- /wp:woocommerce/single-product -->' );
+		$markup = $this->render_product_image_block( $data['product'] );

 		$this->assertStringContainsString( 'wc-block-components-product-image', $markup );
 		$this->assertStringContainsString( 'data-testid="product-image"', $markup );
@@ -122,7 +178,7 @@ class ProductImage extends \WP_UnitTestCase {
 	public function test_product_image_render_includes_product_link_by_default() {
 		$data = $this->create_product_with_image();

-		$markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image /--><!-- /wp:woocommerce/single-product -->' );
+		$markup = $this->render_product_image_block( $data['product'] );

 		$this->assertStringContainsString( '<a href="' . $data['product']->get_permalink() . '"', $markup );
 		$this->assertStringContainsString( 'data-wp-on--click="woocommerce/product-collection::actions.viewProduct"', $markup );
@@ -138,7 +194,7 @@ class ProductImage extends \WP_UnitTestCase {
 	public function test_product_image_render_omits_anchor_when_product_link_is_hidden() {
 		$data = $this->create_product_with_image();

-		$markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image {"showProductLink":false} /--><!-- /wp:woocommerce/single-product -->' );
+		$markup = $this->render_product_image_block( $data['product'], '{"showProductLink":false}' );

 		$this->assertStringContainsString( 'data-testid="product-image"', $markup );
 		$this->assertStringNotContainsString( '<a ', $markup );
@@ -162,19 +218,19 @@ class ProductImage extends \WP_UnitTestCase {
 		$variation_image_id = $data['variation_image_ids'][0];

 		// Test that the ProductImage block recognizes the variation image when provided via context.
-		$markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image {"imageId":' . $variation_image_id . '} /--><!-- /wp:woocommerce/single-product -->' );
+		$markup = $this->render_product_image_block( $data['product'], '{"imageId":' . $variation_image_id . '}' );

 		// The block should recognize the variation image as valid and use it.
 		$this->assertStringContainsString( 'data-image-id="' . $variation_image_id . '"', $markup );
 		$this->assertStringContainsString( 'wc-block-components-product-image', $markup );

 		// Test that the block falls back to the main product image when no imageId is provided.
-		$markup_no_image_id = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image /--><!-- /wp:woocommerce/single-product -->' );
+		$markup_no_image_id = $this->render_product_image_block( $data['product'] );
 		$this->assertStringContainsString( 'data-image-id="' . $data['main_image_id'] . '"', $markup_no_image_id );

 		// Test that the block rejects invalid image IDs.
 		$invalid_image_id = 99999;
-		$markup_invalid   = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image {"imageId":' . $invalid_image_id . '} /--><!-- /wp:woocommerce/single-product -->' );
+		$markup_invalid   = $this->render_product_image_block( $data['product'], '{"imageId":' . $invalid_image_id . '}' );
 		// Should fall back to main product image when invalid image ID is provided.
 		$this->assertStringContainsString( 'data-image-id="' . $data['main_image_id'] . '"', $markup_invalid );

@@ -188,18 +244,51 @@ class ProductImage extends \WP_UnitTestCase {
 	}

 	/**
-	 * Test that the ProductImage block renders correctly with different image sizing options.
+	 * Test that the ProductImage block uses store thumbnail cropping aspect ratio.
+	 *
+	 * @testdox Should honor the store thumbnail cropping aspect ratio when imageSizing is thumbnail.
 	 */
-	public function test_product_image_render_with_different_sizing() {
+	public function test_product_image_render_with_store_aspect_ratio() {
 		$data = $this->create_product_with_image();

-		// Test with 'single' image sizing.
-		$markup_single = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image {"imageSizing":"single"} /--><!-- /wp:woocommerce/single-product -->' );
-		$this->assertStringContainsString( 'wc-block-components-product-image', $markup_single );
+		update_option( 'woocommerce_thumbnail_cropping', '1:1' );
+		$markup_single    = $this->render_product_image_block( $data['product'], '{"imageSizing":"single"}' );
+		$markup_thumbnail = $this->render_product_image_block( $data['product'], '{"imageSizing":"thumbnail"}' );
+		$this->assertStringNotContainsString( 'aspect-ratio:1/1', $markup_single );
+		$this->assertStringNotContainsString( 'wc-block-components-product-image--aspect-ratio-1-1', $markup_single );
+		$this->assertStringContainsString( 'aspect-ratio:1/1', $markup_thumbnail );
+		$this->assertStringContainsString( 'wc-block-components-product-image--aspect-ratio-1-1', $markup_thumbnail );
+
+		update_option( 'woocommerce_thumbnail_cropping', 'custom' );
+		update_option( 'woocommerce_thumbnail_cropping_custom_width', '4' );
+		update_option( 'woocommerce_thumbnail_cropping_custom_height', '3' );
+		$markup = $this->render_product_image_block( $data['product'], '{"imageSizing":"thumbnail"}' );
+		$this->assertStringContainsString( 'aspect-ratio:4/3', $markup );
+		$this->assertStringContainsString( 'wc-block-components-product-image--aspect-ratio-4-3', $markup );
+
+		update_option( 'woocommerce_thumbnail_cropping', 'uncropped' );
+		$markup = $this->render_product_image_block( $data['product'], '{"imageSizing":"thumbnail"}' );
+		$this->assertStringNotContainsString( 'aspect-ratio:', $markup );
+		$this->assertStringContainsString( 'wc-block-components-product-image--aspect-ratio-auto', $markup );
+
+		// Clean up.
+		$data['product']->delete( true );
+		wp_delete_attachment( $data['image_id'], true );
+	}
+
+	/**
+	 * Test that block aspect ratio overrides store thumbnail cropping.
+	 *
+	 * @testdox Should prioritize the block aspect ratio over the store thumbnail cropping aspect ratio.
+	 */
+	public function test_product_image_render_with_block_aspect_ratio_override() {
+		$data = $this->create_product_with_image();

-		// Test with 'thumbnail' image sizing.
-		$markup_thumbnail = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image {"imageSizing":"thumbnail"} /--><!-- /wp:woocommerce/single-product -->' );
-		$this->assertStringContainsString( 'wc-block-components-product-image', $markup_thumbnail );
+		update_option( 'woocommerce_thumbnail_cropping', '1:1' );
+		$markup = $this->render_product_image_block( $data['product'], '{"aspectRatio":"3/5","imageSizing":"thumbnail"}' );
+		$this->assertStringContainsString( 'aspect-ratio:3/5', $markup );
+		$this->assertStringContainsString( 'wc-block-components-product-image--aspect-ratio-3-5', $markup );
+		$this->assertStringNotContainsString( 'aspect-ratio:1/1', $markup );

 		// Clean up.
 		$data['product']->delete( true );
@@ -215,7 +304,7 @@ class ProductImage extends \WP_UnitTestCase {
 		$data['product']->set_sale_price( 5 );
 		$data['product']->save();

-		$markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image {"showSaleBadge":true} /--><!-- /wp:woocommerce/single-product -->' );
+		$markup = $this->render_product_image_block( $data['product'], '{"showSaleBadge":true}' );

 		$this->assertStringContainsString( 'wc-block-components-product-image', $markup );
 		$this->assertStringContainsString( 'wp-block-woocommerce-product-sale-badge', $markup );
@@ -250,7 +339,7 @@ class ProductImage extends \WP_UnitTestCase {
 		$product = WC_Helper_Product::create_simple_product();
 		$product->save();

-		$markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $product->get_id() . '} --><!-- wp:woocommerce/product-image /--><!-- /wp:woocommerce/single-product -->' );
+		$markup = $this->render_product_image_block( $product );

 		$this->assertStringContainsString( 'wc-block-components-product-image', $markup );
 		// Should contain placeholder image.