On-device, the native SSO buttons never appeared and the flow dumped users on
the desktop website login (which can't deep-link a mobile session back), so it
hung. Two app-side causes:
1. Discovery conflated "no providers" with "call failed" (ssoProviders() returned
emptyList() on any error) and the screen then showed a dead website-login
hand-off. Now ssoProviders() returns Available/None/Unavailable, retries once,
and the login screen renders native provider buttons, a loading hint, or a
retry — never the website login fallback (removed, along with WebsiteUrls.login).
2. The pending {state, verifier} lived only in memory, so a Custom-Tab-induced
process eviction lost it and the exchange failed STATE_MISMATCH. Persist it via
a new encrypted PendingSsoStore (EncryptedSharedPreferences, mirrors the token
store), cleared the moment the callback is consumed so replays still fail closed.
SsoAuthManager stays framework-free (store behind an interface). +1 test proving a
fresh manager on the persisted store completes (process-death sim); 15/15 SSO tests
pass, lint + assembleDebug green (JDK21, -Pksp.incremental=false).
Verified end-to-end against the local site via the dev stub IdP: player and admin
both sign in natively and receive the correct role.
Co-Authored-By: Claude <noreply@anthropic.com>