Skip to content

Rewards: show level Plutonium held until the account is trusted - #5879

Open
iiamlewis wants to merge 5 commits into
mainfrom
held-level-plutonium
Open

iiamlewis wants to merge 5 commits into
mainfrom
held-level-plutonium

Conversation

@iiamlewis

@iiamlewis iiamlewis commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

What this is

Level Plutonium is now kept until a player's account is trusted (openfrontio/infra#933). This makes the game show that properly instead of offering a Claim button that would fail.

infra#933 must not deploy before this lands.

What changed

  • Rewards list: a held reward shows "Trusted accounts only" where the Claim button would be. One note under the list says how to become trusted, and it depends on the account:
    • Signed in: "Plutonium from levels is kept for you until your account is trusted. Your account becomes trusted once it is 14 days old and you've played 25 public games, or right away with any purchase."
    • Not signed in (no linked account, so playing or paying can never make it trusted): "Plutonium from levels is kept for you until your account is trusted. To become trusted, sign in to your account, then play more public games or make any purchase."
    • On CrazyGames the purchase part is left out of both, as there is no purchase route there.
    • Whether the player counts as signed in uses the same check as the trusted-lobby and ranked messages (responseHasLinkedIdentity, or a CrazyGames sign-in).
    • A hold of any other kind gets a neutral "Some rewards can't be claimed yet."
  • Fails closed: any held value the client doesn't recognise, or one that is malformed, still counts as held. Only an absent held is claimable. One rule, isRewardClaimable(), decides it everywhere.
  • Claim all: only shown when more than one reward can actually be claimed. Afterwards the list keeps the held rewards the API returns, parsed one by one so a bad row can't drop the rest.
  • Claiming a held reward: if the API answers 403 with held, the whole list is re-read (the hold is per account), with no "claim failed" alert.
  • Login rewards popup: counts only claimable rewards, doesn't open for held rewards alone, and closes once nothing claimable is left.

The end-of-game XP panel doesn't list rewards from /users/@me/xp/:gameId, so it needed no change.

Screenshots

All three use sample data: the rewards are injected into the local dev client; no account or API is involved.

Signed in, a held reward next to claimable ones (sample data):

Rewards list: a +25 Plutonium level milestone shows Trusted accounts only; two +50 level-up rewards have Claim buttons; the note names 14 days and 25 public games

Not signed in, the same rewards (sample data):

The same rewards list with the note asking the player to sign in first

Phone width, 390px, signed in (sample data):

The rewards list at phone width

Testing

  • Schema tests: held present, absent, null, unknown and malformed; claim-all held with good and bad rows.
  • RewardsPanel and RewardsModal tests: label and note per variant (signed in or not, CrazyGames or not, other holds); Claim all hidden when only held rewards remain; claim-all keeps the held ones; a held 403 re-reads the list without the failure alert; the popup closes when nothing claimable is left.
  • Api tests: a 403 without a usable held fails generically.
  • npm run typecheck and npm run lint pass. The client test suite passes (3,991 tests).

🤖 Generated with Claude Code

The API now holds level Plutonium rewards (level_milestone) until the
account is trusted, marking them `held: "trust"` on /users/@me's rewards.
The rewards panel shows a held reward with "Trusted accounts only" in
place of its Claim button, and a note under the list saying how the
account becomes trusted (CrazyGames gets a variant without purchases).

- Claim all is offered only for claimable rewards, and after it the panel
  keeps the rewards the server left held (`held` on the claim-all
  response) instead of clearing the list.
- A single claim answered 403 with `held` marks that reward held rather
  than showing the claim-failed alert.
- The login rewards popup counts only claimable rewards, so held ones
  alone don't reopen it every boot.
- Every new field is optional and reads a malformed value as absent, so
  older and newer APIs both parse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@iiamlewis
iiamlewis marked this pull request as ready for review October 9, 2026 14:38
@iiamlewis
iiamlewis requested a review from a team as a code owner October 9, 2026 14:38
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b941596b-57d5-4cc2-b818-ffb0710dee8c

📥 Commits

Reviewing files that changed from the base of the PR and between ad9bbcf and 2c3006e.


