Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ private fun AppAuthenticatedContent(
}
},
state = rememberTooltipState(
initialIsVisible = !configuration.isMfaEnabled
initialIsVisible = false
)
) {
Button(
Expand Down
54 changes: 39 additions & 15 deletions auth/src/main/java/com/firebase/ui/auth/FirebaseAuthUI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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))
}

Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -431,7 +431,7 @@ internal suspend fun FirebaseAuthUI.signInWithEmailAndPassword(
}
}

updateAuthState(AuthState.Idle)
updateAuthStateWithResult(result)
}
} catch (e: FirebaseAuthMultiFactorException) {
// MFA required - extract resolver and update state
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ internal suspend fun FirebaseAuthUI.signInWithProvider(
photoUrl = authResult.user?.photoUrl,
)
}
updateAuthState(AuthState.Idle)
Comment thread
demolaf marked this conversation as resolved.
return
}

Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ private fun AuthSuccessContent(
}
},
state = rememberTooltipState(
initialIsVisible = !configuration.isMfaEnabled
initialIsVisible = false
)
) {
Button(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AuthResult>()
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<AuthResult>()
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<AuthResult>()
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<AuthResult>()
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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,9 @@ class OAuthProviderFirebaseAuthUITest {
any<OAuthProvider>()
)

// 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))
}

// =============================================================================================
Expand Down
Loading