Skip to content

Split isSteamShell() from isDesktopShell() so the Android shell skips Steam paths - #6408

Merged
evanpelle merged 1 commit into
mainfrom
evan/ope-764-client-separate-issteamshell-from
Nov 12, 2026
Merged

evanpelle merged 1 commit into
mainfrom
evan/ope-764-client-separate-issteamshell-from

Conversation

@evanpelle

Copy link
Copy Markdown
Collaborator

Description:

The Android app defines window.openfrontDesktop too, but its bridge has no steam namespace (and no linkGate, presence, invite, display or quit). isDesktopShell() is true there, so every Steam-only path would fire in a shell that can't do any of it. That covers the Steam sign-in and link flow, the Turnstile exemption, Steam presence and invites, the "(Steam v…)" label and the Steam session gate.

This adds isSteamShell() next to isDesktopShell(). It is true when the bridge carries steam, the same test SteamSDK.isOnSteam() and Payments.steamBridge() already make. Every isDesktopShell() call site was audited:

  • Sites that call or depend on Steam move to isSteamShell().
  • Sites about being in any native shell keep isDesktopShell(): ads off, the shell's own updater, a document the shell serves.

isDesktopShell is not renamed, and nothing Android-specific is added beyond the split.

Why the Steam build is unchanged

On Steam, isSteamShell() === isDesktopShell() always holds:

  • openfront-desktop's preload exposes steam unconditionally, whether or not a Steam client is running. So whenever the bridge exists on Steam, steam is in it.
  • On replay.* hosts the preload withholds the whole bridge, so both predicates are false there.

Every moved site therefore takes the same branch on Steam as before. The three sites that combined an update gate and a session gate are now split: update on isDesktopShell(), session on isSteamShell(). On Steam both halves are still on.

Audit

Moved to isSteamShell() (19)

Site What it does Why Steam
Auth.ts:90 startDesktopLinkFlow Shell with no showLinkGate gets an update prompt instead of the OAuth redirect The redirect can't come back to app://. Android is served at the site's own origin, so the web redirect works there.
Auth.ts:152 linkGoogle Opens the website's account settings instead of running OAuth Same app:// reason
AccountModal.ts:865 "Link Google on the website" caption Must match linkGoogle
AccountModal.ts:883 renderSteamLink Hides "Link Steam" Only a Steam shell already holds the Steam identity through the ticket
AccountModal.ts:1022 viaBrowser "Continue in browser" login captions Must match startDesktopLinkFlow
AccountSettingsPanel.ts:324 Google link caption Must match linkGoogle
ClientPlatform.ts:13 Reports "steam" Literally Steam. Android now reports "web" (see open questions).
Footer.ts:117 "(Steam v…)" version line The label names Steam
FreePlayPerks.ts:34 "No ads in the Steam app, ever" perk The string names Steam
WebGLGate.ts:105 Steam/driver instructions and a Quit button Calls quit, which only Steam has. Tells the player to use Steam.
DesktopSessionRecovery.ts:101 Auto-retries Steam sign-in on online / reachability Re-runs the Steam ticket flow
DesktopStatusBar.ts:221 Seeds the Steam session state The session is Steam's
Main.ts:575 Skips the Turnstile prefetch The server exemption is a provider="steam" JWT (JoinVerify.planJoinVerify), not the shell. Android has to send a token.
Main.ts:2350 getTurnstileToken Returns no token Same
Main.ts:957 steamNoticeDue Steam account notice Steam
Main.ts:1778 blockedJoin (session half) Gates joins on the Steam session Steam sign-in. The update half stays on isDesktopShell().
GameModeSelector.ts:415 (session half) Seeds the session gate Same split
DetailedGameViewModal.ts:223 (session half) Seeds the session gate Same split
DesktopPresence.ts:62 isAvailable Gates presence, invites and the Invite Friends button It used to check only shell.api >= 2. That number space is shared with Android, which is past 2 but has no presence or invites, so it would have shown a dead Invite Friends button.