📒 Files selected for processing (8)
  • resources/lang/en.json
  • src/client/AccountModal.ts
  • src/client/Main.ts
  • src/client/RewardsModal.ts
  • src/client/components/RewardsPanel.ts
  • tests/client/AccountModalPrestige.test.ts
  • tests/client/RewardsModal.test.ts
  • tests/client/RewardsPanel.test.ts

🚧 Files skipped from review as they are similar to previous changes (1)
  • resources/lang/en.json

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 5 remain after this review.



Walkthrough

The change adds hold fields to reward schemas and recognizes held responses from reward claims. The rewards panel retains and displays held rewards. Boot interrupt counting and rewards modal visibility now use claimable rewards.

Changes

Held Reward Handling

Layer / File(s) Summary
Held reward contract and claim API
packages/shared/src/ApiSchemas.ts, src/client/Api.ts, tests/ApiSchemas.test.ts, tests/Api.test.ts, tests/client/ProgressionSchemas.test.ts
Reward schemas parse hold values and held rewards in claim-all responses. claimReward returns a held result for a valid 403 response. Tests cover parsing and claim outcomes.
Held reward panel flow
src/client/components/RewardsPanel.ts, resources/lang/en.json, tests/client/RewardsPanel.test.ts
The panel retains held rewards after individual and bulk claims, displays hold status and explanatory text, and shows claim-all only when multiple rewards are claimable. Tests cover rendering, claim events, and failures.
Claimable reward gating
src/client/Main.ts, src/client/BootInterrupts.ts, src/client/RewardsModal.ts, src/client/AccountModal.ts, tests/client/RewardsModal.test.ts, tests/client/AccountModalPrestige.test.ts
Boot interrupt counts now exclude held rewards. The modal opens and remains open only when at least one reward is claimable. Sign-in status is passed to the modal and rewards panel.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant RewardsPanel
  participant claimReward
  participant AccountRefresh
  RewardsPanel->>claimReward: Submit reward claim
  claimReward-->>RewardsPanel: Return held result
  RewardsPanel->>AccountRefresh: Refresh account data
  AccountRefresh-->>RewardsPanel: Return refreshed rewards and currency
  RewardsPanel->>RewardsPanel: Emit updated reward list
Loading

Suggested reviewers: evanpelle

Merge Risk: ⚪ Minimal · up to 2c300

No actionable merge-blocking issue is established for the current change. The held-reward behavior is ready for normal merge checks.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 13 functions across 13 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly describes the main change: showing level Plutonium rewards held until an account is trusted.
Description check Passed The description explains the held-reward behavior, UI changes, claim handling, and tests. It is directly related to the changeset.

Full details: Docstring Coverage

Explanation

Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 13 functions across 13 files. (1 skipped: 1 unsupported.)



  • Fix all pre-merge checks with AI
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A reward can wait, its hold is clear
The panel keeps the pending prize near
Claimable counts guide the way
Trust notes tell what must hold sway
When claims return, the list stays in view

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Oct 9, 2026
@Celant

Celant commented Oct 9, 2026

Copy link
Copy Markdown
Member

Coordinated review — #5879

