From 450aefe2b0161e3c14a7f1f43faa6dce2002d816 Mon Sep 17 00:00:00 2001 From: PierreVieira Date: Tue, 23 Jun 2026 23:48:12 -0300 Subject: [PATCH] feat(auth): add resetStatusOnBackground config to keep session status on background On Android, the lifecycle observer resets `Auth.sessionStatus` to `SessionStatus.Initializing` when the app moves to the background (`onStop`), before settling back to `Authenticated` on resume. Consumers that render UI from `sessionStatus` therefore observe a transient `Initializing` after every resume, causing flicker (e.g. an authenticated user briefly appears as loading). This is Android-only: the other platforms never reset to `Initializing` on background. Add an opt-out `resetStatusOnBackground` config flag (default `true`, no behavior change). When `false`, the last resolved status is kept while auto-refresh is still paused on background; `loadFromStorage()`/refresh on resume keep working as before. The reset is now done via an internal `Auth.resetSessionStatusForBackground()` helper, covered by tests in `AuthTest`. Closes #1338 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../github/jan/supabase/auth/setupPlatform.kt | 3 +- .../io/github/jan/supabase/auth/AuthConfig.kt | 14 +++++++++ .../io/github/jan/supabase/auth/Utils.kt | 6 ++++ Auth/src/commonTest/kotlin/AuthTest.kt | 30 +++++++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/Auth/src/androidMain/kotlin/io/github/jan/supabase/auth/setupPlatform.kt b/Auth/src/androidMain/kotlin/io/github/jan/supabase/auth/setupPlatform.kt index 6446b4c48..52298833d 100644 --- a/Auth/src/androidMain/kotlin/io/github/jan/supabase/auth/setupPlatform.kt +++ b/Auth/src/androidMain/kotlin/io/github/jan/supabase/auth/setupPlatform.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner import androidx.startup.Initializer import io.github.jan.supabase.annotations.SupabaseInternal -import io.github.jan.supabase.auth.status.SessionStatus import io.github.jan.supabase.logging.d import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -60,7 +59,7 @@ private fun addLifecycleCallbacks(auth: Auth) { auth.logger.d { "Cancelling auto refresh because app is switching to the background" } scope.launch { auth.stopAutoRefreshForCurrentSession() - auth.setSessionStatus(SessionStatus.Initializing) + auth.resetSessionStatusForBackground() } } } diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt index ec78ec500..0957db327 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt @@ -7,6 +7,7 @@ import io.github.jan.supabase.annotations.SupabaseExperimental import io.github.jan.supabase.annotations.SupabaseInternal import io.github.jan.supabase.auth.jwt.JwkCache import io.github.jan.supabase.auth.jwt.SharedJwkCache +import io.github.jan.supabase.auth.status.SessionStatus import io.github.jan.supabase.auth.user.UserSession import io.github.jan.supabase.plugins.CustomSerializationConfig import io.github.jan.supabase.plugins.MainConfig @@ -106,6 +107,19 @@ open class AuthConfigDefaults : MainConfig(), AuthDependentPluginConfig, CustomS */ var enableLifecycleCallbacks: Boolean = true + /** + * Whether to reset [Auth.sessionStatus] to [SessionStatus.Initializing] when the app moves to + * the background. + * + * When `true` (default), the status is reset on background and re-established on resume, which + * means consumers observe a transient [SessionStatus.Initializing] after every resume. When + * `false`, the last known status (e.g. [SessionStatus.Authenticated]) is kept while auto-refresh + * is paused, avoiding that transient state. + * + * Only has an effect when [enableLifecycleCallbacks] is `true`. Currently only supported on Android. + */ + var resetStatusOnBackground: Boolean = true + /** * The URL launcher used to open OAuth links in the system browser. */ diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/Utils.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/Utils.kt index 775d3bb24..5c654a2e9 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/Utils.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/Utils.kt @@ -23,6 +23,12 @@ internal fun Auth.initDone() { } } +internal fun Auth.resetSessionStatusForBackground() { + if(config.resetStatusOnBackground) { + setSessionStatus(SessionStatus.Initializing) + } +} + internal suspend fun Auth.tryToGetUser(accessToken: String) = try { retrieveUser(accessToken) } catch (e: Exception) { diff --git a/Auth/src/commonTest/kotlin/AuthTest.kt b/Auth/src/commonTest/kotlin/AuthTest.kt index 8cd98c8ee..0533b00a4 100644 --- a/Auth/src/commonTest/kotlin/AuthTest.kt +++ b/Auth/src/commonTest/kotlin/AuthTest.kt @@ -7,6 +7,7 @@ import io.github.jan.supabase.auth.auth import io.github.jan.supabase.auth.event.AuthEvent import io.github.jan.supabase.auth.minimalConfig import io.github.jan.supabase.auth.providers.Github +import io.github.jan.supabase.auth.resetSessionStatusForBackground import io.github.jan.supabase.auth.status.RefreshFailureCause import io.github.jan.supabase.auth.status.SessionSource import io.github.jan.supabase.auth.status.SessionStatus @@ -104,6 +105,35 @@ class AuthTest { } } + @Test + fun testBackgroundKeepsAuthenticatedStatusWhenResetDisabled() = runTest { + client = createMockedSupabaseClient(configuration = { + install(Auth) { + minimalConfig() + resetStatusOnBackground = false + } + }) + client.auth.awaitInitialization() + client.auth.importSession(userSession()) + assertIs(client.auth.sessionStatus.value) + client.auth.resetSessionStatusForBackground() + assertIs(client.auth.sessionStatus.value) + } + + @Test + fun testBackgroundResetsStatusToInitializingByDefault() = runTest { + client = createMockedSupabaseClient(configuration = { + install(Auth) { + minimalConfig() + } + }) + client.auth.awaitInitialization() + client.auth.importSession(userSession()) + assertIs(client.auth.sessionStatus.value) + client.auth.resetSessionStatusForBackground() + assertIs(client.auth.sessionStatus.value) + } + @Test fun testImportExpiredSession() { runTest {