diff --git a/app/src/main/java/com/firebaseui/android/demo/HighLevelApiDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/HighLevelApiDemoActivity.kt index ff008abcf..a4c708a6b 100644 --- a/app/src/main/java/com/firebaseui/android/demo/HighLevelApiDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/HighLevelApiDemoActivity.kt @@ -247,7 +247,7 @@ private fun AppAuthenticatedContent( } }, state = rememberTooltipState( - initialIsVisible = !configuration.isMfaEnabled + initialIsVisible = false ) ) { Button( diff --git a/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthUI.kt index af1daa3c0..827c37abd 100644 --- a/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthUI.kt @@ -23,6 +23,7 @@ import com.firebase.ui.auth.configuration.auth_provider.signOutFromFacebook import com.firebase.ui.auth.configuration.auth_provider.signOutFromGoogle import com.google.firebase.Firebase import com.google.firebase.FirebaseApp +import com.google.firebase.auth.AuthResult import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseAuth.AuthStateListener import com.google.firebase.auth.FirebaseAuth.IdTokenListener @@ -258,21 +259,7 @@ class FirebaseAuthUI private constructor( val firebaseAuthFlow = callbackFlow { fun buildState(currentUser: FirebaseUser?): AuthState { return if (currentUser != null) { - if (!currentUser.isEmailVerified && - currentUser.email != null && - currentUser.providerData.any { it.providerId == "password" } - ) { - AuthState.RequiresEmailVerification( - user = currentUser, - email = currentUser.email!! - ) - } else { - AuthState.Success( - result = null, - user = currentUser, - isNewUser = false - ) - } + handleAuthUserState(currentUser, result = null, isNewUser = false) } else { AuthState.Idle } @@ -285,6 +272,17 @@ class FirebaseAuthUI private constructor( // Create auth state listener val authStateListener = AuthStateListener { firebaseAuth -> + // When user signs out, clear stale user-presence internal states so the combine + // doesn't return Success/RequiresEmailVerification after the user is gone. + if (firebaseAuth.currentUser == null) { + val current = _authStateFlow.value + if (current is AuthState.Success || + current is AuthState.RequiresEmailVerification || + current is AuthState.RequiresProfileCompletion + ) { + _authStateFlow.value = AuthState.Idle + } + } trySend(buildState(firebaseAuth.currentUser)) } @@ -325,6 +323,32 @@ class FirebaseAuthUI private constructor( _authStateFlow.value = state } + internal fun updateAuthStateWithResult(result: AuthResult?, defaultIsNewUser: Boolean = false) { + val user = result?.user + if (user != null) { + updateAuthState( + handleAuthUserState( + user = user, + result = result, + isNewUser = result.additionalUserInfo?.isNewUser ?: defaultIsNewUser + ) + ) + } else { + updateAuthState(AuthState.Idle) + } + } + + private fun handleAuthUserState(user: FirebaseUser, result: AuthResult?, isNewUser: Boolean): AuthState { + return if (!user.isEmailVerified && + user.email != null && + user.providerData.any { it.providerId == "password" } + ) { + AuthState.RequiresEmailVerification(user = user, email = user.email!!) + } else { + AuthState.Success(result = result, user = user, isNewUser = isNewUser) + } + } + /** * Signs out the current user and clears authentication state. * diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProvider+FirebaseAuthUI.kt index 746537390..baf9cef82 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProvider+FirebaseAuthUI.kt @@ -110,8 +110,8 @@ internal fun FirebaseAuthUI.rememberAnonymousSignInHandler(): () -> Unit { internal suspend fun FirebaseAuthUI.signInAnonymously() { try { updateAuthState(AuthState.Loading("Signing in anonymously...")) - auth.signInAnonymously().await() - updateAuthState(AuthState.Idle) + val result = auth.signInAnonymously().await() + updateAuthStateWithResult(result, defaultIsNewUser = true) } catch (e: CancellationException) { val cancelledException = AuthException.AuthCancelledException( message = "Sign in anonymously was cancelled", diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt index 61b50c613..670b451bc 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt @@ -197,7 +197,7 @@ internal suspend fun FirebaseAuthUI.createOrLinkUserWithEmailAndPassword( } } - updateAuthState(AuthState.Idle) + updateAuthStateWithResult(result, defaultIsNewUser = true) return result } catch (e: FirebaseAuthUserCollisionException) { // Account collision: email already exists @@ -431,7 +431,7 @@ internal suspend fun FirebaseAuthUI.signInWithEmailAndPassword( } } - updateAuthState(AuthState.Idle) + updateAuthStateWithResult(result) } } catch (e: FirebaseAuthMultiFactorException) { // MFA required - extract resolver and update state @@ -557,7 +557,7 @@ internal suspend fun FirebaseAuthUI.signInAndLinkWithCredential( result?.user?.let { mergeProfile(auth, displayName, photoUrl) } - updateAuthState(AuthState.Idle) + updateAuthStateWithResult(result) } } catch (e: FirebaseAuthMultiFactorException) { // MFA required - extract resolver and update state @@ -974,7 +974,7 @@ internal suspend fun FirebaseAuthUI.signInWithEmailLink( } // Clear DataStore after success persistenceManager.clear(context) - updateAuthState(AuthState.Idle) + updateAuthStateWithResult(result) return result } catch (e: CancellationException) { val cancelledException = AuthException.AuthCancelledException( diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProvider+FirebaseAuthUI.kt index 4053684d6..e2d141400 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProvider+FirebaseAuthUI.kt @@ -162,7 +162,6 @@ internal suspend fun FirebaseAuthUI.signInWithProvider( photoUrl = authResult.user?.photoUrl, ) } - updateAuthState(AuthState.Idle) return } @@ -195,8 +194,7 @@ internal suspend fun FirebaseAuthUI.signInWithProvider( android.util.Log.w("OAuthProvider", "Failed to save sign-in preference", e) } - // Just update state to Idle - updateAuthState(AuthState.Idle) + updateAuthStateWithResult(authResult) } else { throw AuthException.UnknownException( message = "OAuth sign-in did not return a valid credential" diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt index 7a67b977e..9a7e3911a 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt @@ -780,7 +780,7 @@ private fun AuthSuccessContent( } }, state = rememberTooltipState( - initialIsVisible = !configuration.isMfaEnabled + initialIsVisible = false ) ) { Button( diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProviderFirebaseAuthUITest.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProviderFirebaseAuthUITest.kt index 53f465b9a..7ddfb3ac8 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProviderFirebaseAuthUITest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProviderFirebaseAuthUITest.kt @@ -110,8 +110,8 @@ class AnonymousAuthProviderFirebaseAuthUITest { verify(mockFirebaseAuth).signInAnonymously() - val finalState = instance.authStateFlow().first { it is AuthState.Idle } - assertThat(finalState).isInstanceOf(AuthState.Idle::class.java) + val finalState = instance.authStateFlow().first { it is AuthState.Success } + assertThat(finalState).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = true)) } @Test diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProviderFirebaseAuthUITest.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProviderFirebaseAuthUITest.kt index dc027e3dc..d42bbab5f 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProviderFirebaseAuthUITest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProviderFirebaseAuthUITest.kt @@ -1457,4 +1457,151 @@ class EmailAuthProviderFirebaseAuthUITest { assertThat(e).isNotNull() } } + + @Test + fun `signInWithEmailAndPassword - emits AuthState Success with non-null result`() = runTest { + val mockUser = mock(FirebaseUser::class.java) + val mockAuthResult = mock(AuthResult::class.java) + `when`(mockAuthResult.user).thenReturn(mockUser) + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setResult(mockAuthResult) + `when`(mockFirebaseAuth.signInWithEmailAndPassword("test@example.com", "Pass@123")) + .thenReturn(taskCompletionSource.task) + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + val emailProvider = AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + val config = authUIConfiguration { + context = applicationContext + providers { provider(emailProvider) } + } + + instance.signInWithEmailAndPassword( + context = applicationContext, + config = config, + email = "test@example.com", + password = "Pass@123" + ) + + val state = instance.authStateFlow().first { it !is AuthState.Loading } + assertThat(state).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = false)) + } + + @Test + fun `signInAndLinkWithCredential - emits AuthState Success with non-null result`() = runTest { + val credential = GoogleAuthProvider.getCredential("google-id-token", null) + val mockUser = mock(FirebaseUser::class.java) + val mockAuthResult = mock(AuthResult::class.java) + `when`(mockAuthResult.user).thenReturn(mockUser) + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setResult(mockAuthResult) + `when`(mockFirebaseAuth.signInWithCredential(credential)) + .thenReturn(taskCompletionSource.task) + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + val emailProvider = AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + val config = authUIConfiguration { + context = applicationContext + providers { provider(emailProvider) } + } + + instance.signInAndLinkWithCredential(config = config, credential = credential) + + val state = instance.authStateFlow().first { it !is AuthState.Loading } + assertThat(state).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = false)) + } + + @Test + fun `createOrLinkUserWithEmailAndPassword - emits AuthState Success with non-null result`() = runTest { + val mockUser = mock(FirebaseUser::class.java) + val mockAuthResult = mock(AuthResult::class.java) + `when`(mockAuthResult.user).thenReturn(mockUser) + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setResult(mockAuthResult) + `when`(mockFirebaseAuth.createUserWithEmailAndPassword("new@example.com", "Pass@123")) + .thenReturn(taskCompletionSource.task) + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + val emailProvider = AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + val config = authUIConfiguration { + context = applicationContext + providers { provider(emailProvider) } + } + + instance.createOrLinkUserWithEmailAndPassword( + context = applicationContext, + config = config, + provider = emailProvider, + name = null, + email = "new@example.com", + password = "Pass@123" + ) + + val state = instance.authStateFlow().first { it !is AuthState.Loading } + assertThat(state).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = true)) + } + + @Test + fun `signInWithEmailLink - emits AuthState Success with non-null result`() = runTest { + val mockUser = mock(FirebaseUser::class.java) + `when`(mockUser.email).thenReturn("test@example.com") + `when`(mockUser.isAnonymous).thenReturn(false) + val mockAuthResult = mock(AuthResult::class.java) + `when`(mockAuthResult.user).thenReturn(mockUser) + + `when`(mockFirebaseAuth.currentUser).thenReturn(null) + `when`(mockFirebaseAuth.isSignInWithEmailLink(anyString())).thenReturn(true) + + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setResult(mockAuthResult) + `when`(mockFirebaseAuth.signInWithCredential(any())).thenReturn(taskCompletionSource.task) + + val provider = AuthProvider.Email( + isEmailLinkSignInEnabled = true, + emailLinkActionCodeSettings = ActionCodeSettings.newBuilder() + .setUrl("https://example.com") + .setHandleCodeInApp(true) + .build(), + passwordValidationRules = emptyList() + ) + val config = authUIConfiguration { + context = applicationContext + providers { provider(provider) } + } + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + + val mockPersistence = MockPersistenceManager() + mockPersistence.setSessionRecord( + EmailLinkPersistenceManager.SessionRecord( + sessionId = "session123", + email = "test@example.com", + anonymousUserId = null, + credentialForLinking = null + ) + ) + + val emailLink = + "https://example.com/__/auth/action?apiKey=key&mode=signIn&oobCode=code&continueUrl=https://example.com?ui_sid=session123" + + instance.signInWithEmailLink( + context = applicationContext, + config = config, + provider = provider, + email = "test@example.com", + emailLink = emailLink, + persistenceManager = mockPersistence + ) + + val state = instance.authStateFlow().first { it !is AuthState.Loading } + assertThat(state).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = false)) + } } diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProviderFirebaseAuthUITest.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProviderFirebaseAuthUITest.kt index 2fd855c37..185483b9c 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProviderFirebaseAuthUITest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProviderFirebaseAuthUITest.kt @@ -180,9 +180,9 @@ class GoogleAuthProviderFirebaseAuthUITest { // Verify Firebase sign-in was called verify(mockFirebaseAuth).signInWithCredential(mockCredential) - // Verify state is Idle after success - val finalState = instance.authStateFlow().first() - assertThat(finalState).isEqualTo(AuthState.Idle) + // Verify state is Success (with the real AuthResult) after sign-in + val finalState = instance.authStateFlow().first { it !is AuthState.Loading } + assertThat(finalState).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = false)) } @Test @@ -853,8 +853,8 @@ class GoogleAuthProviderFirebaseAuthUITest { credentialManagerProvider = mockCredentialManagerProvider ) - // Verify final state - val finalState = instance.authStateFlow().first() - assertThat(finalState).isEqualTo(AuthState.Idle) + // Verify final state is Success (with the real AuthResult) + val finalState = instance.authStateFlow().first { it !is AuthState.Loading } + assertThat(finalState).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = false)) } } diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProviderFirebaseAuthUITest.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProviderFirebaseAuthUITest.kt index 1d027d9ea..672e0c11d 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProviderFirebaseAuthUITest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProviderFirebaseAuthUITest.kt @@ -143,9 +143,9 @@ class OAuthProviderFirebaseAuthUITest { any() ) - // Verify state is Idle after success - val finalState = instance.authStateFlow().first() - assertThat(finalState).isEqualTo(AuthState.Idle) + // Verify state is Success after sign-in + val finalState = instance.authStateFlow().first { it !is AuthState.Loading } + assertThat(finalState).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = false)) } // =============================================================================================