mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-23 10:02:29 +00:00
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:
@@ -241,6 +241,7 @@ describe("LeaderboardModal", () => {
|
||||
total: 10,
|
||||
public_id: "player-1",
|
||||
username: "Alpha",
|
||||
accountUsername: "alpha.4821",
|
||||
clanTag: "[AAA]",
|
||||
},
|
||||
{
|
||||
@@ -252,6 +253,7 @@ describe("LeaderboardModal", () => {
|
||||
total: 10,
|
||||
public_id: "player-2",
|
||||
username: "Bravo",
|
||||
accountUsername: null,
|
||||
clanTag: null,
|
||||
},
|
||||
],
|
||||
@@ -269,7 +271,7 @@ describe("LeaderboardModal", () => {
|
||||
expect(playerData[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
playerId: "player-1",
|
||||
username: "Alpha",
|
||||
accountUsername: "alpha.4821",
|
||||
clanTag: "[AAA]",
|
||||
elo: 1200,
|
||||
games: 10,
|
||||
@@ -278,10 +280,12 @@ describe("LeaderboardModal", () => {
|
||||
winRate: 0.6,
|
||||
}),
|
||||
);
|
||||
// The session username ("Bravo") is deliberately ignored — display
|
||||
// falls back to the playerId when no account username is set.
|
||||
expect(playerData[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
playerId: "player-2",
|
||||
username: "Bravo",
|
||||
accountUsername: null,
|
||||
clanTag: undefined,
|
||||
winRate: 0.4,
|
||||
}),
|
||||
|
||||
@@ -153,6 +153,26 @@ describe("ClanMemberSchema", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts an account username, null, or absence (older API)", () => {
|
||||
const base = {
|
||||
role: "member",
|
||||
joinedAt: "2024-03-01T09:30:00.000Z",
|
||||
publicId: "abc123",
|
||||
};
|
||||
const named = ClanMemberSchema.safeParse({
|
||||
...base,
|
||||
username: "bob.4821",
|
||||
});
|
||||
expect(named.success).toBe(true);
|
||||
if (named.success) {
|
||||
expect(named.data.username).toBe("bob.4821");
|
||||
}
|
||||
expect(
|
||||
ClanMemberSchema.safeParse({ ...base, username: null }).success,
|
||||
).toBe(true);
|
||||
expect(ClanMemberSchema.safeParse(base).success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects stats missing a bucket", () => {
|
||||
const result = ClanMemberSchema.safeParse({
|
||||
role: "member",
|
||||
@@ -183,6 +203,21 @@ describe("ClanJoinRequestSchema", () => {
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts the requester's account username, null, or absence", () => {
|
||||
const base = {
|
||||
publicId: "player-xyz",
|
||||
createdAt: "2024-06-10T08:00:00.000Z",
|
||||
};
|
||||
expect(
|
||||
ClanJoinRequestSchema.safeParse({ ...base, username: "bob.4821" })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
ClanJoinRequestSchema.safeParse({ ...base, username: null }).success,
|
||||
).toBe(true);
|
||||
expect(ClanJoinRequestSchema.safeParse(base).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ClanBanSchema", () => {
|
||||
@@ -227,6 +262,23 @@ describe("ClanBanSchema", () => {
|
||||
const result = ClanBanSchema.safeParse({ ...validBan, bannedBy: null });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts account usernames for both the banned player and the officer", () => {
|
||||
const result = ClanBanSchema.safeParse({
|
||||
...validBan,
|
||||
username: null,
|
||||
bannedByUsername: "bigboss",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.username).toBeNull();
|
||||
expect(result.data.bannedByUsername).toBe("bigboss");
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts a ban without the username fields (older API)", () => {
|
||||
expect(ClanBanSchema.safeParse(validBan).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ClanGameResultSchema", () => {
|
||||
|
||||
Reference in New Issue
Block a user