From 0aeb09203235055e784d66106a59ae0f87b6a806 Mon Sep 17 00:00:00 2001 From: dianaKhortiuk-frontegg Date: Thu, 9 Oct 2025 12:28:37 +0200 Subject: [PATCH 1/2] FR-22003 --- .../java/com/frontegg/android/services/Api.kt | 6 +- .../android/services/FronteggAuthService.kt | 66 +++++++++++++++++-- .../android/services/FronteggReconnector.kt | 14 ++-- 3 files changed, 72 insertions(+), 14 deletions(-) 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 e9ea2b2d..c4667211 100644 --- a/android/src/main/java/com/frontegg/android/services/Api.kt +++ b/android/src/main/java/com/frontegg/android/services/Api.kt @@ -31,7 +31,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 741b4707..5cd13d52 100644 --- a/android/src/main/java/com/frontegg/android/services/FronteggAuthService.kt +++ b/android/src/main/java/com/frontegg/android/services/FronteggAuthService.kt @@ -201,6 +201,9 @@ class FronteggAuthService( @Volatile private var refreshingInProgress = false + private var refreshRetryCount = 0 + private val maxRetries = 2 + private val baseRetryDelayMs = 3000L override fun refreshTokenIfNeeded(): Boolean { // Prevent multiple simultaneous refresh attempts @@ -210,15 +213,33 @@ class FronteggAuthService( 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 } @@ -517,13 +538,13 @@ class FronteggAuthService( // 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 { @@ -546,8 +567,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 280ac180..b4639b8a 100644 --- a/android/src/main/java/com/frontegg/android/services/FronteggReconnector.kt +++ b/android/src/main/java/com/frontegg/android/services/FronteggReconnector.kt @@ -78,22 +78,22 @@ object FronteggReconnector { delay(50) } } catch (t: Throwable) { - when (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) { - when (t) { - } + Log.e(TAG, "Failed to refresh token on network reconnect", t) } } } From ad6e2ff56af3ec34b8fba7455daab115f03db5ca Mon Sep 17 00:00:00 2001 From: dianaKhortiuk-frontegg Date: Thu, 9 Oct 2025 14:00:28 +0200 Subject: [PATCH 2/2] Merge conflicts fixes --- .../java/com/frontegg/android/services/FronteggAuthService.kt | 3 --- 1 file changed, 3 deletions(-) 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 0bb7e28d..fe38c2dd 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()