Skip to content

Commit 16e2206

Browse files
ryanbarlow97claude
andcommitted
Put Minecraft linking on its own row in Linked accounts
Sign in with Microsoft and "Use a code" sit on the right of the Minecraft row, and the code form opens beneath it. The separate linking section, its explanations and the Profile footnote are gone. The supporter link's arrow is drawn, as the ↗ character turns into an emoji on some systems. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e150168 commit 16e2206

2 files changed

Lines changed: 84 additions & 110 deletions

File tree

‎frontend/app/components/account/AccountPanel.test.tsx‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ it("previews then links a code, naming both accounts", async () => {
181181
vi.mocked(previewMinecraftLink).mockResolvedValue({ player_uuid: "u", minecraft_name: "SteveMC", expires_at: "z" });
182182
vi.mocked(linkMinecraft).mockResolvedValue(null);
183183
render(<AccountPanel signin={null} />);
184-
fireEvent.change(await screen.findByLabelText("Link code"), { target: { value: "abcd1234ef56" } });
184+
fireEvent.click(await screen.findByRole("button", { name: "Link with a code from in game" }));
185+
fireEvent.change(screen.getByLabelText("Link code"), { target: { value: "abcd1234ef56" } });
185186
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
186187
expect((await screen.findByText(/Link Minecraft account/)).textContent).toBe(
187188
"Link Minecraft account SteveMC to Discord @steve_tfmc?"
@@ -196,7 +197,8 @@ it("asks for a fresh Discord check when the guild check is stale", async () => {
196197
vi.mocked(previewMinecraftLink).mockResolvedValue({ player_uuid: "u", minecraft_name: "SteveMC", expires_at: "z" });
197198
vi.mocked(linkMinecraft).mockRejectedValue(new AccountApiError("guild_check_stale", 403));
198199
render(<AccountPanel signin={null} />);
199-
fireEvent.change(await screen.findByLabelText("Link code"), { target: { value: "ABCD-1234-EF56" } });
200+
fireEvent.click(await screen.findByRole("button", { name: "Link with a code from in game" }));
201+
fireEvent.change(screen.getByLabelText("Link code"), { target: { value: "ABCD-1234-EF56" } });
200202
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
201203
fireEvent.click(await screen.findByRole("button", { name: "Link account" }));
202204
expect((await screen.findByRole("link", { name: "Confirm with Discord" })).getAttribute("href")).toContain(
@@ -208,16 +210,19 @@ it("shows code errors from the server", async () => {
208210
vi.mocked(getAccount).mockResolvedValue(account());
209211
vi.mocked(previewMinecraftLink).mockRejectedValue(new AccountApiError("Link code has expired", 400));
210212
render(<AccountPanel signin={null} />);
211-
fireEvent.change(await screen.findByLabelText("Link code"), { target: { value: "ABCD-1234-EF56" } });
213+
fireEvent.click(await screen.findByRole("button", { name: "Link with a code from in game" }));
214+
fireEvent.change(screen.getByLabelText("Link code"), { target: { value: "ABCD-1234-EF56" } });
212215
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
213216
expect((await screen.findByRole("alert")).textContent).toBe("Link code has expired");
214217
});
215218

216219
it("asks non-members to join the Discord first", async () => {
217220
vi.mocked(getAccount).mockResolvedValue(account({ guild: { member: false, checked_at: null, fresh: false } }));
218221
render(<AccountPanel signin={null} />);
219-
expect(await screen.findByRole("link", { name: "Join it" })).toBeTruthy();
220-
expect(screen.queryByLabelText("Link code")).toBeNull();
222+
const row = await screen.findByLabelText("Minecraft account");
223+
expect(within(row).getByRole("link", { name: "TFMC Discord" }).getAttribute("href")).toBe("https://discord.gg/tfmc");
224+
expect(within(row).getByRole("link", { name: "I’ve joined, check again" })).toBeTruthy();
225+
expect(within(row).queryByRole("button", { name: /code/ })).toBeNull();
221226
});
222227

223228
it("offers Patreon when enabled and unlinked", async () => {
@@ -319,9 +324,11 @@ it("offers Microsoft first, with the in-game code as a fallback", async () => {
319324
const assign = vi.fn();
320325
vi.stubGlobal("location", { ...window.location, assign });
321326
render(<AccountPanel signin={null} />);
322-
const section = await screen.findByRole("region", { name: "Link your Minecraft account" });
323-
expect(within(section).getByText("Or use a code from in game")).toBeTruthy();
324-
fireEvent.click(within(section).getByRole("button", { name: "Sign in with Microsoft" }));
327+
const row = await screen.findByLabelText("Minecraft account");
328+
expect(within(row).queryByLabelText("Link code")).toBeNull();
329+
fireEvent.click(within(row).getByRole("button", { name: "Use a code" }));
330+
expect(within(row).getByLabelText("Link code")).toBeTruthy();
331+
fireEvent.click(within(row).getByRole("button", { name: "Sign in with Microsoft" }));
325332
await vi.waitFor(() => expect(assign).toHaveBeenCalledWith(
326333
"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?x=1"
327334
));
@@ -331,7 +338,8 @@ it("offers Microsoft first, with the in-game code as a fallback", async () => {
331338
it("keeps only the code form when Microsoft is not set up", async () => {
332339
vi.mocked(getAccount).mockResolvedValue(account({ microsoft_link: false }));
333340
render(<AccountPanel signin={null} />);
334-
expect(await screen.findByLabelText("Link code")).toBeTruthy();
341+
fireEvent.click(await screen.findByRole("button", { name: "Link with a code from in game" }));
342+
expect(screen.getByLabelText("Link code")).toBeTruthy();
335343
expect(screen.queryByRole("button", { name: "Sign in with Microsoft" })).toBeNull();
336344
});
337345

‎frontend/app/components/account/AccountPanel.tsx‎

Lines changed: 67 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export default function AccountPanel({
129129
const [confirmPatreon, setConfirmPatreon] = useState(false);
130130
const [actionError, setActionError] = useState<string | null>(null);
131131
const [recheck, setRecheck] = useState(minecraftStatus === "guild_check_stale");
132+
const [showCode, setShowCode] = useState(false);
132133
const notice = signInMessage(signin) || minecraftLinkMessage(minecraftStatus);
133134

134135
const refresh = useCallback(async () => {
@@ -339,27 +340,7 @@ export default function AccountPanel({
339340
</p>
340341
) : null}
341342

342-
{minecraft ? (
343-
dashboard ? (
344-
<ProfileTiles dashboard={dashboard} />
345-
) : null
346-
) : (
347-
<section className="mt-8" aria-labelledby="link-heading">
348-
<h2 id="link-heading" className={`${sectionHeadingClass} mb-3`}>
349-
Link your Minecraft account
350-
</h2>
351-
<div className={`${cardClass} p-5`}>
352-
<LinkMinecraft
353-
account={account}
354-
recheck={recheck}
355-
busy={busy}
356-
discordHandle={discordHandle}
357-
onConnectMicrosoft={() => void onConnectMicrosoft()}
358-
onLinked={refresh}
359-
/>
360-
</div>
361-
</section>
362-
)}
343+
{minecraft && dashboard ? <ProfileTiles dashboard={dashboard} /> : null}
363344

364345
<section className="mt-8" aria-labelledby="accounts-heading">
365346
<h2 id="accounts-heading" className={`${sectionHeadingClass} mb-3`}>
@@ -393,10 +374,26 @@ export default function AccountPanel({
393374
service="Minecraft"
394375
icon={minecraft ? <PlayerHead uuid={minecraft.player_uuid} fallback={null} size={32} /> : <EmptyIcon />}
395376
action={
396-
minecraft && !confirmUnlink ? (
397-
<button type="button" onClick={() => setConfirmUnlink(true)} className={quietButtonClass}>
398-
Unlink
399-
</button>
377+
minecraft ? (
378+
confirmUnlink ? null : (
379+
<button type="button" onClick={() => setConfirmUnlink(true)} className={quietButtonClass}>
380+
Unlink
381+
</button>
382+
)
383+
) : (
384+
<LinkActions
385+
account={account}
386+
recheck={recheck}
387+
busy={busy}
388+
showCode={showCode}
389+
onConnectMicrosoft={() => void onConnectMicrosoft()}
390+
onToggleCode={() => setShowCode((open) => !open)}
391+
/>
392+
)
393+
}
394+
below={
395+
!minecraft && showCode && account.guild.member && !recheck ? (
396+
<MinecraftLinkForm discordName={discordHandle} onLinked={refresh} />
400397
) : null
401398
}
402399
>
@@ -419,7 +416,20 @@ export default function AccountPanel({
419416
) : null}
420417
</>
421418
) : (
422-
<p className="text-[var(--tfmc-stone)]">Not linked</p>
419+
<>
420+
<p className="text-[var(--tfmc-stone)]">Not linked</p>
421+
{!account.guild.member ? (
422+
<p className="text-sm text-[var(--tfmc-stone)]">
423+
Join the{" "}
424+
<a href={SITE_DISCORD_URL} className={accentLinkClass}>
425+
TFMC Discord
426+
</a>{" "}
427+
to link.
428+
</p>
429+
) : recheck ? (
430+
<p className="text-sm text-[var(--tfmc-stone)]">Confirm your Discord membership to link.</p>
431+
) : null}
432+
</>
423433
)}
424434
</AccountRow>
425435

@@ -437,23 +447,6 @@ export default function AccountPanel({
437447
</ul>
438448
</section>
439449

440-
{minecraft ? (
441-
<p className="mt-8 text-sm text-[var(--tfmc-stone)]">
442-
Your{" "}
443-
<Link href="/profile" className={accentLinkClass}>
444-
Profile
445-
</Link>{" "}
446-
opens straight away now your accounts are linked, with no code from in game.
447-
</p>
448-
) : (
449-
<p className="mt-8 text-sm text-[var(--tfmc-stone)]">
450-
Once linked, your{" "}
451-
<Link href="/profile" className={accentLinkClass}>
452-
Profile
453-
</Link>{" "}
454-
opens without a code from in game.
455-
</p>
456-
)}
457450
</>
458451
);
459452
}
@@ -499,12 +492,15 @@ function AccountRow({
499492
service,
500493
icon,
501494
action,
495+
below,
502496
children,
503497
}: {
504498
label: string;
505499
service: string;
506500
icon: ReactNode;
507501
action?: ReactNode;
502+
/** Full-width content under the row, such as a form. */
503+
below?: ReactNode;
508504
children: ReactNode;
509505
}) {
510506
return (
@@ -518,6 +514,7 @@ function AccountRow({
518514
{children}
519515
</div>
520516
{action ? <div className="ml-auto flex shrink-0 flex-wrap items-center justify-end gap-3">{action}</div> : null}
517+
{below ? <div className="basis-full sm:pl-12">{below}</div> : null}
521518
</li>
522519
);
523520
}
@@ -551,7 +548,6 @@ function PatreonRow({
551548
<PatreonConnectButton onClick={onConnect} disabled={busy} />
552549
}>
553550
<p className="text-[var(--tfmc-stone)]">Not connected</p>
554-
<p className="text-sm text-[var(--tfmc-stone)]">Supporter perks on Discord and in game.</p>
555551
</AccountRow>
556552
);
557553
}
@@ -566,7 +562,7 @@ function PatreonRow({
566562
<>
567563
{!tier ? (
568564
<a href={PATREON_URL} target="_blank" rel="noopener noreferrer" className={`text-sm ${accentLinkClass}`}>
569-
Become a supporter ↗
565+
Become a supporter <ExternalArrow />
570566
</a>
571567
) : null}
572568
<button type="button" onClick={onBegin} className={quietButtonClass}>
@@ -603,82 +599,52 @@ function PatreonRow({
603599
);
604600
}
605601

606-
function LinkMinecraft({
602+
function LinkActions({
607603
account,
608604
recheck,
609605
busy,
610-
discordHandle,
606+
showCode,
611607
onConnectMicrosoft,
612-
onLinked,
608+
onToggleCode,
613609
}: {
614610
account: Account;
615611
recheck: boolean;
616612
busy: boolean;
617-
discordHandle: string;
613+
showCode: boolean;
618614
onConnectMicrosoft: () => void;
619-
onLinked: () => void | Promise<void>;
615+
onToggleCode: () => void;
620616
}) {
621-
const intro = (
622-
<p className="text-sm text-[var(--tfmc-mist)]">
623-
Linking opens your characters, skins and drinks here, and brings your Patreon perks into the game.
624-
</p>
625-
);
626617
if (!account.guild.member) {
627618
return (
628-
<>
629-
{intro}
630-
<p className="mt-3 text-sm text-[var(--tfmc-mist)]">
631-
Linking needs you to be in the TFMC Discord server.{" "}
632-
<a href={SITE_DISCORD_URL} className={accentLinkClass}>
633-
Join it
634-
</a>
635-
, then check again.
636-
</p>
637-
<a href={discordSignInUrl("/account")} className={`${buttonClass} mt-4`}>
638-
I’ve joined, check again
639-
</a>
640-
</>
641-
);
642-
}
643-
if (recheck) {
644-
return (
645-
<>
646-
<p className="text-sm text-[var(--tfmc-mist)]">Please confirm your Discord membership again before linking.</p>
647-
<div className="mt-4 flex">
648-
<DiscordSignInLink href={discordSignInUrl("/account")} />
649-
</div>
650-
</>
651-
);
652-
}
653-
if (!account.microsoft_link) {
654-
return (
655-
<>
656-
{intro}
657-
<div className="mt-4">
658-
<MinecraftLinkForm discordName={discordHandle} onLinked={onLinked} />
659-
</div>
660-
</>
619+
<a href={discordSignInUrl("/account")} className={quietButtonClass}>
620+
I’ve joined, check again
621+
</a>
661622
);
662623
}
624+
if (recheck) return <DiscordSignInLink href={discordSignInUrl("/account")} />;
625+
const codeToggle = (
626+
<button type="button" onClick={onToggleCode} aria-expanded={showCode} className={quietButtonClass}>
627+
{account.microsoft_link ? "Use a code" : "Link with a code from in game"}
628+
</button>
629+
);
630+
if (!account.microsoft_link) return codeToggle;
663631
return (
664632
<>
665-
{intro}
666-
<p className="mt-2 text-sm text-[var(--tfmc-mist)]">
667-
Sign in with the Microsoft account you play Minecraft with. We only read your Minecraft name and ID.
668-
</p>
669-
<div className="mt-4 flex">
670-
<MicrosoftSignInButton onClick={onConnectMicrosoft} disabled={busy} />
671-
</div>
672-
<details className="mt-5">
673-
<summary className={`${quietButtonClass} cursor-pointer`}>Or use a code from in game</summary>
674-
<div className="mt-3">
675-
<MinecraftLinkForm discordName={discordHandle} onLinked={onLinked} />
676-
</div>
677-
</details>
633+
{codeToggle}
634+
<MicrosoftSignInButton onClick={onConnectMicrosoft} disabled={busy} />
678635
</>
679636
);
680637
}
681638

639+
/** A text arrow: the ↗ character turns into an emoji on some systems. */
640+
function ExternalArrow() {
641+
return (
642+
<svg aria-hidden viewBox="0 0 12 12" width={10} height={10} className="ml-0.5 inline-block align-baseline">
643+
<path d="M3.5 2.5h6v6M9.5 2.5l-7 7" fill="none" stroke="currentColor" strokeWidth={1.5} />
644+
</svg>
645+
);
646+
}
647+
682648
function waiting(count: number): string | null {
683649
return count ? `${count} waiting` : null;
684650
}

0 commit comments

Comments
 (0)