From dfe50718782717b31dc91a49bf48497f9f9e2c74 Mon Sep 17 00:00:00 2001 From: Devin Zuczek Date: Mon, 12 Oct 2026 16:30:40 -0400 Subject: [PATCH] [www] fix role sync not grabbing all roles --- apps/www/README.md | 14 ++++--- apps/www/src/client/App.tsx | 46 ++++++++++++++++++----- apps/www/src/discord.ts | 6 ++- apps/www/src/www.app.ts | 73 +++++++++++++++++++++++++------------ 4 files changed, 99 insertions(+), 40 deletions(-) diff --git a/apps/www/README.md b/apps/www/README.md index 4f4a2a1..6d6a2f9 100644 --- a/apps/www/README.md +++ b/apps/www/README.md @@ -87,9 +87,13 @@ The tests seed the same pair into their own local store in `beforeAll`. ### Benefits claim and Discord -The **Claim benefits** tab on the account page lets a player prove they hold one of -the qualifying roles in the community Discord and, if they do, gives their account Rec -Room Plus (`account.hasPlus`). The same panel also renders at `/claim`, which is the +The **Claim benefits** tab on the account page lets a player link their Discord to +their account. Any member of the community Discord can link: the roles they hold there +are recorded on the link (`platform_account.role`, which the game reads — `auth`'s +rollout gate, for one — and `www`'s daily sweep keeps fresh), and if one of them is a +qualifying role the claim also gives the account Rec Room Plus (`account.hasPlus`). A +member with no qualifying role is still linked and answered `hasPlus: false`, not +refused. The same panel also renders at `/claim`, which is the app's registered `redirect_uri` — Discord sends the browser back there mid-flow, so that route has to keep working on a cold load even though nothing links to it. It runs a standard OAuth2 **authorization code** flow: @@ -102,8 +106,8 @@ OAuth2 **authorization code** flow: `POST /api/benefits/claim` with the player's bearer token. 3. The worker swaps the code for an access token with the client secret, reads `GET /users/@me/guilds/{guild}/member` to get the player's roles, revokes the - token, and — if any one of the configured roles is there — writes `hasPlus` onto - the account and links the Discord id. + token, links the Discord id with the full role list, and — if any one of the + configured roles is there — writes `hasPlus` onto the account. **A claim takes effect on the player's next sign-in, not immediately.** `auth` stamps `hasPlus` into every token it mints as the `rn.plus` claim, and `econ` decides the diff --git a/apps/www/src/client/App.tsx b/apps/www/src/client/App.tsx index 76039bc..55f1ea8 100644 --- a/apps/www/src/client/App.tsx +++ b/apps/www/src/client/App.tsx @@ -545,6 +545,12 @@ const fetchBenefitsStatus = (): Promise => /** What a successful claim answers. */ interface ClaimResult { + /** + * Where the account stands after this claim. The Discord is linked and its roles saved + * either way; this is false when none of them is a qualifying role (and the account had + * no Plus from before). + */ + hasPlus: boolean discordUsername?: string /** * The supporter gift paid on this claim — the tokens the server hands a linked role once, @@ -755,17 +761,23 @@ function BenefitsPanel({ account, config }: { account: SelfAccount; config: Site setPending(true) claimBenefits(code) .then((result) => { - setStatus({ hasPlus: true, linked: true }) - const verified = result.discordUsername - ? `Verified as ${result.discordUsername} — Rec Room Plus is now on your account.` - : 'Verified — Rec Room Plus is now on your account.' + setStatus({ hasPlus: result.hasPlus, linked: true }) + const who = result.discordUsername ? ` as ${result.discordUsername}` : '' + // Linked either way; whether Plus came with it depends on the roles. Without a + // qualifying role the roles are still saved for the game, and that is said + // plainly rather than reading as a failure. + const verified = result.hasPlus + ? `Verified${who} — Rec Room Plus is now on your account.` + : `Linked${who} — your Discord roles are saved to your account, but none of them currently grants Rec Room Plus.` // The first-link gift, when one was paid: it's sitting in a box in the game, and // the player would otherwise only find it by opening their gifts. const gift = result.tokensAwarded ? ` A welcome gift of ${result.tokensAwarded.toLocaleString()} tokens is waiting in your gift boxes.` : '' setDone(verified + gift) - setRelogin(true) + // The re-login hint is about Plus riding on the game token; without Plus there + // is nothing a sign-in would pick up. + setRelogin(result.hasPlus) }) .catch((err: unknown) => setError(err instanceof Error ? err.message : String(err))) .finally(() => setPending(false)) @@ -783,14 +795,15 @@ function BenefitsPanel({ account, config }: { account: SelfAccount; config: Site } const claimed = status?.hasPlus === true + const linked = status?.linked === true return (
-

Rec Room Plus

+

Link your Discord

- Members of our Discord with a supporter role get Rec Room Plus on their account. Verify with - Discord and we’ll check your roles — we only ever read your username and which roles you - hold in our server. + Link your Discord to carry the roles you hold in our server into the game. Members with a + supporter role also get Rec Room Plus on their account. We only ever read your username and + which roles you hold in our server.

Claiming as @{account.username} (#{account.accountId}). A Discord account @@ -826,6 +839,21 @@ function BenefitsPanel({ account, config }: { account: SelfAccount; config: Site Re-verify with Discord + ) : linked ? ( + // Linked, no Plus: a member without a qualifying role, or one who lost it. Their + // roles are on the account; re-verifying refreshes them (and grants Plus if a + // qualifying role has since been added). + <> + {!done && ( +

+ Your Discord is linked and its roles are saved to this account. None of them currently + grants Rec Room Plus. +

+ )} + + ) : (