diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt index 28ef45636..ac99ad747 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt @@ -46,6 +46,7 @@ import kotlinx.coroutines.launch * @param context Android context for DataStore access when saving credentials for linking * @param config The [AuthUIConfiguration] containing authentication settings * @param provider The [AuthProvider.Facebook] configuration with scopes and credential provider + * @param loginManagerProvider Provides logout operations to clear stale Facebook sessions * * @return A launcher function that starts the Facebook sign-in flow when invoked * @@ -56,6 +57,7 @@ internal fun FirebaseAuthUI.rememberSignInWithFacebookLauncher( context: Context, config: AuthUIConfiguration, provider: AuthProvider.Facebook, + loginManagerProvider: AuthProvider.Facebook.LoginManagerProvider = AuthProvider.Facebook.DefaultLoginManagerProvider(), ): () -> Unit { val coroutineScope = rememberCoroutineScope() val callbackManager = remember { CallbackManager.Factory.create() } @@ -114,6 +116,11 @@ internal fun FirebaseAuthUI.rememberSignInWithFacebookLauncher( updateAuthState( AuthState.Loading("Signing in with facebook...") ) + try { + (testLoginManagerProvider ?: loginManagerProvider).logOut() + } catch (e: Exception) { + Log.w("FacebookAuthProvider", "Failed to clear Facebook session before sign in", e) + } launcher.launch(provider.scopes) } } diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProviderFirebaseAuthUI.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProviderFirebaseAuthUI.kt index 1e48bae90..fe10118e6 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProviderFirebaseAuthUI.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProviderFirebaseAuthUI.kt @@ -16,9 +16,11 @@ package com.firebase.ui.auth.configuration.auth_provider import android.content.Context import android.net.Uri +import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.core.app.ApplicationProvider import com.facebook.AccessToken import com.facebook.FacebookException +import com.facebook.FacebookSdk import com.firebase.ui.auth.AuthException import com.firebase.ui.auth.AuthState import com.firebase.ui.auth.FirebaseAuthUI @@ -40,6 +42,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock @@ -61,6 +64,9 @@ import org.robolectric.annotation.Config @Config(manifest = Config.NONE) class FacebookAuthProviderFirebaseAuthUITest { + @get:Rule + val composeTestRule = createComposeRule() + @Mock private lateinit var mockFirebaseAuth: FirebaseAuth @@ -78,6 +84,11 @@ class FacebookAuthProviderFirebaseAuthUITest { applicationContext = ApplicationProvider.getApplicationContext() + FacebookSdk.setApplicationId("fake-app-id") + FacebookSdk.setClientToken("fake-client-token") + @Suppress("DEPRECATION") + FacebookSdk.sdkInitialize(applicationContext) + FirebaseApp.getApps(applicationContext).forEach { app -> app.delete() } @@ -102,6 +113,84 @@ class FacebookAuthProviderFirebaseAuthUITest { } } + @Test + @Config(manifest = Config.NONE, qualifiers = "night") + fun `rememberSignInWithFacebookLauncher - calls logOut before launching to clear stale token`() { + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + val provider = AuthProvider.Facebook() + val config = authUIConfiguration { + context = applicationContext + providers { + provider(provider) + } + } + + var launcher: (() -> Unit)? = null + + composeTestRule.setContent { + launcher = instance.rememberSignInWithFacebookLauncher( + context = applicationContext, + config = config, + provider = provider, + loginManagerProvider = mockFBAuthCredentialProvider, + ) + } + + composeTestRule.runOnIdle { + try { + launcher?.invoke() + } catch (_: Exception) { + // launcher.launch() may throw in test environment — that's expected + } + } + + verify(mockFBAuthCredentialProvider).logOut() + } + + @Test + @Config(manifest = Config.NONE, qualifiers = "night") + fun `rememberSignInWithFacebookLauncher - does not propagate stale token logout failure`() { + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + val provider = AuthProvider.Facebook() + val config = authUIConfiguration { + context = applicationContext + providers { + provider(provider) + } + } + val logoutException = RuntimeException("logout failed") + doAnswer { + throw logoutException + }.whenever(mockFBAuthCredentialProvider).logOut() + + var launcher: (() -> Unit)? = null + var thrownException: Exception? = null + + composeTestRule.setContent { + launcher = instance.rememberSignInWithFacebookLauncher( + context = applicationContext, + config = config, + provider = provider, + loginManagerProvider = mockFBAuthCredentialProvider, + ) + } + + composeTestRule.runOnIdle { + try { + launcher?.invoke() + } catch (e: Exception) { + thrownException = e + } + } + + var exceptionInChain: Throwable? = thrownException + while (exceptionInChain != null) { + assertThat(exceptionInChain).isNotEqualTo(logoutException) + exceptionInChain = exceptionInChain.cause + } + verify(mockFBAuthCredentialProvider).logOut() + } + @Test @Config(manifest = Config.NONE, qualifiers = "night") fun `signInWithFacebook - successful sign in signs user in and emits Success authState`() = runTest {