docs(android): record M3 (auth) landed, M4 next

Records the M3 functional auth pass in docs/android/PLAN.md: native
username/password (+TOTP) login, EncryptedSharedPreferences token storage,
the refresh-on-401 authenticator, /auth/me resume re-validation, the
declarative access-level menu + My Account, and the Custom-Tab hand-offs for
register / forgot-password / SSO. Also records the decision to defer the
optional biometric app-lock to M6 (tokens are already encrypted at rest).

No backend/API change accompanied M3 — the app is a pure consumer of the
existing mobile bearer + /auth/me surface — so no Swagger regeneration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
This commit is contained in:
2026-07-19 19:09:15 -05:00
parent 90bc9a0dad
commit 0d5743ec74

View File

@@ -1,6 +1,6 @@
# Android App — Plan
Status: **M0M2 landed; M3 (auth) next.** This document is the
Status: **M0M3 landed; M4 (player self-service & game data) next.** This document is the
design contract for the `RunicGateway/Android-app` repo. It was written before implementation so the
API changes it depends on could be landed in `website/` and `docs/` first. The authoritative API
reference is the committed OpenAPI spec at `website/server/swagger/swagger-output.json` (regenerated
@@ -47,7 +47,35 @@ DTOs. Wired into the shared drawer (§5), all strings externalized (§2). JVM un
SSE frame parsing. No backend/API change — the app is a pure consumer of the existing public shard
surface.
**M3M4 (functional pass) and M5 (design pass) remain.**
**M3 — auth** (2026-07-19, `RunicGateway/Android-app#8`, functional Kotlin pass): native
**username/password (+ single-request TOTP) login** over the existing `POST /auth/mobile/login` — a
`401 { totpRequired }` reveals the code field and a wrong code re-lands as a code error; `429` surfaces
a friendly backoff message (§4.1). The token pair lives in **EncryptedSharedPreferences** (a
`TokenStore` behind `SessionManager`, the single source of truth for the in-memory bearer + the
observable `Session`); the base URL stays in plain DataStore (§4.3). An OkHttp `AuthInterceptor`
attaches the bearer and a `TokenAuthenticator` does a **one-shot, mutex-serialized refresh** on a
bearer `401` and replays the request — refresh runs on its own **bare** client (no interceptor/
authenticator) so it can never recurse, rotated single-use tokens are stored atomically, and a dead
refresh (`401`) signs out while a transient network error keeps the session. Logout
(`POST /auth/mobile/logout`, this session or all devices) tears down locally even if the call fails.
`GET /auth/me` **re-validates the role on every resume** (`LifecycleResumeEffect`); a surviving `401`
signs out, so a server-side demotion drops menu access promptly (role stays advisory — the backend is
authority, §4.3). The **access-level menu** is one declarative list (`visibleEntries` filters by
session — public / signed-in / player) with a Sign in / Sign out toggle and a **My Account** screen
(identity + role + sign-out / sign-out-everywhere). Registration, forgot-password, and SSO are
**Custom-Tab hand-offs** (androidx.browser) to the website's own pages (`/account/register`,
`/account/forgot`, `/account/login`) — no native screens (§4.2). The Settings → Server switch now also
clears the stored session (§3). JVM unit tests (18) cover auth-DTO decode (incl. `totpRequired` vs a
plain credential `401`), the `SessionManager` lifecycle over a fake store, and the menu access filter
+ role mapping. **No backend/API change** — the app is a pure consumer of the existing mobile bearer +
`/auth/me` surface.
> **Biometric app-lock — deferred (decided at M3).** §4.3/§9 flag an *optional* biometric app-lock;
> it is **deferred to M6 (release hardening)**. Tokens are already encrypted at rest (Tink/AES-256-GCM),
> so an app-lock is a UX layer, not a security requirement, and adding it in the functional pass would
> widen scope without changing the data flow. Revisit as an opt-in setting during M6.
**M4 (functional pass) and M5 (design pass) remain.**
**Prerequisite progress (§8):** all v1 prerequisites are **done** (2026-07-19) — ✅ password reset
(item 2; website#75 + docs#8), ✅ role-agnostic `/auth/me/*` self surface (item 1; website#76 + docs#10),
@@ -527,7 +555,8 @@ password+TOTP only, with registration/invite/reset/SSO **handled by the website*
reset built on backend + web first**, before app work (§8); minSdk 29, compile/target 35 (§2); no
telemetry in v1 (§2); strings externalized from day one, English-only bundled (§2); **text-only** game
data in v1, pretty paperdoll is future (§6.3); **no offline cache in v1** (§7); push via self-hosted
ntfy / UnifiedPush (§11).
ntfy / UnifiedPush (§11); **biometric app-lock deferred to M6** (tokens already encrypted at rest, so
it is opt-in UX, not a v1 requirement — decided at M3).
**Still open:**
- **App identity / domain.** Target application ID **`com.runicgateway.app`** — pending securing the