`;
}
+ // Game history shows the name each player actually used *in that game* (their
+ // in-game/session name), not their current account name — the record should
+ // reflect who they were at match time. `verified` is recorded per session at
+ // ingest (server-validated at join), so the blue check reflects whether they
+ // actually played under their verified account name in that specific game.
+ private renderClanPlayerName(
+ p: ClanGame["clanPlayers"][number],
+ ): TemplateResult {
+ return html`
+
+
+ ${p.verified === true ? verifiedBadge() : nothing}
+
+ `;
+ }
+
// Ranked games cap clan participation at a single player, so
// "1 / N total" is noise — just show the total. FFA can carry
// multiple clan members (renderResultBadge already handles partial
diff --git a/src/core/ClanApiSchemas.ts b/src/core/ClanApiSchemas.ts
index be1d37780..66343c1b1 100644
--- a/src/core/ClanApiSchemas.ts
+++ b/src/core/ClanApiSchemas.ts
@@ -188,7 +188,13 @@ export type JoinClanResponse = z.infer;
export const ClanGamePlayerSchema = z.object({
publicId: z.string(),
+ // The name the player actually used in that game (the in-lobby name). This
+ // is what the history displays.
username: z.string(),
+ // Whether the player joined that game under their verified account name
+ // (recorded per session at ingest, server-validated at join). Drives the
+ // verified check. Optional so older API responses still parse (→ no badge).
+ verified: z.boolean().optional(),
won: z.boolean(),
});
export type ClanGamePlayer = z.infer;
diff --git a/tests/client/PlayerProfileModal.test.ts b/tests/client/PlayerProfileModal.test.ts
index fd4e9d1f1..b6c3685c8 100644
--- a/tests/client/PlayerProfileModal.test.ts
+++ b/tests/client/PlayerProfileModal.test.ts
@@ -159,18 +159,20 @@ describe("public player profile route", () => {
expect(fetchPublicPlayerProfileMock).not.toHaveBeenCalled();
});
- it("returns to the clan members tab when opened from a clan", async () => {
+ it("hands back to the clan modal when opened from a clan", async () => {
fetchPublicPlayerProfileMock.mockResolvedValue({
createdAt: "2026-01-01T00:00:00.000Z",
stats: statsTree,
});
- const returnToMembers = vi.fn();
+ // The clan modal decides which tab to restore (Members vs Game History)
+ // via returnFromPlayerProfile — the profile modal just hands back to it.
+ const returnFromPlayerProfile = vi.fn();
const fakeClanModal = document.createElement(
"clan-modal",
) as HTMLElement & {
- returnToMembers: () => void;
+ returnFromPlayerProfile: () => void;
};
- fakeClanModal.returnToMembers = returnToMembers;
+ fakeClanModal.returnFromPlayerProfile = returnFromPlayerProfile;
document.body.appendChild(fakeClanModal);
modal.openFromClan("clan-member");
@@ -183,7 +185,7 @@ describe("public player profile route", () => {
backButton.click();
expect(modal.isOpen()).toBe(false);
- expect(returnToMembers).toHaveBeenCalled();
+ expect(returnFromPlayerProfile).toHaveBeenCalled();
fakeClanModal.remove();
});
});
diff --git a/tests/client/clan/ClanApiSchemas.test.ts b/tests/client/clan/ClanApiSchemas.test.ts
index 654d4be85..e14e80543 100644
--- a/tests/client/clan/ClanApiSchemas.test.ts
+++ b/tests/client/clan/ClanApiSchemas.test.ts
@@ -312,6 +312,26 @@ describe("ClanGamePlayerSchema", () => {
expect(ClanGamePlayerSchema.safeParse(validPlayer).success).toBe(true);
});
+ it("accepts the verified flag or its absence (older API)", () => {
+ const verified = ClanGamePlayerSchema.safeParse({
+ ...validPlayer,
+ verified: true,
+ });
+ expect(verified.success).toBe(true);
+ if (verified.success) expect(verified.data.verified).toBe(true);
+ // validPlayer omits verified entirely — still valid (→ undefined).
+ const bare = ClanGamePlayerSchema.safeParse(validPlayer);
+ expect(bare.success).toBe(true);
+ if (bare.success) expect(bare.data.verified).toBeUndefined();
+ });
+
+ it("rejects a non-boolean verified", () => {
+ expect(
+ ClanGamePlayerSchema.safeParse({ ...validPlayer, verified: "yes" })
+ .success,
+ ).toBe(false);
+ });
+
it("rejects when won is not a boolean", () => {
expect(
ClanGamePlayerSchema.safeParse({ ...validPlayer, won: "true" }).success,