From b6e6d4c9e8661bcb90b990cc80c2dbd46ece46ab Mon Sep 17 00:00:00 2001 From: kipavy Date: Sun, 16 Aug 2026 09:30:44 +0000 Subject: [PATCH] fix(portal): offer the handle control to any verified account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Custom handles are free now, so the Change-handle button no longer keys off the tier. It is shown to everyone and disabled — with the reason — for an unverified email, since hiding it is what made the tier gate unreadable. The claim modal maps the server's new 403 to "verify your email first" in place of the dead 402 branch. email_verified is taken from /v1/auth/me rather than the JWT claim: the token predates a verification the user may have completed since it was issued, and this value now decides whether the control is offered. Rendered in a browser at both states before committing — the row's first visual check. --- portal/app/account/ChangeHandleModal.tsx | 4 +-- portal/app/account/page.tsx | 34 ++++++++++++++---------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/portal/app/account/ChangeHandleModal.tsx b/portal/app/account/ChangeHandleModal.tsx index d149426..743b8fb 100644 --- a/portal/app/account/ChangeHandleModal.tsx +++ b/portal/app/account/ChangeHandleModal.tsx @@ -36,8 +36,8 @@ export default function ChangeHandleModal({ currentHandle, token, onClose, onSuc try { await updateHandle(trimmed, token); } catch (err) { - if (err instanceof ApiError && err.status === 402) { - setError("Custom handles require Pro or above."); + if (err instanceof ApiError && err.status === 403) { + setError("Verify your email first, then choose your handle."); } else if (err instanceof ApiError && err.status === 409) { setError("That handle is taken."); } else if (err instanceof ApiError && err.status === 422) { diff --git a/portal/app/account/page.tsx b/portal/app/account/page.tsx index 2908d6e..ff55510 100644 --- a/portal/app/account/page.tsx +++ b/portal/app/account/page.tsx @@ -194,6 +194,10 @@ export default function AccountPage() { setAccountId(me.account_id); setAccountEmail(me.email); setAccountHandle(me.handle); + // Authoritative over the JWT claim above: the token predates a + // verification the user may have completed since it was issued, and + // this value now decides whether the handle control is offered. + setEmailVerified(me.email_verified); } catch { /* non-critical */ } // Trial expired modal @@ -442,12 +446,13 @@ export default function AccountPage() {

Handle

{accountHandle ? `@${accountHandle}` : "—"}

- {displayTier === "free" && ( -
-

Custom handles are a Pro feature.

-

Others can still reach you at this handle.

-
- )} +
+

A custom handle makes you findable by people outside your teams.

+

Your current handle already works for anyone you give it to.

+ {emailVerified === false && ( +

Verify your email to choose a custom handle.

+ )} +
{accountHandle && ( @@ -458,14 +463,15 @@ export default function AccountPage() { {handleCopied ? "Copied" : "Copy"} )} - {displayTier != null && displayTier !== "free" && ( - - )} + {/* Disabled with the reason rather than hidden — hiding it is + what made the old tier gate unreadable. */} +
{/* Email + password row */}