THE BUG: In pollDeviceCredentials(), the credentials endpoint returns client_id and client_secret, which are extracted into local variables. Then exchangeForAccessToken() is called, which hits the token endpoint and gets back access_token, refresh_token, expires_in, token_type — but NOT client_id/client_secret. The saveCredentials() method null-checks these fields and skips saving them. So client_id and client_secret are NEVER persisted to SharedPreferences during the initial login flow.
This means refreshAccessToken() always fails because clientId == null || clientSecret == null from prefs. The original access_token works for its validity period (~48 hours), then expires with no way to refresh.
So we must persist the clientId and clientSecret