Commit 447e7d8f964 for woocommerce
commit 447e7d8f96406210b61767bb78e730c76dfd36b4
Author: Hannah Tinkler <hannah.tinkler@gmail.com>
Date: Thu Sep 24 14:33:29 2026 +0100
Return the account username and email with each push token (#68714)
The diagnostics devices page showed only the store's numeric user ID, which
cannot be matched to anything in WPCOM. Each token on the index now carries
the account's user_login and user_email, null when the user no longer exists.
The payload the dispatcher sends to WPCOM is unchanged.
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/Controllers/PushTokenRestController.php b/plugins/woocommerce/src/Internal/PushNotifications/Controllers/PushTokenRestController.php
index 842799eb594..3f8fcfc4c39 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/Controllers/PushTokenRestController.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/Controllers/PushTokenRestController.php
@@ -138,8 +138,8 @@ class PushTokenRestController extends RestApiControllerBase {
/**
* Returns all push tokens for roles that can receive push notifications,
- * along with when each token was registered and when the app last
- * confirmed it.
+ * along with when each token was registered, when the app last confirmed
+ * it, and the username and email of the account it belongs to.
*
* @since 10.8.0
*
@@ -169,12 +169,7 @@ class PushTokenRestController extends RestApiControllerBase {
}
$response = new WP_REST_Response(
- array(
- 'tokens' => array_map(
- fn ( $token ) => $token->to_rest_format(),
- $result['tokens']
- ),
- ),
+ array( 'tokens' => $this->prepare_tokens_for_response( $result['tokens'] ) ),
WP_Http::OK
);
@@ -184,6 +179,59 @@ class PushTokenRestController extends RestApiControllerBase {
return $response;
}
+ /**
+ * Formats tokens for the index response.
+ *
+ * The account fields are added here rather than in
+ * {@see PushToken::to_rest_format()}, so the users for a whole page are
+ * loaded in one query rather than one per token.
+ *
+ * Requesting three named columns rather than user objects skips loading
+ * usermeta, and keeps WP_User_Query's result cache on.
+ *
+ * @param PushToken[] $tokens The tokens to format.
+ * @return array[]
+ */
+ private function prepare_tokens_for_response( array $tokens ): array {
+ $user_ids = array_values(
+ array_unique(
+ array_filter(
+ array_map( fn ( PushToken $token ) => $token->get_user_id(), $tokens )
+ )
+ )
+ );
+
+ if ( ! $user_ids ) {
+ return array();
+ }
+
+ $users = array_column(
+ get_users(
+ array(
+ 'include' => $user_ids,
+ 'fields' => array( 'ID', 'user_login', 'user_email' ),
+ )
+ ),
+ null,
+ 'ID'
+ );
+
+ return array_map(
+ function ( PushToken $token ) use ( $users ) {
+ $user = $users[ $token->get_user_id() ] ?? null;
+
+ return array_merge(
+ $token->to_rest_format(),
+ array(
+ 'user_login' => $user ? $user->user_login : null,
+ 'user_email' => $user ? $user->user_email : null,
+ )
+ );
+ },
+ $tokens
+ );
+ }
+
/**
* Creates a push token record.
*
@@ -308,6 +356,18 @@ class PushTokenRestController extends RestApiControllerBase {
'context' => array( 'view' ),
'readonly' => true,
),
+ 'user_login' => array(
+ 'description' => __( 'The username of the account the token belongs to. Null when the user no longer exists.', 'woocommerce' ),
+ 'type' => array( 'string', 'null' ),
+ 'context' => array( 'view' ),
+ 'readonly' => true,
+ ),
+ 'user_email' => array(
+ 'description' => __( 'The email address of the account the token belongs to. Null when the user no longer exists.', 'woocommerce' ),
+ 'type' => array( 'string', 'null' ),
+ 'context' => array( 'view' ),
+ 'readonly' => true,
+ ),
'token' => array(
'description' => __( 'The push token issued by Apple or Google.', 'woocommerce' ),
'type' => 'string',
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
index 286f9a2d676..26d38d82593 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
@@ -1226,6 +1226,8 @@ class PushTokenRestControllerTest extends WC_Unit_Test_Case {
array(
'id',
'user_id',
+ 'user_login',
+ 'user_email',
'token',
'platform',
'origin',
@@ -1502,6 +1504,82 @@ class PushTokenRestControllerTest extends WC_Unit_Test_Case {
$this->assertNotEmpty( $response->get_headers()['X-WP-TotalPages'] );
}
+ /**
+ * @testdox Should return the username and email of the account each token belongs to.
+ */
+ public function test_index_returns_account_fields_for_each_token(): void {
+ $this->mock_jetpack_connection_manager_is_connected();
+ wc_get_container()->get( PushNotifications::class )->on_init();
+
+ wc_get_container()->get( PushTokensDataStore::class )->create(
+ array(
+ 'user_id' => $this->user_id,
+ 'token' => 'account-fields-token',
+ 'platform' => PushToken::PLATFORM_APPLE,
+ 'device_uuid' => 'account-fields-uuid',
+ 'origin' => PushToken::ORIGIN_WOOCOMMERCE_IOS,
+ 'device_locale' => 'en_US',
+ )
+ );
+
+ $controller = new PushTokenRestController();
+ $request = new WP_REST_Request( 'GET', '/wc-push-notifications/push-tokens' );
+ $request->set_param( 'page', 1 );
+ $request->set_param( 'per_page', 100 );
+ $response = $controller->index( $request );
+
+ $user = get_userdata( $this->user_id );
+ $token_data = $response->get_data()['tokens'][0];
+
+ $this->assertSame( $this->user_id, $token_data['user_id'] );
+ $this->assertSame( $user->user_login, $token_data['user_login'] );
+ $this->assertSame( $user->user_email, $token_data['user_email'] );
+ }
+
+ /**
+ * @testdox Should return null account fields when the token's user no longer exists.
+ */
+ public function test_index_returns_null_account_fields_when_user_no_longer_exists(): void {
+ $missing_user_id = 999999;
+
+ $token = new PushToken(
+ array(
+ 'id' => 1,
+ 'user_id' => $missing_user_id,
+ 'token' => 'orphaned-token',
+ 'platform' => PushToken::PLATFORM_APPLE,
+ 'device_uuid' => 'orphaned-uuid',
+ 'origin' => PushToken::ORIGIN_WOOCOMMERCE_IOS,
+ 'device_locale' => 'en_US',
+ )
+ );
+
+ $data_store = $this->createMock( PushTokensDataStore::class );
+ $data_store
+ ->method( 'get_tokens_for_roles' )
+ ->willReturn(
+ array(
+ 'tokens' => array( $token ),
+ 'total' => 1,
+ 'total_pages' => 1,
+ )
+ );
+
+ wc_get_container()->replace( PushTokensDataStore::class, $data_store );
+
+ $controller = new PushTokenRestController();
+ $request = new WP_REST_Request( 'GET', '/wc-push-notifications/push-tokens' );
+ $request->set_param( 'page', 1 );
+ $request->set_param( 'per_page', 100 );
+ $response = $controller->index( $request );
+
+ $token_data = $response->get_data()['tokens'][0];
+
+ $this->assertSame( $missing_user_id, $token_data['user_id'] );
+ $this->assertNull( $token_data['user_login'] );
+ $this->assertNull( $token_data['user_email'] );
+ }
+
/**
* @testdox Should publish a schema on the index route describing every returned field.
*
@@ -1525,6 +1603,8 @@ class PushTokenRestControllerTest extends WC_Unit_Test_Case {
array(
'id',
'user_id',
+ 'user_login',
+ 'user_email',
'token',
'platform',
'origin',