feat(client): account usernames + verified badge on player-facing lists (#4653)

## Summary

Client side of openfrontio/infra#436 — every player-facing GET now
returns the player's **account username** next to their `publicId`, and
this PR renders it everywhere, with a blue verified check for
premium/indefinite bare-name holders.

### Schemas (all additive, `.nullable().optional()` — no deploy-order
constraint)
- `FriendEntry`, `PlayerProfile`: `username`
- `ClanMember`, `ClanJoinRequest`: `username`; `ClanBan`: `username` +
`bannedByUsername`
- `RankedLeaderboardEntry`: `accountUsername` (the existing session
`username` field is untouched)

### `<player-name>` element
One component owns player-identity rendering on account surfaces:
- displays `username ?? publicId`
- click-to-copy copies the **username when set**, the publicId
otherwise; `copyText` overrides (profile modal copies the share URL)
- `onNameClick` replaces copying with an action (clan member rows +
leaderboard rows open the player's profile); `nameClass` overrides the
chip styling so leaderboard names keep their original bold look
- appends the verified check (same mark as the username-row toggle) when
`isVerifiedUsername(username)`

Used by: friends list + requests, all clan member/request/ban rows (bans
badge both the banned player and the issuing officer), ranked
leaderboard rows + sticky self row (showing `accountUsername ??
public_id` — the session name is deliberately dropped ahead of its API
removal), profile modal header.

### Verified inference
No API flag needed: account-username bases can never contain dots, so a
dotless display name **is** the bare-name claim (premium/indefinite).
`TEMPORARY####` server renames are excluded, matching the play-toggle
eligibility rule. The leaderboard derives the badge from
`accountUsername` only — session names are free-form and would
false-positive.

Also: clan member/request/ban search filters now match usernames, not
just publicIds, and sending a friend request refetches the request lists
instead of inserting the raw input (which may be a username, not a
publicId).

## Test plan
- [x] `npm test` — full suite green (2087 + 176 server)
- [x] New schema tests: each field's set / null / absent states;
`accountUsername` coexists with session `username`
- [x] `isVerifiedUsername` unit tests (bare / suffixed / null /
TEMPORARY)
- [x] tsc, ESLint, Prettier clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Evan
2026-07-20 14:15:10 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent ad8d7e995a
commit 826d8b1bcf
16 changed files with 433 additions and 102 deletions
+3 -2
View File
@@ -40,7 +40,7 @@
"no_stats": "No stats available yet. Play some games to start tracking.",
"not_found": "Not Found",
"or": "OR",
"public_player_id": "Public Player ID:",
"public_player_id": "Public ID:",
"reactivate_subscription": "Reactivate",
"recovery_email_sent": "Recovery email sent to {email}",
"reward_daily": "Daily subscription reward",
@@ -544,7 +544,7 @@
"no_friends": "You haven't added any friends yet.",
"outgoing": "Outgoing",
"pending_requests": "Pending Requests",
"public_id_placeholder": "Enter their public player ID",
"public_id_placeholder": "Enter their username or player ID",
"remove": "Remove",
"request_accepted": "Friend request accepted",
"request_auto_accepted": "Friend request accepted — you are now friends",
@@ -1624,6 +1624,7 @@
"too_long": "Username must not exceed {max} characters.",
"too_short": "Username must be at least {min} characters long.",
"verified_heading": "Verified name",
"verified_player": "Verified player",
"verified_sub_required": "You need an active subscription to play under your verified name.",
"verified_sub_required_confirm": "View store",
"verified_toggle": "Verified"