diff --git a/android/src/main/java/com/frontegg/android/services/Api.kt b/android/src/main/java/com/frontegg/android/services/Api.kt index 785cf81..14b40f3 100644 --- a/android/src/main/java/com/frontegg/android/services/Api.kt +++ b/android/src/main/java/com/frontegg/android/services/Api.kt @@ -32,7 +32,11 @@ import java.io.IOException open class Api( private var credentialManager: CredentialManager ) { - private var httpClient: OkHttpClient = OkHttpClient() + private var httpClient: OkHttpClient = OkHttpClient.Builder() + .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS) // For slow networks (EDGE) + .readTimeout(60, java.util.concurrent.TimeUnit.SECONDS) // For slow networks (EDGE) + .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS) + .build() private val storage = StorageProvider.getInnerStorage() private val baseUrl: String get() { diff --git a/android/src/main/java/com/frontegg/android/services/FronteggAuthService.kt b/android/src/main/java/com/frontegg/android/services/FronteggAuthService.kt index 09de8ee..fe38c2d 100644 --- a/android/src/main/java/com/frontegg/android/services/FronteggAuthService.kt +++ b/android/src/main/java/com/frontegg/android/services/FronteggAuthService.kt @@ -72,9 +72,6 @@ class FronteggAuthService( MultiFactorAuthenticatorProvider.getMultiFactorAuthenticator() private val stepUpAuthenticator = StepUpAuthenticatorProvider.getStepUpAuthenticator(credentialManager) - - @Volatile - private var refreshingInProgress = false override val isMultiRegion: Boolean get() = regions.isNotEmpty() @@ -217,18 +214,42 @@ class FronteggAuthService( } } + @Volatile + private var refreshingInProgress = false + private var refreshRetryCount = 0 + private val maxRetries = 2 + private val baseRetryDelayMs = 3000L + override fun refreshTokenIfNeeded(): Boolean { return try { refreshingInProgress = true - this.sendRefreshToken() + val success = this.sendRefreshToken() + if (success) { + refreshRetryCount = 0 // Reset on success + } + success } catch (e: FailedToAuthenticateException) { Log.e(TAG, "Refresh token is invalid, clearing credentials", e) + refreshRetryCount = 0 // Clear credentials when refresh token is invalid clearCredentials() false } catch (e: Exception) { - Log.e(TAG, "Failed to send refresh token request", e) - false + Log.e(TAG, "Failed to send refresh token request (attempt ${refreshRetryCount + 1})", e) + + // Retry on network errors only + if (isNetworkError() && refreshRetryCount < maxRetries) { + refreshRetryCount++ + val delayMs = baseRetryDelayMs * refreshRetryCount // 3s, 6s + Log.d(TAG, "Scheduling retry in ${delayMs}ms (attempt ${refreshRetryCount}/$maxRetries)") + + // Schedule retry using the timer + refreshTokenTimer.scheduleTimer(delayMs) + return false + } else { + refreshRetryCount = 0 + return false + } } finally { refreshingInProgress = false } @@ -521,11 +542,19 @@ class FronteggAuthService( refreshToken.value = refreshTokenSaved if (!refreshTokenIfNeeded()) { - // Offline-safe: keep saved refresh token to allow later reconnect - accessToken.value = null - // keep refreshToken.value as is + // Check if this is a network error or auth error + if (isNetworkError()) { + // Offline-safe: keep saved refresh token to allow later reconnect + // But mark as not authenticated until we can refresh + accessToken.value = null + isAuthenticated.value = false + isLoading.value = true // Keep loading state for retry + // keep refreshToken.value as is + } else { + // Auth error: clear tokens as refresh token is invalid + clearCredentials() + } initializing.value = false - isLoading.value = false } } else { @@ -548,8 +577,41 @@ class FronteggAuthService( try { val data = api.refreshToken(refreshToken) lastRefreshError = null // Clear error on success - val success = setCredentials(data.access_token, data.refresh_token) - return success + + // Save new tokens immediately to prevent rotation issues + credentialManager.save(CredentialKeys.REFRESH_TOKEN, data.refresh_token) + credentialManager.save(CredentialKeys.ACCESS_TOKEN, data.access_token) + + // Update state with new tokens + this.refreshToken.value = data.refresh_token + this.accessToken.value = data.access_token + + // Try to get user info, but don't fail if network is slow + try { + val user = api.me() + if (user != null) { + this.user.value = user + this.isAuthenticated.value = true + + // Schedule next refresh timer + refreshTokenTimer.cancelLastTimer() + val decoded = JWTHelper.decode(data.access_token) + if (decoded.exp > 0) { + val offset = decoded.exp.calculateTimerOffset() + refreshTokenTimer.scheduleTimer(offset) + } + } + } catch (e: Exception) { + // If me() fails due to network, keep tokens but mark as loading + Log.w(TAG, "Failed to fetch user info after token refresh, keeping tokens", e) + this.isLoading.value = true + } + + // Always set initializing to false after successful token refresh + this.initializing.value = false + this.isLoading.value = false + + return true } catch (e: Exception) { lastRefreshError = e throw e diff --git a/android/src/main/java/com/frontegg/android/services/FronteggReconnector.kt b/android/src/main/java/com/frontegg/android/services/FronteggReconnector.kt index 93470be..b4639b8 100644 --- a/android/src/main/java/com/frontegg/android/services/FronteggReconnector.kt +++ b/android/src/main/java/com/frontegg/android/services/FronteggReconnector.kt @@ -78,20 +78,22 @@ object FronteggReconnector { delay(50) } } catch (t: Throwable) { - Log.e(TAG, "Failed to handle network reconnect", t) + Log.e(TAG, "Failed to retry SDK initialization", t) } // Always try to refresh if possible, swallow network errors try { - try { + if (FronteggApp.instance != null) { + Log.d(TAG, "Attempting token refresh on network reconnect") val refreshSuccess = context.fronteggAuth.refreshTokenIfNeeded() if (refreshSuccess) { + Log.d(TAG, "Token refresh successful on network reconnect") + } else { + Log.d(TAG, "Token refresh failed on network reconnect, will retry later") } - } catch (_: Throwable) { - // If FronteggApp not initialized, skip refresh } } catch (t: Throwable) { - Log.e(TAG, "Failed to handle network reconnect", t) + Log.e(TAG, "Failed to refresh token on network reconnect", t) } } }