Skip to content

Commit 525b271

Browse files
ryanbarlow97claude
andcommitted
Offer codes to unlinked Discord accounts and reserve the skin id "new"
A Discord sign-in without a Minecraft link had no code form, so the old /skins and /drinks links left those players stuck. Profile now shows "Enter code" for them. A staff skin named "New" would get the id "new" and be hidden behind /profile/skins/new; no such skin exists on either site, and the id is now reserved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent aa7fc7c commit 525b271

4 files changed

Lines changed: 71 additions & 6 deletions

File tree

‎backend/src/skins/naming.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
"undefined",
1919
"admin",
2020
"tfmc",
21+
# Profile's editor is /profile/skins/new, so no skin can be called that.
22+
"new",
2123
}
2224
)
2325

‎backend/src/skins/test_naming.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Skin ids that would clash with the site's own addresses."""
2+
3+
import pytest
4+
5+
from src.skins.naming import SlugError, build_staff_submission_id, build_submission_id
6+
7+
8+
def test_a_staff_skin_cannot_take_the_editor_address():
9+
with pytest.raises(SlugError):
10+
build_staff_submission_id("New")
11+
assert build_staff_submission_id("New", "dev") == "dev_new"
12+
assert build_staff_submission_id("New Moon") == "new_moon"
13+
14+
15+
def test_player_skins_called_new_keep_their_name_prefix():
16+
assert build_submission_id("Steve", "New") == "steve_new"

‎frontend/app/components/profile/ProfilePanel.test.tsx‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,3 +908,21 @@ it("opens the code form for old skin and drink links", async () => {
908908
expect(form.hidden).toBe(false);
909909
expect(form.textContent).toContain("/token create profile, skin or drink");
910910
});
911+
912+
it("lets a Discord account without a Minecraft link open a wardrobe with a code", async () => {
913+
vi.mocked(getAccount).mockResolvedValue(account());
914+
vi.mocked(signInWithCode).mockImplementation(async () => {
915+
setSkinsSession(SKIN_CODE);
916+
return { tab: "skins", profile: null };
917+
});
918+
vi.mocked(getCodeWardrobe).mockResolvedValue({ items: [SKIN], can_start: READY });
919+
render(<ProfilePanel tab="skins" />);
920+
const toggle = await screen.findByRole("button", { name: "Enter code" });
921+
// Old /skins links land here with the form open.
922+
expect(toggle.getAttribute("aria-expanded")).toBe("true");
923+
fireEvent.change(screen.getByLabelText("Log in with code"), { target: { value: "SKIN-CODE" } });
924+
fireEvent.click(screen.getByRole("button", { name: "Enter" }));
925+
expect(await screen.findByRole("link", { name: /Emberfang/ })).toBeTruthy();
926+
expect(screen.getAllByRole("tab").map((tab) => tab.textContent)).toEqual(["Skins", "Linked accounts"]);
927+
expect(screen.queryByRole("button", { name: "Enter code" })).toBeNull();
928+
});

‎frontend/app/components/profile/ProfilePanel.tsx‎

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,17 @@ export default function ProfilePanel({
390390

391391
if (holding) return <ProfilePlaceholder shape={shape} tab={initialTab} />;
392392

393+
const codeForm = (
394+
<>
395+
<p className="text-sm text-[var(--tfmc-mist)]">
396+
Run <code className="text-[var(--tfmc-accent)]">/token create profile</code>,{" "}
397+
<code className="text-[var(--tfmc-accent)]">skin</code> or{" "}
398+
<code className="text-[var(--tfmc-accent)]">drink</code> in game, then enter the code.
399+
</p>
400+
<ProfileRedeemForm onRedeemed={onRedeemed} />
401+
</>
402+
);
403+
393404
if (!account && !session && !codeTabs.length) {
394405
// Without Discord sign-in the code is the only way in, so it shows open.
395406
const codeOpen = showCode || load.kind !== "signed_out";
@@ -429,12 +440,7 @@ export default function ProfilePanel({
429440
</>
430441
) : null}
431442
<div id={codeId} hidden={!codeOpen} className="mt-8 max-w-md">
432-
<p className="text-sm text-[var(--tfmc-mist)]">
433-
Run <code className="text-[var(--tfmc-accent)]">/token create profile</code>,{" "}
434-
<code className="text-[var(--tfmc-accent)]">skin</code> or{" "}
435-
<code className="text-[var(--tfmc-accent)]">drink</code> in game, then enter the code.
436-
</p>
437-
<ProfileRedeemForm onRedeemed={onRedeemed} />
443+
{codeForm}
438444
</div>
439445
</>
440446
);
@@ -511,6 +517,29 @@ export default function ProfilePanel({
511517
</p>
512518
) : null}
513519

520+
{/* Signed in without a Minecraft link: a code still opens Profile or a wardrobe. */}
521+
{!contentTabs && !codeTabs.length ? (
522+
<>
523+
<p className="mt-3 flex flex-wrap items-center justify-between gap-3 text-sm text-[var(--tfmc-mist)]">
524+
<span>
525+
Have a <code className="text-[var(--tfmc-accent)]">/token create</code> code?
526+
</span>
527+
<button
528+
type="button"
529+
onClick={() => setShowCode((open) => !open)}
530+
aria-expanded={showCode}
531+
aria-controls={codeId}
532+
className={quietButtonClass}
533+
>
534+
Enter code
535+
</button>
536+
</p>
537+
<div id={codeId} hidden={!showCode} className="mt-4 max-w-md">
538+
{codeForm}
539+
</div>
540+
</>
541+
) : null}
542+
514543
{actionError || notice ? (
515544
<p className="mt-6 text-sm text-[#e8a0a0]" role="alert">
516545
{actionError || notice}

0 commit comments

Comments
 (0)