From 7fe35545506d59367a67da0ed47e30c2146c056c Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Sun, 23 Aug 2026 23:04:12 -0400 Subject: [PATCH] feat(onboarding): turn a phone away from Play, a lobby and the daily The three ways into a match now end at the gate on a phone, before the session is read. markShown accepts null so the gate is not a destination, the same answer the not-found screen gets. --- plan/onboarding.md | 14 ++++++++++++- web/src/nav.ts | 9 ++++++++- web/src/router.ts | 43 +++++++++++++++++++++++++++++++++++++++- web/src/screens/daily.ts | 15 ++++++++++++++ 4 files changed, 78 insertions(+), 3 deletions(-) diff --git a/plan/onboarding.md b/plan/onboarding.md index eb9441a..f51992f 100644 --- a/plan/onboarding.md +++ b/plan/onboarding.md @@ -73,4 +73,16 @@ without words, so the two land together. ## Done -Nothing closed yet. +- [x] **A phone is told it cannot play, before it is in a match.** A match is + MegaMek's desktop client, and it neither fits nor takes a finger. The + three ways into one — Play, a lobby address, and the daily card's Play — + now end at a card saying so on a phone, instead of at a match a player + finds out about once somebody is sitting opposite them. `web/src/phone.ts` + is the question, `web/src/screens/phone-gate.ts` the answer. The rest of + the site is untouched by it and stays readable on a phone, which is + [site-a11y](site-a11y.md)'s business. + + A stopgap in this epic's terms rather than an answer to it: it decides + who a first match is not for. What that first match is remains open + above, and whether a phone ever gets one is a question for whatever + replaces MegaMek's client rather than for this gate. diff --git a/web/src/nav.ts b/web/src/nav.ts index 7d05dc4..9b8ff56 100644 --- a/web/src/nav.ts +++ b/web/src/nav.ts @@ -31,8 +31,15 @@ export const nav: { * later click on the Matches link compares the address it names against a * `shown` that never left — a no-op, which was the original bug report * this route exists to fix. + * + * Null says a screen that is not a destination is up — the phone gate, + * which any of the play routes can end at and which the front page's + * daily card can put up without the address moving at all. Nothing in the + * masthead is current then, and no later address equals it, which is what + * keeps every link on the page working from it. Same answer the router + * gives its own dead end, the not-found screen. */ - markShown: ((route: Route, lobbyId?: string | null) => void) | null; + markShown: ((route: Route | null, lobbyId?: string | null) => void) | null; } = { start: null, matches: null, diff --git a/web/src/router.ts b/web/src/router.ts index dc3f49b..0b1a7ed 100644 --- a/web/src/router.ts +++ b/web/src/router.ts @@ -49,6 +49,8 @@ import { lobbyScreen } from "./screens/lobby"; import { lobbyInviteScreen } from "./screens/lobby-invite"; import { matchesScreen } from "./screens/matches"; import { notFoundScreen } from "./screens/not-found"; +import { isPhone } from "./phone"; +import { phoneGateScreen } from "./screens/phone-gate"; import { hangarScreen } from "./screens/hangar"; /** @@ -162,7 +164,10 @@ let shownLobbyId: string | null = null; * on screen. Wired to nav.markShown in main.ts — see that field's own doc * for why matches.ts needs it for the fresh-lobby entry. */ -export function markEntered(route: Route, lobbyId: string | null = null): void { +export function markEntered( + route: Route | null, + lobbyId: string | null = null, +): void { shown = route; shownLobbyId = route === "lobby" ? lobbyId : null; markCurrent(route); @@ -208,6 +213,32 @@ export async function start(): Promise { route === "lobby" ? lobbyIdFromHash(window.location.hash) : null; markCurrent(route); + // Before any of the play routes are resolved: a phone is told it cannot + // play rather than being walked up to a match it will not be able to use. + // Play and a lobby are two of the three ways into one; the daily branch + // below carries the third, which only becomes a match once its stash says + // so. See screens/phone-gate.ts. + // + // Above the session read, so a signed-out phone gets this rather than the + // front page with a sign-in form on it. Play is in the masthead for + // everyone, and "sign in first" would be an answer that leads nowhere: the + // account is not what is missing. + if ((route === "matches" || route === "lobby") && isPhone()) { + // Spent, not left sitting: both of these are a press this gate is the + // answer to, and the posture the branches below take is that reaching + // one of these addresses for any reason clears a stash an earlier press + // left. A phone getting turned away is still having reached it. + takePendingChallenge(); + takePendingLobbyInvite(); + // Not a destination, the same as the not-found screen above: nothing in + // the masthead is current here, and a second press of Play should put + // the gate up again rather than compare "matches" against itself and do + // nothing. + markEntered(null); + render(...phoneGateScreen()); + return; + } + // About and the pages under it: one screen, which reads the page out of the // route. None of them touches the session. if (route.startsWith("about")) { @@ -224,6 +255,16 @@ export async function start(): Promise { // sign-in left, so an old Play press cannot hijack a later one. const pending = takePendingDailyPlay(); if (pending) { + // The daily's Play, pressed on a phone: the third way into a match, + // and the one that is only a match once the stash says so. Above the + // session read because the answer does not depend on it. The stash is + // spent by now, which is what this wants — the gate is the answer to + // that press, and a later one from a desktop should start fresh. + if (isPhone()) { + markEntered(null); + render(...phoneGateScreen()); + return; + } // The one thing here that does need a session, and the only reason this // branch ever reads one. A failed read falls through to the archive // rather than to an error screen: the reader asked to play a fight, and diff --git a/web/src/screens/daily.ts b/web/src/screens/daily.ts index 5e5900e..f28f2a5 100644 --- a/web/src/screens/daily.ts +++ b/web/src/screens/daily.ts @@ -47,6 +47,8 @@ import { } from "../content/overrides"; import { ERA_ICONS } from "../content/trivia"; import { drawMinimap, haveBoards, minimapSize } from "./minimap"; +import { isPhone } from "../phone"; +import { phoneGateScreen } from "./phone-gate"; import { sideCss, sideRgb, type RGB } from "../content/sides"; /** @@ -906,6 +908,19 @@ export function playControl(challenge: Challenge, day: string): PlayControl { play.addEventListener("click", () => { play.disabled = true; + // The daily card is on the front page and on the archive, both of which a + // phone can read — so this button is a way into a match from a screen the + // gate does not stand in front of, and has to ask for itself. Before the + // session read: whether a phone can play does not depend on who is + // pressing. See screens/phone-gate.ts. + if (isPhone()) { + // The gate is not a destination, and the address has not moved: without + // this the router still believes the front page is up, and the Home + // link in the masthead would be a no-op against it. + nav.markShown!(null); + render(...phoneGateScreen()); + return; + } void currentSession() .then(async (session) => { if (!session) { -- 2.51.2