Reviewed against the backend half (infra #933) and Trust.ts. Several things check out well: the 403 handling is right on both claim paths (single claim returns "held" only for a 403 whose body parses; claim-all renders the partial outcome from held; neither shows the failure alert; a 403 that isn't a hold still falls through to the generic failure). Claim-all gating correctly counts claimable rewards — claimable.length > 1 — and agrees with what the server will actually claim. The CrazyGames branch uses the same crazyGamesSDK.isOnCrazyGames() predicate as trustRequiredDialog, and CrazyGames genuinely has no purchase path.

But the explanatory copy is wrong in a way that lands on exactly the players who will read it.

[high] The held-reward note omits the linked-identity gate, so it is false for the players who see it

Where: resources/lang/en.json → account_modal.reward_held_trust_info / _crazygames, selected in renderHeldNote().

The real rules in Trust.ts (trustFromFacts), in order: an active cheat-family ban → untrusted, always; no linked identity → untrusted, always; then trusted on a paid Steam licence, a settled payment, a Valve-"Trusted" Steam account, soft_total >= 200 lifetime Caps, or (account age ≥ 14 days AND ≥ 25 Public games).

docs/trusted-accounts.md is explicit about the gate: "Any social_identities row, email included. Without one an abandoned account is replaced for free… A payment does not waive this", with the worked example "1 year, 1000 games, paid $50, no link (a guest) → untrusted". It even states what the UI should say: "Since game ingest creates players as guests, every player who has not linked anything is untrusted… the message for that state should be 'link an account'."

So both halves of the note are false for a guest — playing more public games won't lift it, and neither will a purchase.

Failure scenario, and the popup is the live path: a guest reaches level 25, the backend issues the level_milestone grant and marks it held: "trust" (every account gets the grant). Main.ts passes the full reward list to rewardsModal.openWithRewards(rewards), and rewardCount is non-zero because the guest's level_up Caps are claimable — so the login popup opens and tells a player who can never become trusted on that account to play more games or buy something. The account-modal path is gated by isLinkedAccount(), so the popup is the exposure.

On CrazyGames it's sharper: with no purchase route, signing in is the only way for that player to become trusted — and the note doesn't mention signing in at all.

Suggested fix: add signed-out variants and branch on them, exactly as the existing trust copy already does. trustRequiredDialog() in LobbyCard.ts takes a signedIn flag and picks public_lobby.trust_required_body_signed_out / mode_selector.ranked_trust_required_body_signed_out ("…sign in to your account, then play more games or make any purchase"), with the test being responseHasLinkedIdentity(userMe) || crazyGamesSignedIn. AccountIdentity.ts's own comment says "Every identity gate in the client goes through this one function" — this is a fourth, identity-blind copy of a message that already exists correctly. RewardsPanel needs that flag passed in from AccountModal/RewardsModal/Main.

[medium] Even for a linked account, "after you play more public games" understates it

Both floors in the games branch are hard: 14 days of account age AND 25 Public games (ranked and unranked; Singleplayer and Private excluded). The note mentions neither. It also omits the instant pass a free player can actually hit (≥ 200 lifetime Caps, roughly two 10-player wins) and says nothing about a cheat ban overriding everything.

A day-1 player grinds 40 public games, is told "play more public games", and stays held for a fortnight with no explanation.

Suggested fix: name the floors — "…after your account is 14 days old and you've played 25 public games, or right away with any purchase". The vaguer phrasing is pre-existing in the ranked strings, but a reward the player is waiting to be paid is where it reads as a broken promise.

[medium] An unknown held value is treated as claimable, and the 403's real reason is discarded

Where: packages/shared/src/ApiSchemas.ts — held: z.enum(["trust"]).optional().catch(undefined); Api.ts's ClaimRewardHeldResponseSchema parsed for success only; RewardsPanel.ts's { ...r, held: "trust" }.

This fails open. The backend's RewardHold type is designed to grow, and a future variant makes the whole reward read as claimable: held === undefined drives the Claim button, the claim-all count, and Main.ts's rewardCount. The 403 body does carry the real reason, but claimReward throws the parsed value away and the panel stamps held: "trust" regardless.

A non-trust hold then means: the login popup opens on every boot (the exact behaviour the Main.ts filter exists to prevent), a Claim button that always 403s, and after the refusal the wrong reason displayed.

Suggested fix: parse as z.string().optional().catch(undefined) (any non-empty value = held) and switch the copy on === "trust", falling back to a neutral "not claimable yet" line; and carry the 403 body's held into the row rather than hardcoding it.

Worth noting this is the third fail-open .catch() in this codebase in as many days — levelHidden (fixed to .catch(true)) and RowLevelFields (fixed to drop the badge) were the others. The pattern is that .catch(undefined) on a restriction flag reads as "unrestricted". Might be worth a lint rule or a convention note.

[low] One malformed row drops the whole claim-all held list from the UI

held: z.array(RewardSchema).default([]).catch([]) — the .catch is on the array, so one unparseable element discards every held row. The panel emits rewards: [], AccountModal.handleRewardsChanged patches that in place without re-fetching, and the held rewards vanish from the list until reopen or reload.

Fix: parse per element with safeParse and keep the successes; or when held is empty yet claimed.length < this.rewards.length, re-read /users/@me as the not_found branch already does.

[low] A held 403 corrects only the clicked row, though the hold is per-account

docs/trusted-accounts.md says the hold is "a one-time bar on the account" — if one level_milestone is held, all are. Marking only reward.id leaves the others offering Claim buttons that each 403 in turn, so a stale list with three held rewards costs three round trips.

Fix: the branch already calls invalidateUserMe(); re-read and emit the fresh list, mirroring not_found.

[low] The login rewards popup no longer dismisses itself

RewardsModal.ts closes on this.rewards.length === 0. Held rows now survive the claim, so the boot popup stays open showing rows with no affordance and the player has to press back. Next boot is fine, since rewardCount excludes held.

Fix: close when no claimable reward remains, or hand the popup only claimable rewards and leave the note to the account modal.

[low] rewardCount's contract is stale, and claimability is open-coded four times

BootInterrupts.ts:58 still documents rewardCount as "How many unclaimed rewards the account is holding", while its only caller now passes a claimable-only count — so the filter sits at the call site, outside the BootInterrupts test suites, untested. And held === undefined appears in Main.ts plus three places in RewardsPanel.ts; since that expression is the claimability policy, changing it means finding four sites. AccountIdentity.ts is the repo's own precedent for one exported predicate ("One door, one lock").

Fix: export isRewardClaimable(reward) from packages/shared and use it in all four; fix the doc comment or pass the reward list into nextBootInterrupt so the filter is covered. Also Api.ts's new 403 branch has no direct test — tests/Api.test.ts already has the fetch-mocking pattern for a "403 without a held body fails generically" case.


Also checked and clean: schema/panel test coverage of the held and claimable branches is good; the three i18n keys exist, are used, and are alphabetically placed; the claim that /users/@me/xp/:gameId needs no change holds (GameXpEligibleSchema carries no reward list and there is no end-of-game claim UI at this head); and the added comments are all "why" with no change-history narration.

@Celant

Celant commented Oct 9, 2026

Copy link
Copy Markdown
Member

infra #933 merged (7979e603), so this PR is now the deploy blocker.

The backend half is on main: level Plutonium grants are created for every account and marked
held: "trust" on untrusted ones, and POST /rewards/:id/claim answers 403 held:"trust" for
them. Your own note on #933 said this client PR "ships with or before this deploy" — holding you
to that, because deploying #933 to the current client gives a Claim button that 403s with no
explanation.

The six findings in my review above are still unanswered. Both bots read this PR
as clean (CodeRabbit approved it), so there is nothing else flagging them. Shortest path:

  • [high] reward_held_trust_info / _crazygames omit the linked-identity gate, so the note is
    false for exactly the players who see it — a guest can never become trusted by playing more or by
    paying. The login popup is the live path (a guest's level_up Caps are claimable, so it opens).
    trustRequiredDialog() already solves this with a signedIn flag; this would be a fourth,
    identity-blind copy of that message.
  • [medium] held: z.enum(["trust"]).optional().catch(undefined) fails open — a future
    RewardHold variant reads as claimable. Parse z.string(), switch copy on === "trust".
  • [medium] "play more public games" omits both hard floors (14 days and 25 public games).
  • [low] .catch([]) on the held array drops every held row if one element is malformed.
  • [low] a held 403 corrects only the clicked row, though the hold is per-account.
  • [low] the boot popup no longer self-dismisses, since held rows survive the claim.

The high and the fail-open medium are the two I would not ship without. The rest are fine as
follow-ups if you would rather land this quickly.

Separately: this PR has no milestone, so the required Has Milestone check is red.

@iiamlewis iiamlewis added this to the v35 milestone Oct 9, 2026
iiamlewis and others added 3 commits October 9, 2026 19:50
- RewardSchema.held is any non-empty string: a hold kind this client
  doesn't know reads as held, never as claimable. The note switches on
  "trust", with a separate path for other holds.
- claimReward passes the 403's hold on as named ({ held }) instead of a
  bare "held", and the panel uses it rather than assuming "trust".
- A held 403 re-reads /users/@me and emits the whole fresh list (the
  hold is per account), still marking the refused reward held.
- Claim-all's held list is parsed per row, so one bad row drops only
  itself.
- The login rewards popup opens, and stays open, only while a reward can
  be claimed.
- isRewardClaimable() in ApiSchemas is the one claimability predicate
  (Main, RewardsPanel, RewardsModal); rewardCount's doc says claimable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A held value that isn't a non-empty string (a number, an object, "") now
reads as an unknown hold rather than as claimable; only absent or null is
claimable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DRAFT copy, pending design approval.

- Trust needs a linked identity: without one an account is never
  trusted, whatever it plays or buys. RewardsPanel takes a signedIn flag
  (responseHasLinkedIdentity(userMe) || a CrazyGames sign-in, as for
  trustRequiredDialog) from AccountModal, RewardsModal and Main's boot
  popup, and a signed-out viewer is told to sign in first.
- Name the floors: 14 days old and 25 public games
  (TRUST_MIN_ACCOUNT_AGE_DAYS / TRUST_MIN_GAMES), or any purchase off
  CrazyGames.
- A hold with no copy of its own gets a neutral note,
  reward_held_other_info, and no "Trusted accounts only" label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@iiamlewis

Copy link
Copy Markdown
Contributor Author

Thanks — the code findings are pushed (cf487e2, ad9bbcf, plus a main merge):

  • Fail closed: any held that is present and not null reads as held. An unknown kind, or a malformed one (a number, an object, "", read as "unknown"), is never claimable. claimReward carries the 403 body's held through instead of stamping "trust".
  • Claim-all held: parsed per row, so one bad row no longer drops the rest.
  • Held 403: re-reads /users/@me and emits the fresh list, as not_found does, so a per-account hold corrects every row at once.
  • Login popup: closes once nothing claimable is left, and doesn't open for held rewards alone.
  • One rule: isRewardClaimable() in ApiSchemas.ts, used in Main.ts, RewardsPanel and RewardsModal. The rewardCount doc now says claimable, and tests/Api.test.ts covers a 403 without (or with an empty) held failing generically.

The [high] and the [medium] on the copy are built (the panel gets a signed-in flag from the same responseHasLinkedIdentity check trustRequiredDialog uses, with signed-out and CrazyGames variants that name the 14-day / 25-game floors). That's waiting on the designer's sign-off on the wording before it goes up.

🤖 Addressed by Claude Code

@Celant

Celant commented Oct 9, 2026

Copy link
Copy Markdown
Member

Re-reviewed ad9bbcf3 — the four code findings are fixed. The deploy gate is now the copy.

Verified each in the file, not from the summary.

  • [medium] fail-open held → now fails closed. z.unknown().transform() maps undefined/null
    to undefined and everything else that isn't a non-empty string — a number, an object, "" —
    to "unknown". So an unknown hold can never read as claimable. This is the right shape: it
    inverts the default instead of enumerating kinds, so a RewardHold variant added server-side is
    safe against an old client by construction.
  • [medium] the 403's reason is carried, not stamped. claimReward returns
    { held: hold.data.held }, and held: "trust" has zero occurrences left in src/ and
    packages/. A 403 whose body doesn't parse falls through to the generic failure, which is right —
    a 403 that isn't a hold should not be dressed up as one.
  • [low] one malformed row no longer drops the list. Per-element safeParse + flatMap.
  • [low] a held 403 now corrects every row. invalidateUserMe() → getUserMe() → emit the fresh
    list, overlaying the 403's hold on the refused reward only while it still reads claimable.
  • [low] the popup self-dismisses. hasClaimable() gates open().

isRewardClaimable() as the single rule is better than the per-site checks I suggested, and it is
genuinely reused at every decision point — Main.ts:871 (rewardCount), RewardsModal:29,
RewardsPanel:69/161/194/208 — rather than re-derived anywhere. That is the property that keeps
this from drifting apart again.

What this means for the rollout

The [high] (the note omits the linked-identity gate, so it is false for guests) and the
[medium] on the floors are built but not in this PR, pending the designer's wording sign-off.

That is now the only thing gating the #933 deploy, and it is worth being explicit that the gate
is real rather than cosmetic: infra #933 is merged, so once it deploys, every account gets
level_milestone grants marked held: "trust", and a guest's own level_up Caps are claimable —
so the login popup opens for them and shows the note. Shipping the structural half without the copy
half means telling players who can never become trusted on that account to play more games or buy
something.

So: #933 must not deploy until the copy lands here. If the wording sign-off is going to take a
while, the cheaper unblock is a neutral interim string (something that doesn't name a route to
becoming trusted at all) rather than holding the deploy on a design review — the current strings are
wrong in a way a vague one would not be.

Nothing else outstanding from me on this PR.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Oct 9, 2026
@Celant

Celant commented Oct 9, 2026

Copy link
Copy Markdown
Member

2c3006e0 — the [high] and the copy [medium] are fixed. Nothing blocking from me.

Verified the part that actually decides it: which predicate picks the copy.

  • trustNoteKey() branches on this.signedIn and CrazyGames, giving four strings, and both new
    signed-out variants lead with "sign in to your account, then…" — so the guest case now names the
    one route that works for them.
  • Both plumbing sites use the same notion trustRequiredDialog takes:
    AccountModal.ts:452 is responseHasLinkedIdentity(this.userMeResponse ?? false) || this.crazyGamesUser !== null,
    and Main.ts:898 is responseHasLinkedIdentity(userMeResponse) || (isOnCrazyGames() && await getUserProfile() !== null).
    A linked identity, not merely a session — which was the whole point, since a guest with a payment
    is still untrusted.
  • The signed-in strings now name both hard floors (14 days and 25 public games).
  • renderHeldNotes() routes held !== "trust" to reward_held_other_info ("Some rewards can't be
    claimed yet."), so the fail-closed "unknown" hold has neutral copy instead of borrowing the
    trust wording. That pairs the schema change with the UI properly.

One [low] left, and it is the reason I checked the predicate rather than taking the summary

"Signed in for trust purposes" is now re-derived at every call site, and the CrazyGames halves
are not the same test: AccountModal reads cached state (this.crazyGamesUser !== null) while
Main makes a live SDK call (await getUserProfile() !== null). GameModeSelector,
RankedModal and DetailedGameViewModal each compute their own for trustRequiredDialog. There is
no shared helper.

Concretely: if crazyGamesUser has not resolved when the account modal renders, a signed-in
CrazyGames player is told to sign in. Mild on its own — but AccountIdentity.ts already says
"Every identity gate in the client goes through this one function", and that is true of
responseHasLinkedIdentity while the two-part trust test it is half of has five copies. The
drift-proof version is one exported isSignedInForTrust(...) that all five call, so a change to what
"trusted" requires cannot leave some of them behind.

Your call whether that is worth doing here or as a follow-up — the copy itself is correct now, and I
would not hold the PR for it.

@iiamlewis

Copy link
Copy Markdown
Contributor Author

The copy is in (2c3006e), signed off by the designer: the note now depends on whether the account has a linked identity (the same responseHasLinkedIdentity check trustRequiredDialog uses, or a CrazyGames sign-in). Signed-in players are told the 14-day / 25-public-game floors or a purchase. Signed-out players are told to sign in first. CrazyGames drops the purchase route in both, and any other hold gets a neutral line. Description and screenshots are updated. With this, nothing on #5879 should stand in the way of the #933 deploy.

🤖 Addressed by Claude Code

@iiamlewis

Copy link
Copy Markdown
Contributor Author

Thanks for checking the predicate itself. Agreed on the low. Since three of the five sites are outside this PR (GameModeSelector, RankedModal, DetailedGameViewModal), I'll do it as a follow-up: one exported isSignedInForTrust() next to responseHasLinkedIdentity, used by all five, with the CrazyGames half settled on one source. That covers the account-modal timing case you describe.

🤖 Addressed by Claude Code

@openfrontio openfrontio deleted a comment from github-actions Bot Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Verdict: No issues found. Findings: 0 high, 0 medium, 0 low.

Checked for bugs and CLAUDE.md compliance: i18n keys and en.json, homepage and lazy-modal import rules, package layering, fail-closed held parsing, the Claim-all and popup conditions, and the handling of a held 403.

🤖 Generated with Claude Code

This branch was successfully deployed

1 active deployment
staging — 2c3006e0 Deployed Oct 9, 2026 by openfrontio[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

2 participants