Merge pull request 'docs(android): record M2 (public shard) + M3 (auth) landed' (#15) from docs/android-m3-landed into main
Reviewed-on: #15 Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Android App — Plan
|
||||
|
||||
Status: **M0–M2 landed; M3 (auth) next.** This document is the
|
||||
Status: **M0–M3 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.
|
||||
|
||||
**M3–M4 (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
|
||||
|
||||
Reference in New Issue
Block a user