hotfix: OAuth login being aborted by a forced logout on Android
The app logged itself out in the middle of the OAuth code exchange, tearing down the page before the callback request could finish. On resume, the deep-link handler and the sync-on-resume listener fire in the same tick. Sync ran first, hit a 401 (no session exists yet), and ApiClient treated that as an expired session: it cleared tokens, wiped the offline DB, and hard-navigated to /login via window.location.href. That reload aborted the in-flight POST /auth/oauth2/callback, surfacing as "Authentication request failed TypeError: Failed to fetch".
This commit is contained in:
@@ -2,6 +2,7 @@ import { Preferences } from '@capacitor/preferences'
|
||||
import { API_URL } from '../Config'
|
||||
import { networkManager } from '../hooks/NetworkManager'
|
||||
import { logout, RefreshToken } from './Fetcher'
|
||||
import { isOAuthExchangeInProgress } from './OAuthExchangeState'
|
||||
import { offlineDB } from './OfflineDB'
|
||||
import {
|
||||
clearAllTokens,
|
||||
@@ -9,6 +10,8 @@ import {
|
||||
saveTokens,
|
||||
} from './TokenStorage'
|
||||
|
||||
const OAUTH_EXCHANGE_IN_PROGRESS = 'OAuth exchange in progress'
|
||||
|
||||
class ApiClient {
|
||||
constructor() {
|
||||
this.customServerURL = `${API_URL}/api/v1`
|
||||
@@ -46,6 +49,13 @@ class ApiClient {
|
||||
}
|
||||
|
||||
async refreshToken() {
|
||||
// No session exists yet while an OAuth code exchange is in flight, so there
|
||||
// is nothing to refresh. Callers must treat this as a non-fatal failure
|
||||
// (see request()) rather than an expired session.
|
||||
if (isOAuthExchangeInProgress()) {
|
||||
return { success: false, error: OAUTH_EXCHANGE_IN_PROGRESS }
|
||||
}
|
||||
|
||||
// Check if refresh token is expired BEFORE attempting refresh
|
||||
const refreshExpired = await isRefreshTokenExpired()
|
||||
if (refreshExpired) {
|
||||
@@ -132,6 +142,14 @@ class ApiClient {
|
||||
|
||||
// Helper to avoid repeating cleanup code
|
||||
async handleLogout() {
|
||||
// Backstop for every forced-logout path: never tear down the session while
|
||||
// an OAuth exchange is running, or we clear the tokens it just saved and
|
||||
// reload the page out from under it.
|
||||
if (isOAuthExchangeInProgress()) {
|
||||
console.log('Skipping forced logout: OAuth exchange in progress')
|
||||
return
|
||||
}
|
||||
|
||||
await clearAllTokens()
|
||||
try {
|
||||
await offlineDB.clearAll()
|
||||
@@ -230,6 +248,13 @@ class ApiClient {
|
||||
this.handleLogout()
|
||||
return null
|
||||
}
|
||||
} else if (refreshResult.error === OAUTH_EXCHANGE_IN_PROGRESS) {
|
||||
// Expected 401: the code exchange hasn't produced tokens yet. Fail
|
||||
// just this request — logging out here would wipe storage and hard
|
||||
// navigate to /login, aborting the exchange fetch mid-flight.
|
||||
queuedPromise.catch(() => {}) // not returned below; keep it handled
|
||||
this.processQueue(new Error(refreshResult.error), null)
|
||||
return response
|
||||
} else if (refreshResult.error === 'Already refreshing') {
|
||||
// This shouldn't happen since we check isRefreshing above, but handle it anyway
|
||||
console.log('Already refreshing - waiting for refresh to complete')
|
||||
|
||||
Reference in New Issue
Block a user