Commit 557e7b0325c for woocommerce

commit 557e7b0325cb36154121e8f44e6a28f175d5d26d
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Tue Oct 13 14:40:33 2026 +0300

    Label saved payment methods in Checkout with the token's display name (#68731)

    * Add the token display name to saved payment method list items

    The Checkout block labels saved payment methods from the items that
    wc_get_account_saved_payment_methods_list() builds. Those items only
    carry card fields (brand, last4, expires), so a token type without
    them, such as a bank account or a wallet, is labelled "Saved token
    for {gateway}" even though its WC_Payment_Token::get_display_name()
    already returns a readable name.

    Add the display name to the base item, before the
    woocommerce_payment_methods_list_item filter runs. Item callbacks
    receive it and can replace it, so an extension can set the label its
    customers see without overriding the token class. The key is
    additive: My Account, the Store API default payment method lookup and
    the Checkout block receive one more field, and nothing is removed.

    * Label saved payment methods in Checkout with their display name

    Saved payment methods that are not cards or eChecks fall back to
    "Saved token for {gateway}" when their item has no brand and last4.
    Stripe's Link, Cash App Pay, Amazon Pay and Klarna tokens, and custom
    token types such as bank accounts, end up with that label.

    Use the item's display_name in that fallback position. Brand plus
    last4 still wins, so labels that extensions build from those fields,
    such as WooPayments' "SEPA IBAN ending in 3000", do not change. Card
    and eCheck labels do not read display_name at all, because extensions
    enrich their brand (for example "Google Pay Visa") and the token's own
    name does not know about that.

    Skip a display name that only repeats the token type. The default
    WC_Payment_Token::get_display_name() returns get_type(), so a token
    class that does not override it, such as PayPal Payments' Apple Pay
    token, would otherwise show its internal type code; it keeps the
    generic label instead.

    Document display_name in the payment store reference and add it to
    the SavedPaymentMethod type as optional, since data from older
    WooCommerce versions does not include it.

    * Fix the missing brand warning in the My Account payment methods template

    The Method column reads $method['method']['brand'] directly. Only card
    and eCheck items set a brand, so any other token type without an
    extension filter that adds one prints an "Undefined array key
    "brand"" warning and a strtolower() null deprecation in that cell.

    Fall back to an empty brand, which renders an empty label as before
    without the notices, and bump the template version.

    * Show a saved method's own name in the My Account payment methods list

    Without a card brand, the Method column was left blank for token types
    such as bank accounts, even though their list item now carries the
    token's display_name. The same token is named in Checkout, so the
    account page showed an empty cell for a method the customer could
    recognise elsewhere.

    When the brand label is empty, show display_name instead. Rows that
    already had a label keep it. As in Checkout, a name that only repeats
    the token type is skipped, since the default
    WC_Payment_Token::get_display_name() returns get_type().

    * Add changelog entry for saved payment method labels

    * Select payment method cells with XPath in the template test

    The test pulled the Method cells out with a regex that only matched
    when class was the first attribute of the td and ended with
    payment-method-method, and counted Delete links by an exact class
    string. A harmless markup change, such as reordering attributes or
    adding a class, would fail the test without any behaviour change.

    Parse the template output with DOMDocument and select the cells and
    links by class token with XPath, as ReviewsListTableTest does.

    * docs(changelog): Describe the saved payment method label fix accurately

    The entry said both screens previously showed "Saved token for", but
    only Checkout used that label. My Account printed an empty cell (and a
    PHP warning) for methods without a brand. Use wording that fits both
    screens, as suggested in review.

    Refs #47941

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * fix(my-account): Prefer a saved method's name over a brandless last4

    When an extension's list-item filter sets last4 without a brand, the
    payment methods table rendered a partial " ending in 1234" label and
    ignored the token's own display name, while Checkout already shows the
    name in that case. Check for a usable name first so both screens agree.

    A name that only repeats the token type is still skipped, and such rows
    keep the last4 label rather than going blank.

    Refs #47941

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
    Co-authored-by: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com>