Also index.html:215: the first-paint desktop-layout class is now Steam only. Android lays out the way the site does in a mobile browser. That script also defines BOOTSTRAP_CONFIG, so it uses && rather than optional chaining.

Kept on isDesktopShell() (26)

Site What it does Why every shell
Main.ts:837 Ads off No ads in any app shell
FeaturedStream.ts:198 No Twitch embed Same reason as ads
PurchaseNudgeModal.ts:38 No "ad-free for life" pitch Ads are already off
CosmeticCard.ts:619 No ad-free badge Ads are already off
WinModal.ts:590 No "go ad-free" text Ads are already off
UserSettings.ts:984 Default master volume The app is a deliberate launch, not a tab
Main.ts:942, Main.ts:1012 bootInterruptsAllowed The shell serves /index.html (Android navigates there too)
Main.ts:1733 No redirect of an unknown letter host to the apex The shell owns navigation
Transport.ts:618 No WrongWorker page redirect The shell owns navigation
ServerList.ts:199 Server-list site is the injected serverHost Shell configuration
ServerList.ts:804, 942, 973 No version redirects or "behind latest" The shell's updater owns the version
ClientGameRunner.ts:398 version_mismatch shows the updater alert Updater. The string doesn't name Steam.
GameModeSelector.ts:351 Updater handling Updater
GameModeSelector.ts:414, DetailedGameViewModal.ts:222, Main.ts:1777 Update half of the join gate Every shell has the updater
GameModeSelector.ts:234 Outage notice suppressed The shell's status bar shows the outage
DesktopStatusBar.ts:233 Offline / reachability strip Not Steam
UserSettingModal.ts:807 Display tab Also gated on desktopDisplay(), which Android lacks, so it stays hidden there anyway
Cosmetics.ts:373 promptManageBilling Doesn't link out to the Stripe portal Ambiguous, see below
SubscriptionPanel.ts:456 onDesktop Shows "manage on the website" instead of Stripe buttons Ambiguous, see below
GameRightSidebar.ts:65, ReplayControls.ts:94 Hides the fullscreen button Ambiguous, see below

Direct bridge reads that already key on their own namespace (SteamSDK, Payments, DesktopDisplay) are untouched.

Ambiguous sites (decided from the code)

  • Payment link-outs (Cosmetics.ts:373, SubscriptionPanel.ts:456): kept as "no external payment link-outs from an app shell". The comment says the guard that kills the button lives in the shell. On Android, Play's anti-steering policy points the same way. The strings say "on the OpenFront website", not Steam. Keeping this is the conservative choice.
  • Fullscreen button (GameRightSidebar.ts:65, ReplayControls.ts:94): kept hidden. A native app owns its own window.

Open questions

  • Android clientPlatform() now reports "web". There is no "android" value in ClientPlatform.
  • The WebGL gate shows the browser instructions on Android, since the Steam ones don't apply.
  • Android doesn't get the ad-free perk line, though it has no ads, because the string names Steam.
  • paymentsProvider() is "stripe" on Android (no steam bridge), while the link-out guards above still hide Stripe's buttons. That needs settling with Google Play billing.
  • Android now shows the web's Steam OpenID login and "Link Steam" options, as a browser does.

Tests

  • isDesktopShell / isSteamShell: web is false/false, Steam is true/true, an Android-shaped bridge is true/false.
  • Android-shaped bridge, for the riskiest moved sites:
    • Discord/Google login redirect with no update prompt
    • DesktopPresence.isAvailable() is false at api 7
    • clientPlatform() is "web"
  • Existing Steam fixtures that used a bare {} bridge now carry steam: {}, matching what the real preload always exposes.

npm test, npm run lint, npm run typecheck and oxfmt --check pass locally.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory

Please put your Discord username so you can be contacted if a bug or regression is found:

evan

🤖 Generated with Claude Code

@CLAassistant

CLAassistant commented Nov 12, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

