Commit 154ebb53489 for woocommerce

commit 154ebb534893c368eb6ae9145b27d2812c6837b1
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date:   Mon Aug 3 11:07:46 2026 +0200

    Display user avatar in Customer Account block (#67032)

    * Display user avatar in Customer Account block

    * Don't display avatars in the editor

    * Minor improvements

    * Add changelog

    * Fix #67064

    * Wrap new selectors with :where()

    * Minor fixes

    * Improve CSS selector to not use .icon

    * Add extra test for when avatars are disabled

    * Make the disabled-avatar test provide an avatar fixture

    * Add help text to icon selector mentioning it might be replaced by the avatar

diff --git a/plugins/woocommerce/changelog/fix-42449-customer-account-avatar b/plugins/woocommerce/changelog/fix-42449-customer-account-avatar
new file mode 100644
index 00000000000..e13cedb2b7f
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-42449-customer-account-avatar
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Display user avatar in Customer Account block
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/customer-account/block.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/customer-account/block.tsx
index df07f705336..3f42b2ec2d8 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/customer-account/block.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/customer-account/block.tsx
@@ -31,9 +31,15 @@ const AccountIcon = ( {
 	displayStyle: DisplayStyle;
 	iconClass: string;
 } ) => {
-	return displayStyle === DisplayStyle.TEXT_ONLY ? null : (
-		<Icon className={ iconClass } icon={ icons[ iconStyle ] } size={ 18 } />
-	);
+	return displayStyle !== DisplayStyle.TEXT_ONLY ? (
+		<div className="wc-block-customer-account__visual">
+			<Icon
+				className={ iconClass }
+				icon={ icons[ iconStyle ] }
+				size={ 18 }
+			/>
+		</div>
+	) : null;
 };

 const Label = ( { displayStyle }: { displayStyle: DisplayStyle } ) => {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/customer-account/sidebar-settings.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/customer-account/sidebar-settings.tsx
index 6edd0ff0473..c8c17a23e49 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/customer-account/sidebar-settings.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/customer-account/sidebar-settings.tsx
@@ -109,6 +109,10 @@ export const BlockSettings = ( {
 								iconStyle: value,
 							} )
 						}
+						help={ __(
+							'When a logged-in customer has a profile photo, it replaces the icon.',
+							'woocommerce'
+						) }
 						className="wc-block-editor-customer-account__icon-style-toggle"
 					>
 						<ToggleGroupControlOption
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/customer-account/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/customer-account/style.scss
index c553ff6c3ce..1fb0286a386 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/customer-account/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/customer-account/style.scss
@@ -61,10 +61,34 @@
 		text-decoration: underline;
 	}

+	:where(.wc-block-customer-account__visual) {
+		position: relative;
+	}
+
+	:where(.wc-block-customer-account__visual + .label) {
+		padding-left: em($gap-smallest);
+	}
+
+	:where(.wc-block-customer-account__visual > svg),
 	.wc-block-customer-account__account-icon {
+		display: block;
 		width: em($grid-unit-30);
 		height: em($grid-unit-30);
 	}