diff --git a/docs/block-development/reference/data-store/payment.md b/docs/block-development/reference/data-store/payment.md
index 9fa3e2005a2..3160b81cd89 100644
--- a/docs/block-development/reference/data-store/payment.md
+++ b/docs/block-development/reference/data-store/payment.md
@@ -46,6 +46,7 @@ An example of data held within the Payment Data Store is shown below. This examp
           },
           expires: '12/32',
           is_default: true,
+          display_name: 'Visa ending in 4242 (expires 12/32)',
           actions: {
             'delete': {
               url: 'https://store.local/checkout/delete-payment-method/1/?_wpnonce=123456',
@@ -312,7 +313,7 @@ Returns all saved payment methods for the current customer.

 #### _Returns_

--   `object`: The saved payment methods for the current customer. This is an object, it will be specific to each payment method. As an example, Stripe's saved tokens are returned like so:
+-   `object`: The saved payment methods for the current customer. This is an object, it will be specific to each payment method. Since WooCommerce 11.3.0, each saved method includes `display_name`, the label the token's `WC_Payment_Token::get_display_name()` method returns. Checkout builds card and eCheck labels from `method` and `expires` instead, so `display_name` can differ from the label Checkout shows. As an example, Stripe's saved tokens are returned like so:

 ```js
 savedPaymentMethods: {
@@ -325,6 +326,7 @@ savedPaymentMethods: {
 			},
 			expires: '04/24',
 			is_default: true,
+			display_name: 'Visa ending in 4242 (expires 04/24)',
 			actions: {
 				wcs_deletion_error: {
 					url: '#choose_default',
@@ -363,6 +365,7 @@ activeSavedPaymentMethods: {
 			},
 			expires: '04/24',
 			is_default: true,
+			display_name: 'Visa ending in 4242 (expires 04/24)',
 			actions: {
 				wcs_deletion_error: {
 					url: '#choose_default',
diff --git a/plugins/woocommerce/changelog/fix-47941-checkout-token-display-name b/plugins/woocommerce/changelog/fix-47941-checkout-token-display-name
new file mode 100644
index 00000000000..30b61be46fa
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-47941-checkout-token-display-name
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Improve labels for saved payment methods without card details in Checkout and My Account.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/saved-payment-method-options.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/saved-payment-method-options.tsx
index 8aeb4620a14..fbfad06d391 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/saved-payment-method-options.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/saved-payment-method-options.tsx
@@ -21,7 +21,10 @@ import { isNull } from '@woocommerce/types';
  * Internal dependencies
  */
 import { getCanMakePaymentArg } from '@woocommerce/block-data/payment/utils/check-payment-methods';
-import { CustomerPaymentMethodConfiguration } from '@woocommerce/block-data/payment/types';
+import type {
+	CustomerPaymentMethodConfiguration,
+	SavedPaymentMethod,
+} from '@woocommerce/block-data/payment/types';

 /**
  * Returns the option object for a cc or echeck saved payment method token.
@@ -45,11 +48,13 @@ const getCcOrEcheckLabel = ( {
 /**
  * Returns the option object for any non specific saved payment method.
  */
-const getDefaultLabel = ( {
-	method,
-}: {
-	method: CustomerPaymentMethodConfiguration;
-} ): string => {
+const getDefaultLabel = (
+	{
+		method,
+		display_name: displayName,
+	}: Pick< SavedPaymentMethod, 'method' | 'display_name' >,
+	type: string
+): string => {
 	/* For saved payment methods with brand & last 4 */
 	if ( method.brand && method.last4 ) {
 		return sprintf(
@@ -60,6 +65,16 @@ const getDefaultLabel = ( {
 		);
 	}

+	/* For saved payment methods that provide their own display name. The default WC_Payment_Token::get_display_name() returns the token type, which is not a label. */
+	const trimmedDisplayName =
+		typeof displayName === 'string' ? displayName.trim() : '';
+	if (
+		trimmedDisplayName !== '' &&
+		trimmedDisplayName.toLowerCase() !== type.toLowerCase()
+	) {
+		return trimmedDisplayName;
+	}
+
 	/* For saved payment methods without brand & last 4 */
 	return sprintf(
 		/* translators: %s is the name of the payment method gateway. */
@@ -120,7 +135,7 @@ const SavedPaymentMethodOptions = () => {
 					name: `wc-saved-payment-method-token-${ paymentMethodSlug }`,
 					label: isCC
 						? getCcOrEcheckLabel( paymentMethod )
-						: getDefaultLabel( paymentMethod ),
+						: getDefaultLabel( paymentMethod, type ),
 					value: paymentMethod.tokenId.toString(),
 					onChange: ( token: string ) => {
 						const savedTokenKey = `wc-${ paymentMethodSlug }-payment-token`;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/saved-payment-method-options.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/saved-payment-method-options.tsx
index 0d7dc2cffb0..63f58eb8b16 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/saved-payment-method-options.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/saved-payment-method-options.tsx
@@ -39,6 +39,8 @@ mockedUseSelect.mockImplementation(
 								{
 									tokenId: 1,
 									expires: '1/2099',
+									// Card labels come from brand, last4 and expiry; extensions enrich those fields, so display_name is not used.
+									display_name: 'Card display name',
 									method: {
 										brand: 'Visa',
 										gateway:
@@ -77,6 +79,56 @@ mockedUseSelect.mockImplementation(
 									},
 								},
 							],
+							fake_custom: [
+								{
+									tokenId: 5,
+									expires: 'N/A',
+									display_name:
+										'Test bank account ending in 9876',
+									method: {
+										brand: '',
+										gateway:
+											'can-pay-true-test-payment-method',
+										last4: '',
+									},
+								},
+								{
+									tokenId: 6,
+									expires: 'N/A',
+									display_name: 'Token display name',
+									method: {
+										brand: 'SEPA IBAN',
+										gateway:
+											'can-pay-true-test-payment-method',
+										last4: '3000',
+									},
+								},
+								{
+									tokenId: 7,
+									expires: 'N/A',
+									display_name: '   ',
+									method: {
+										brand: '',
+										gateway:
+											'can-pay-true-test-payment-method',
+										last4: '',
+									},
+								},
+							],
+							applepay: [
+								{
+									tokenId: 8,
+									expires: 'N/A',
+									// The default WC_Payment_Token::get_display_name() returns the token type.
+									display_name: 'ApplePay',
+									method: {
+										brand: 'ApplePay #8',
+										gateway:
+											'can-pay-true-test-payment-method',
+										last4: '',
+									},
+								},
+							],
 						};
 					},
 				};
@@ -127,6 +179,28 @@ describe( 'SavedPaymentMethodOptions', () => {
 				'Cartes Bancaires ending in 1001 (expires 1/2099)'
 			)
 		).toBeInTheDocument();
+
+		// Custom token type without brand and last4: the token's own display name labels the radio.
+		expect(
+			screen.getByRole( 'radio', {
+				name: 'Test bank account ending in 9876',
+			} )
+		).toBeInTheDocument();
+
+		// Custom token type with brand and last4 from an extension: that label wins over display_name.
+		expect(
+			screen.getByRole( 'radio', { name: 'SEPA IBAN ending in 3000' } )
+		).toBeInTheDocument();
+
+		// A blank display name, or one that is only the token type, falls back to the generic label.
+		const fallbackRadios = screen.getAllByRole( 'radio', {
+			name: 'Saved token for can-pay-true-test-payment-method',
+		} );
+		expect(
+			fallbackRadios.map(
+				( radio ) => ( radio as HTMLInputElement ).value
+			)
+		).toEqual( [ '7', '8' ] );
 	} );
 	it( "does not show saved methods when the method's canPay function returns false", () => {
 		registerPaymentMethod( {
diff --git a/plugins/woocommerce/client/blocks/packages/public-api/block-data/payment/types.ts b/plugins/woocommerce/client/blocks/packages/public-api/block-data/payment/types.ts
index 8be6c65d0e1..df290fcfa7c 100644
--- a/plugins/woocommerce/client/blocks/packages/public-api/block-data/payment/types.ts
+++ b/plugins/woocommerce/client/blocks/packages/public-api/block-data/payment/types.ts
@@ -34,6 +34,10 @@ export interface SavedPaymentMethod {
 	method: CustomerPaymentMethodConfiguration;
 	expires: string;
 	is_default: boolean;
+	/**
+	 * The token's own label, from `WC_Payment_Token::get_display_name()`. Absent in data from WooCommerce versions before 11.3.0.
+	 */
+	display_name?: string;
 	tokenId: number;
 	actions: ObjectType;
 }
diff --git a/plugins/woocommerce/includes/wc-account-functions.php b/plugins/woocommerce/includes/wc-account-functions.php
index a5d0071e76f..5ed29ff76ee 100644
--- a/plugins/woocommerce/includes/wc-account-functions.php
+++ b/plugins/woocommerce/includes/wc-account-functions.php
@@ -412,12 +412,13 @@ function wc_get_account_saved_payment_methods_list( $list, $customer_id ) {

 		$type            = strtolower( $payment_token->get_type() );
 		$list[ $type ][] = array(
-			'method'     => array(
+			'method'       => array(
 				'gateway' => $payment_token->get_gateway_id(),
 			),
-			'expires'    => esc_html__( 'N/A', 'woocommerce' ),
-			'is_default' => $payment_token->is_default(),
-			'actions'    => array(
+			'expires'      => esc_html__( 'N/A', 'woocommerce' ),
+			'is_default'   => $payment_token->is_default(),
+			'display_name' => $payment_token->get_display_name(),
+			'actions'      => array(
 				'delete' => array(
 					'url'  => $delete_url,
 					'name' => esc_html__( 'Delete', 'woocommerce' ),
diff --git a/plugins/woocommerce/templates/myaccount/payment-methods.php b/plugins/woocommerce/templates/myaccount/payment-methods.php
index 46b629e70b0..9ec0a4a2f37 100644
--- a/plugins/woocommerce/templates/myaccount/payment-methods.php
+++ b/plugins/woocommerce/templates/myaccount/payment-methods.php
@@ -14,7 +14,7 @@
  *
  * @see https://woocommerce.com/document/template-structure/
  * @package WooCommerce\Templates
- * @version 8.9.0
+ * @version 11.3.0
  */

 defined( 'ABSPATH' ) || exit;
@@ -44,11 +44,16 @@ do_action( 'woocommerce_before_account_payment_methods', $has_methods ); ?>
 							if ( has_action( 'woocommerce_account_payment_methods_column_' . $column_id ) ) {
 								do_action( 'woocommerce_account_payment_methods_column_' . $column_id, $method );
 							} elseif ( 'method' === $column_id ) {
-								if ( ! empty( $method['method']['last4'] ) ) {
+								$method_label = wc_get_credit_card_type_label( $method['method']['brand'] ?? '' );
+								$display_name = isset( $method['display_name'] ) && is_string( $method['display_name'] ) ? trim( $method['display_name'] ) : '';
+								// Without a brand, use the token's own name. The default name is only the token type, which is not a label.
+								if ( '' === $method_label && '' !== $display_name && strtolower( $display_name ) !== $type ) {
+									echo esc_html( $display_name );
+								} elseif ( ! empty( $method['method']['last4'] ) ) {
 									/* translators: 1: credit card type 2: last 4 digits */
-									echo sprintf( esc_html__( '%1$s ending in %2$s', 'woocommerce' ), esc_html( wc_get_credit_card_type_label( $method['method']['brand'] ) ), esc_html( $method['method']['last4'] ) );
+									printf( esc_html__( '%1$s ending in %2$s', 'woocommerce' ), esc_html( $method_label ), esc_html( $method['method']['last4'] ) );
 								} else {
-									echo esc_html( wc_get_credit_card_type_label( $method['method']['brand'] ) );
+									echo esc_html( $method_label );
 								}
 							} elseif ( 'expires' === $column_id ) {
 								echo esc_html( $method['expires'] );
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/account/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/account/functions.php
index 587aa5a218e..3b2ef509191 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/account/functions.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/account/functions.php
@@ -304,14 +304,15 @@ class WC_Tests_Account_Functions extends WC_Unit_Test_Case {
 			array(
 				'cc' => array(
 					array(
-						'method'     => array(
+						'method'       => array(
 							'gateway' => WC_Gateway_BACS::ID,
 							'last4'   => '1234',
 							'brand'   => 'Mastercard',
 						),
-						'expires'    => '12/20',
-						'is_default' => true,
-						'actions'    => array(
+						'expires'      => '12/20',
+						'is_default'   => true,
+						'display_name' => 'Mastercard ending in 1234 (expires 12/20)',
+						'actions'      => array(
 							'delete' => array(
 								'url'  => $delete_url,
 								'name' => 'Delete',
diff --git a/plugins/woocommerce/tests/php/helpers/FakeCustomPaymentToken.php b/plugins/woocommerce/tests/php/helpers/FakeCustomPaymentToken.php
new file mode 100644
index 00000000000..bd39735a0c0
--- /dev/null
+++ b/plugins/woocommerce/tests/php/helpers/FakeCustomPaymentToken.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * Fake custom payment token helper.
+ *
+ * @package WooCommerce\Tests
+ */
+
+declare( strict_types = 1 );
+
+/**
+ * A custom payment token type with its own display name and no card details, as a payment extension would define one.
+ *
+ * Map the type to this class for the duration of a test:
+ *
+ *     add_filter( 'woocommerce_payment_token_class', fn( $class_name, $type ) => FakeCustomPaymentToken::TYPE === $type ? FakeCustomPaymentToken::class : $class_name, 10, 2 );
+ */
+class FakeCustomPaymentToken extends WC_Payment_Token {
+
+	/**
+	 * The token type this class represents.
+	 */
+	public const TYPE = 'fake_custom';
+
+	/**
+	 * The display name every token of this type returns.
+	 */
+	public const DISPLAY_NAME = 'Test bank account ending in 9876';
+
+	/**
+	 * Token type.
+	 *
+	 * @var string
+	 */
+	protected $type = self::TYPE;
+
+	/**
+	 * Get the token's display name.
+	 *
+	 * @param string $deprecated Deprecated argument.
+	 * @return string
+	 */
+	public function get_display_name( $deprecated = '' ) {
+		return self::DISPLAY_NAME;
+	}
+}
diff --git a/plugins/woocommerce/tests/php/includes/templates/class-wc-my-account-payment-methods-template-test.php b/plugins/woocommerce/tests/php/includes/templates/class-wc-my-account-payment-methods-template-test.php
new file mode 100644
index 00000000000..cf40ef9ee9d
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/templates/class-wc-my-account-payment-methods-template-test.php
@@ -0,0 +1,81 @@
+<?php
+/**
+ * Tests for the My Account payment methods template.
+ */
+
+declare( strict_types = 1 );
+
+/**
+ * My Account payment methods template test.
+ */
+class WC_My_Account_Payment_Methods_Template_Test extends WC_Unit_Test_Case {
+
+	/**
+	 * @testdox Should label saved methods without a card brand by their own name, even with last4, keep brand labels, and skip names that only repeat the token type.
+	 */
+	public function test_labels_saved_methods_without_brand(): void {
+		add_filter(
+			'woocommerce_payment_token_class',
+			static function ( $class_name, $type ) {
+				return FakeCustomPaymentToken::TYPE === $type ? FakeCustomPaymentToken::class : $class_name;
+			},
+			10,
+			2
+		);
+		add_filter(
+			'woocommerce_payment_methods_list_item',
+			static function ( $item, $token ) {
+				if ( 'branded' === $token->get_token() ) {
+					$item['method']['brand'] = 'wallet';
+				}
+				if ( in_array( $token->get_token(), array( 'last4', 'last4-unnamed' ), true ) ) {
+					$item['method']['last4'] = '1234';
+				}
+				if ( in_array( $token->get_token(), array( 'unnamed', 'last4-unnamed' ), true ) ) {
+					$item['display_name'] = 'Fake_Custom';
+				}
+				return $item;
+			},
+			10,
+			2
+		);
+
+		$user_id = self::factory()->user->create();
+		wp_set_current_user( $user_id );
+		foreach ( array( 'plain', 'branded', 'unnamed', 'last4', 'last4-unnamed' ) as $token_value ) {
+			$token = new FakeCustomPaymentToken();
+			$token->set_token( $token_value );
+			$token->set_gateway_id( WC_Gateway_BACS::ID );
+			$token->set_user_id( $user_id );
+			$token->save();
+		}
+
+		$html = wc_get_template_html( 'myaccount/payment-methods.php' );
+
+		$document = new DOMDocument();
+		$errors   = libxml_use_internal_errors( true );
+		$document->loadHTML( '<!doctype html><html><body>' . $html . '</body></html>' );
+		libxml_clear_errors();
+		libxml_use_internal_errors( $errors );
+		$xpath = new DOMXPath( $document );
+
+		$method_cells = $xpath->query( '//td[contains(concat(" ", normalize-space(@class), " "), " payment-method-method ")]' );
+		$delete_links = $xpath->query( '//a[contains(concat(" ", normalize-space(@class), " "), " delete ")]' );
+		if ( false === $method_cells || false === $delete_links ) {
+			throw new \RuntimeException( 'Unable to query the payment methods table.' );
+		}
+
+		$method_labels = array();
+		foreach ( $method_cells as $method_cell ) {
+			// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOM API property name.
+			$method_labels[] = trim( $method_cell->textContent );
+		}
+		$this->assertSame(
+			array( FakeCustomPaymentToken::DISPLAY_NAME, 'Wallet', '', FakeCustomPaymentToken::DISPLAY_NAME, 'ending in 1234' ),
+			$method_labels,
+			'Rows without a brand should show the token name even with last4, a brand should win, and a name equal to the token type should be skipped.'
+		);
+		$this->assertSame( 5, $delete_links->length, 'Every row should keep its Delete action.' );
+		$this->assertStringNotContainsString( 'Undefined array key', $html, 'The template should not emit PHP notices for tokens without a brand.' );
+	}
+}
diff --git a/plugins/woocommerce/tests/php/includes/wc-account-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-account-functions-test.php
new file mode 100644
index 00000000000..66e349ff17c
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/wc-account-functions-test.php
@@ -0,0 +1,50 @@
+<?php
+/**
+ * Tests for the account functions.
+ *
+ * @package WooCommerce\Tests
+ */
+
+declare( strict_types = 1 );
+
+/**
+ * Tests for wc-account-functions.php.
+ */
+class WC_Account_Functions_Test extends WC_Unit_Test_Case {
+
+	/**
+	 * @testdox Should give saved method list item filters the token's own display name, and keep the name a filter sets.
+	 */
+	public function test_saved_payment_methods_list_item_carries_token_display_name(): void {
+		add_filter(
+			'woocommerce_payment_token_class',
+			static function ( $class_name, $type ) {
+				return FakeCustomPaymentToken::TYPE === $type ? FakeCustomPaymentToken::class : $class_name;
+			},
+			10,
+			2
+		);
+
+		$user_id = self::factory()->user->create();
+		$token   = new FakeCustomPaymentToken();
+		$token->set_token( 'fake-custom-token' );
+		$token->set_gateway_id( WC_Gateway_BACS::ID );
+		$token->set_user_id( $user_id );
+		$token->save();
+
+		$display_name_seen_by_filter = null;
+		add_filter(
+			'woocommerce_payment_methods_list_item',
+			static function ( $item ) use ( &$display_name_seen_by_filter ) {
+				$display_name_seen_by_filter = $item['display_name'] ?? null;
+				$item['display_name']        = 'Label set by an extension';
+				return $item;
+			}
+		);
+
+		$list = wc_get_account_saved_payment_methods_list( array(), $user_id );
+
+		$this->assertSame( FakeCustomPaymentToken::DISPLAY_NAME, $display_name_seen_by_filter, 'Item filters should receive the token display name.' );
+		$this->assertSame( 'Label set by an extension', $list[ FakeCustomPaymentToken::TYPE ][0]['display_name'], 'A display name set by an item filter should be kept.' );
+	}
+}