The Android app defines window.openfrontDesktop too, with no `steam`
namespace. isDesktopShell() is true there, so every Steam-only path
(Steam sign-in and link flow, the Turnstile exemption, Steam presence and
invites, the "(Steam v...)" label, the Steam session gate) would have
fired in a shell that cannot do any of it.

Add isSteamShell(), true when the bridge carries `steam`, and move the
Steam-dependent call sites to it. Sites about being in any native shell
(ads off, the shell's updater, the shell-served document) keep
isDesktopShell(). The Steam preload exposes `steam` unconditionally, so
on Steam the two predicates are always equal and the Steam build is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@evanpelle
evanpelle force-pushed the evan/ope-764-client-separate-issteamshell-from branch from 006b29d to d1bfa09 Compare November 12, 2026 01:30
@coderabbitai

coderabbitai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The client now identifies Steam shells separately from other desktop shells. Authentication, session handling, platform features, and selected interface behavior use the Steam-shell check. Android shells continue to use web authentication.

Changes

Steam and Android Shell Behavior

Layer / File(s) Summary
Shell identity and native API access
src/client/DesktopShell.ts, src/client/ClientPlatform.ts, src/client/DesktopPresence.ts, src/client/DesktopSessionRecovery.ts, tests/DesktopShell.test.ts, tests/DesktopPresence.test.ts, tests/DesktopSessionRecovery.test.ts, tests/client/ClientPlatform.test.ts
The client adds isSteamShell() while retaining isDesktopShell(). Platform detection, desktop presence availability, and automatic session recovery now distinguish Steam from Android shells. Tests cover shell detection and presence availability.
Authentication outside the Steam shell
src/client/AccountModal.ts, src/client/Auth.ts, src/client/components/AccountSettingsPanel.ts, tests/client/AccountModal.rendering.test.ts, tests/client/Auth.desktopLogin.test.ts, tests/client/Auth.steam.test.ts, tests/client/SteamHandoff.test.ts
Account captions and Steam-link controls now use the Steam-shell check. Android Discord and Google sign-in use web OAuth redirects without an update prompt. Google linking opens website account settings only in the Steam shell.
Desktop update and Steam session state
src/client/GameModeSelector.ts, src/client/Main.ts, src/client/components/DesktopStatusBar.ts, src/client/components/DetailedGameViewModal.ts
Desktop update state remains available in desktop shells. Session state is seeded and supplied to join blocking only in Steam shells. Turnstile exemptions and Steam notice eligibility now use the Steam-shell check.
Steam-specific interface behavior
index.html, src/client/components/Footer.ts, src/client/components/FreePlayPerks.ts, src/client/components/WebGLGate.ts, tests/client/components/Footer.test.ts, tests/client/components/WebGLGate.test.ts
The desktop-layout class, footer version label, ad-free perk, and desktop WebGL gate now require the Steam-shell check. Related test fixtures identify Steam through the steam property.

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

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  actor AndroidPlayer
  participant AccountModal
  participant Auth
  participant Browser
  AndroidPlayer->>AccountModal: Select Discord or Google sign-in
  AccountModal->>Auth: Start sign-in
  Auth->>Browser: Navigate to OAuth URL with web-page redirect_uri
Loading

Suggested reviewers: celant

Merge Risk: Minimal · up to d1bfa

The change cleanly separates Steam-specific behavior from behavior shared by all native shells, and Steam behavior should be unchanged. No concrete merge-blocking defect was found. Assign the required milestone before merging.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names the main change: a new isSteamShell() predicate split from isDesktopShell() so the Android shell skips Steam-only paths. The raw summary and objectives confirm this is the central chan…
Description check Passed The description explains the problem (Android defines window.openfrontDesktop without a steam namespace, so isDesktopShell() wrongly enables Steam paths), the new isSteamShell() predicate, the audit o…
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.
Full details: Title check

Explanation

The title names the main change: a new isSteamShell() predicate split from isDesktopShell() so the Android shell skips Steam-only paths. The raw summary and objectives confirm this is the central change across the Steam-dependent call sites, and the title is specific and concise.

Full details: Description check

Explanation

The description explains the problem (Android defines window.openfrontDesktop without a steam namespace, so isDesktopShell() wrongly enables Steam paths), the new isSteamShell() predicate, the audit of call sites, why the Steam build is unchanged, tests, and open questions. This matches the changeset described in the raw summary.

  • 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

Steam has its own signal now
Android takes the web sign-in route
Session checks follow shell type
A bridge marks where features belong
Tests trace each platform’s path

Recent review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d4ba366d-8a50-4648-bb20-36c71d524e1f
Commits

Reviewing files that changed from the base of the PR and between 66b8d86 and d1bfa09.

Files selected for processing: 25

  • index.html
  • src/client/AccountModal.ts
  • src/client/Auth.ts
  • src/client/ClientPlatform.ts
  • src/client/DesktopPresence.ts
  • src/client/DesktopSessionRecovery.ts
  • src/client/DesktopShell.ts
  • src/client/GameModeSelector.ts
  • src/client/Main.ts
  • src/client/components/AccountSettingsPanel.ts
  • src/client/components/DesktopStatusBar.ts
  • src/client/components/DetailedGameViewModal.ts
  • src/client/components/Footer.ts
  • src/client/components/FreePlayPerks.ts
  • src/client/components/WebGLGate.ts
  • tests/DesktopPresence.test.ts
  • tests/DesktopSessionRecovery.test.ts
  • tests/DesktopShell.test.ts
  • tests/client/AccountModal.rendering.test.ts
  • tests/client/Auth.desktopLogin.test.ts
  • tests/client/Auth.steam.test.ts
  • tests/client/ClientPlatform.test.ts
  • tests/client/SteamHandoff.test.ts
  • tests/client/components/Footer.test.ts
  • tests/client/components/WebGLGate.test.ts

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

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Verdict: No issues found. Checked for bugs and CLAUDE.md compliance.

Findings: 0 critical · 0 major · 0 minor

Checked:

  • Same behaviour on Steam: isSteamShell() uses the same steam !== undefined test as SteamSDK.isOnSteam(). Each moved site, and each of the three join gates that now check update and session separately (Main.blockedJoin, GameModeSelector, DetailedGameViewModal), gives the same result as before whenever the bridge has steam.
  • No Steam calls on Android: every call to steam, linkGate, presence, invite or quit is now behind isSteamShell() or a check for its own namespace. DesktopPresence.isAvailable() now returns false without steam.
  • Session gate on Android: the session state stays null, and multiplayerAllowedForSession(null) lets the player through, as its doc comment says. The update gate still applies in every shell.
  • Turnstile: the prefetch and getTurnstileToken() both moved to isSteamShell(). That lines up with the server exemption (provider === "steam" JWT in JoinVerify), so Android now sends a token.
  • index.html: the && check is safe when the bridge is missing, and BOOTSTRAP_CONFIG is still defined.
  • CLAUDE.md: no new user-visible strings and no new imports that cross the homepage or package boundaries. The test environments are correct.

The Android behaviour changes left over (clientPlatform() reports "web", the browser WebGL instructions, the perk line, the Stripe link-outs) are the ones the PR already lists under open questions.

@evanpelle evanpelle added this to the v37 milestone Nov 12, 2026
@evanpelle
evanpelle merged commit 906d93f into main Nov 12, 2026
19 of 20 checks passed
@evanpelle
evanpelle deleted the evan/ope-764-client-separate-issteamshell-from branch November 12, 2026 02:32
@github-project-automation github-project-automation Bot moved this from Triage to Complete in OpenFront Release Management Nov 12, 2026

This branch was successfully deployed

2 active deployments
backport — d1bfa092 Deployed Nov 12, 2026 by evanpelle via Cherry-pick #1027
staging — d1bfa092 Deployed Nov 12, 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: Complete

Development

Successfully merging this pull request may close these issues.

2 participants