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=' ) );
+ }
+}