+
+	:where(.wc-block-customer-account__avatar) {
+		// SVG icons have some transparent spacing around the actual icon.
+		// So we add some spacing around the avatar to simulate a similar size.
+		$avatar_spacing: 4%;
+
+		border-radius: 50%;
+		position: absolute;
+		left: 50%;
+		top: 50%;
+		transform: translate(-50%, -50%);
+		width: calc(100% - ($avatar_spacing * 2));
+		height: auto;
+	}
 }

 :where(.wp-block-woocommerce-customer-account) {
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php b/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php
index 55ebef2a9e9..27feaa7faae 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php
@@ -148,8 +148,6 @@ class CustomerAccount extends AbstractBlock {
 	 * @return string Rendered block output.
 	 */
 	private function render_link( $attributes, $classes_and_styles, $account_link, $aria_label, $label_markup ) {
-		$allowed_svg = $this->get_allowed_svg();
-
 		ob_start();
 		?>
 		<div
@@ -161,7 +159,7 @@ class CustomerAccount extends AbstractBlock {
 				href="<?php echo esc_url( $account_link ); ?>"
 				<?php echo $aria_label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
 			>
-				<?php echo wp_kses( $this->render_icon( $attributes ), $allowed_svg ); ?>
+				<?php echo $this->render_visual( $attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
 				<?php echo $label_markup; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
 			</a>
 		</div>
@@ -180,8 +178,6 @@ class CustomerAccount extends AbstractBlock {
 	 * @return string Rendered block output.
 	 */
 	private function render_dropdown( $attributes, $classes_and_styles, $aria_label, $label_markup ) {
-		$allowed_svg = $this->get_allowed_svg();
-
 		$context = array(
 			'isDropdownOpen' => false,
 			'showAbove'      => false,
@@ -213,7 +209,7 @@ class CustomerAccount extends AbstractBlock {
 				data-wp-bind--aria-expanded="context.isDropdownOpen"
 				data-wp-on--click="actions.toggleDropdown"
 			>
-				<?php echo wp_kses( $this->render_icon( $attributes ), $allowed_svg ); ?>
+				<?php echo $this->render_visual( $attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
 				<?php echo $label_markup; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
 				<?php echo $this->render_caret_icon(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
 			</button>
@@ -300,43 +296,12 @@ class CustomerAccount extends AbstractBlock {
 		</svg>';
 	}

-	/**
-	 * Get the allowed SVG tags and attributes for wp_kses.
-	 *
-	 * @return array Allowed SVG elements and attributes.
-	 */
-	private function get_allowed_svg() {
-		return array(
-			'svg'    => array(
-				'class'   => true,
-				'xmlns'   => true,
-				'width'   => true,
-				'height'  => true,
-				'viewbox' => true,
-			),
-			'path'   => array(
-				'd'         => true,
-				'fill'      => true,
-				'fill-rule' => true,
-				'clip-rule' => true,
-			),
-			'circle' => array(
-				'cx'           => true,
-				'cy'           => true,
-				'r'            => true,
-				'stroke'       => true,
-				'stroke-width' => true,
-				'fill'         => true,
-			),
-		);
-	}
-
 	/**
 	 * Gets the icon to render depending on the iconStyle and displayStyle.
 	 *
 	 * @param array $attributes Block attributes.
 	 *
-	 * @return string Label to render on the block
+	 * @return string SVG icon markup.
 	 */
 	private function render_icon( $attributes ) {
 		if ( self::TEXT_ONLY === $attributes['displayStyle'] ) {
@@ -344,7 +309,7 @@ class CustomerAccount extends AbstractBlock {
 		}

 		if ( self::DISPLAY_LINE === $attributes['iconStyle'] ) {
-			return '<svg class="' . $attributes['iconClass'] . '" viewBox="1 1 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
+			return '<svg class="' . esc_attr( $attributes['iconClass'] ) . '" viewBox="1 1 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
 				<circle
 					cx="16"
 					cy="10.5"
@@ -363,7 +328,7 @@ class CustomerAccount extends AbstractBlock {
 		}

 		if ( self::DISPLAY_ALT === $attributes['iconStyle'] ) {
-			return '<svg class="' . $attributes['iconClass'] . '" xmlns="http://www.w3.org/2000/svg" viewBox="-4 -4 25 25">
+			return '<svg class="' . esc_attr( $attributes['iconClass'] ) . '" xmlns="http://www.w3.org/2000/svg" viewBox="-4 -4 25 25">
 				<path
 					d="M9 0C4.03579 0 0 4.03579 0 9C0 13.9642 4.03579 18 9 18C13.9642 18 18 13.9642 18 9C18 4.03579 13.9642 0 9 0ZM9 4.32C10.5347 4.32 11.7664 5.57056 11.7664 7.08638C11.7664 8.62109 10.5158 9.85277 9 9.85277C7.4653 9.85277 6.23362 8.60221 6.23362 7.08638C6.23362 5.57056 7.46526 4.32 9 4.32ZM9 10.7242C11.1221 10.7242 12.96 12.2021 13.7937 14.4189C12.5242 15.5559 10.8379 16.238 9 16.238C7.16207 16.238 5.49474 15.5369 4.20632 14.4189C5.05891 12.2021 6.87793 10.7242 9 10.7242Z"
 					fill="currentColor"
@@ -371,7 +336,7 @@ class CustomerAccount extends AbstractBlock {
 			</svg>';
 		}

-		return '<svg class="' . $attributes['iconClass'] . '" xmlns="http://www.w3.org/2000/svg" viewBox="-5 -5 25 25">
+		return '<svg class="' . esc_attr( $attributes['iconClass'] ) . '" xmlns="http://www.w3.org/2000/svg" viewBox="-5 -5 25 25">
 			<path
 				fill-rule="evenodd"
 				clip-rule="evenodd"
@@ -381,6 +346,39 @@ class CustomerAccount extends AbstractBlock {
 		</svg>';
 	}

+	/**
+	 * Render the user avatar and icon.
+	 *
+	 * @param array $attributes Block attributes.
+	 *
+	 * @return string Avatar and icon markup.
+	 */
+	private function render_visual( $attributes ) {
+		if ( self::TEXT_ONLY === $attributes['displayStyle'] ) {
+			return '';
+		}
+
+		$icon    = $this->render_icon( $attributes );
+		$user_id = get_current_user_id();
+		if ( $user_id ) {
+			// We use `blank` as the default so if the user has no avatar, the icon underneath is visible.
+			$avatar = get_avatar(
+				$user_id,
+				48,
+				'blank',
+				'',
+				array(
+					'class' => 'wc-block-customer-account__avatar',
+				)
+			);
+
+			if ( $avatar ) {
+				return '<div class="wc-block-customer-account__visual">' . $icon . wp_kses_post( $avatar ) . '</div>';
+			}
+		}
+		return '<div class="wc-block-customer-account__visual">' . $icon . '</div>';
+	}
+
 	/**
 	 * Gets the label to render depending on the displayStyle.
 	 *
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/CustomerAccountTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/CustomerAccountTest.php
new file mode 100644
index 00000000000..8b53ed17b0e
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/CustomerAccountTest.php
@@ -0,0 +1,150 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
+
+use WP_UnitTestCase;
+
+/**
+ * Tests for the Customer Account block.
+ */
+class CustomerAccountTest extends WP_UnitTestCase {
+
+	/**
+	 * User ID for tests.
+	 *
+	 * @var int
+	 */
+	private int $user_id;
+
+	/**
+	 * Original show_avatars option value.
+	 *
+	 * @var mixed
+	 */
+	private $original_show_avatars;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+		$this->user_id               = $this->factory->user->create();
+		$this->original_show_avatars = get_option( 'show_avatars' );
+		update_option( 'show_avatars', 1 );
+	}
+
+	/**
+	 * Tear down test fixtures.
+	 */
+	public function tearDown(): void {
+		remove_all_filters( 'pre_get_avatar_data' );
+		wp_set_current_user( 0 );
+		wp_delete_user( $this->user_id );
+		update_option( 'show_avatars', $this->original_show_avatars );
+		parent::tearDown();
+	}
+
+	/**
+	 * Render the Customer Account block via do_blocks().
+	 *
+	 * @param string $attrs JSON object string for block attributes.
+	 * @return string Rendered markup.
+	 */
+	private function render_customer_account( string $attrs = '' ): string {
+		return do_blocks( "<!-- wp:woocommerce/customer-account {$attrs} /-->" );
+	}
+
+	/**
+	 * @testdox Should render the default account icon when the user is not logged in.
+	 */
+	public function test_renders_icon_when_user_is_not_logged_in(): void {
+		wp_set_current_user( 0 );
+
+		$markup = $this->render_customer_account(
+			'{"iconClass":"wc-block-customer-account__account-icon"}'
+		);
+
+		$this->assertStringContainsString( '<svg', $markup );
+		$this->assertStringNotContainsString( 'wc-block-customer-account__avatar', $markup );
+	}
+
+	/**
+	 * @testdox Should render the user avatar when a custom avatar is available.
+	 */
+	public function test_renders_avatar_when_user_has_custom_avatar(): void {
+		wp_set_current_user( $this->user_id );
+
+		add_filter(
+			'pre_get_avatar_data',
+			function ( $args ) {
+				$args['url'] = 'https://example.com/custom-avatar.jpg';
+				return $args;
+			}
+		);
+
+		$markup = $this->render_customer_account(
+			'{"iconClass":"wc-block-customer-account__account-icon"}'
+		);
+
+		$this->assertStringContainsString( 'wc-block-customer-account__avatar', $markup );
+		$this->assertStringContainsString( 'custom-avatar.jpg', $markup );
+	}
+
+	/**
+	 * @testdox Should not render avatar markup when show_avatars is disabled.
+	 */
+	public function test_does_not_render_avatar_when_show_avatars_is_disabled(): void {
+		wp_set_current_user( $this->user_id );
+		update_option( 'show_avatars', 0 );
+
+		add_filter(
+			'pre_get_avatar_data',
+			function ( $args ) {
+				$args['url'] = 'https://example.com/custom-avatar.jpg';
+				return $args;
+			}
+		);
+
+		$markup = $this->render_customer_account(
+			'{"iconClass":"wc-block-customer-account__account-icon"}'
+		);
+
+		$this->assertStringNotContainsString( 'wc-block-customer-account__avatar', $markup );
+		$this->assertStringNotContainsString( 'custom-avatar.jpg', $markup );
+	}
+
+	/**
+	 * Data provider for displayStyle attribute tests.
+	 *
+	 * @return array<string, array{string, bool, bool, bool}>
+	 */
+	public function provider_display_style(): array {
+		return array(
+			'icon and text' => array( 'icon_and_text', true, true, false ),
+			'text only'     => array( 'text_only', false, true, false ),
+			'icon only'     => array( 'icon_only', true, false, true ),
+		);
+	}
+
+	/**
+	 * @testdox Should render icon, label, and aria-label according to displayStyle.
+	 *
+	 * @dataProvider provider_display_style
+	 *
+	 * @param string $display_style Display style attribute.
+	 * @param bool   $has_icon      Whether an SVG icon is expected.
+	 * @param bool   $has_label     Whether a text label is expected.
+	 * @param bool   $has_aria      Whether an aria-label is expected.
+	 */
+	public function test_display_style( string $display_style, bool $has_icon, bool $has_label, bool $has_aria ): void {
+		$markup = $this->render_customer_account(
+			'{"displayStyle":"' . $display_style . '","iconClass":"wc-block-customer-account__account-icon"}'
+		);
+
+		$this->assertSame( $has_icon, str_contains( $markup, '<svg' ) );
+		$this->assertSame( $has_label, str_contains( $markup, 'class="label"' ) );
+		$this->assertSame( $has_aria, str_contains( $markup, 'aria-label=' ) );
+	}
+}