Skip to content

Commit 64c5e38

Browse files
committed
fix(auth): emit AuthResult on sign-in success and fix MFA tooltip auto-open (#2326)
* fix(auth): update AuthState to reflect success or idle based on user result * updates * updates * refactor: rename auth user state handler
1 parent 45923e0 commit 64c5e38

10 files changed

Lines changed: 206 additions & 37 deletions

File tree

app/src/main/java/com/firebaseui/android/demo/HighLevelApiDemoActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ private fun AppAuthenticatedContent(
247247
}
248248
},
249249
state = rememberTooltipState(
250-
initialIsVisible = !configuration.isMfaEnabled
250+
initialIsVisible = false
251251
)
252252
) {
253253
Button(

auth/src/main/java/com/firebase/ui/auth/FirebaseAuthUI.kt

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.firebase.ui.auth.configuration.auth_provider.signOutFromFacebook
2323
import com.firebase.ui.auth.configuration.auth_provider.signOutFromGoogle
2424
import com.google.firebase.Firebase
2525
import com.google.firebase.FirebaseApp
26+
import com.google.firebase.auth.AuthResult
2627
import com.google.firebase.auth.FirebaseAuth
2728
import com.google.firebase.auth.FirebaseAuth.AuthStateListener
2829
import com.google.firebase.auth.FirebaseAuth.IdTokenListener
@@ -258,21 +259,7 @@ class FirebaseAuthUI private constructor(
258259
val firebaseAuthFlow = callbackFlow {
259260
fun buildState(currentUser: FirebaseUser?): AuthState {
260261
return if (currentUser != null) {
261-
if (!currentUser.isEmailVerified &&
262-
currentUser.email != null &&
263-
currentUser.providerData.any { it.providerId == "password" }
264-
) {
265-
AuthState.RequiresEmailVerification(
266-
user = currentUser,
267-
email = currentUser.email!!
268-
)
269-
} else {
270-
AuthState.Success(
271-
result = null,
272-
user = currentUser,
273-
isNewUser = false
274-
)
275-
}
262+
handleAuthUserState(currentUser, result = null, isNewUser = false)
276263
} else {
277264
AuthState.Idle
278265
}
@@ -285,6 +272,17 @@ class FirebaseAuthUI private constructor(
285272

286273
// Create auth state listener
287274
val authStateListener = AuthStateListener { firebaseAuth ->
275+
// When user signs out, clear stale user-presence internal states so the combine
276+
// doesn't return Success/RequiresEmailVerification after the user is gone.
277+
if (firebaseAuth.currentUser == null) {
278+
val current = _authStateFlow.value
279+
if (current is AuthState.Success ||
280+
current is AuthState.RequiresEmailVerification ||
281+
current is AuthState.RequiresProfileCompletion
282+
) {
283+
_authStateFlow.value = AuthState.Idle
284+
}
285+
}
288286
trySend(buildState(firebaseAuth.currentUser))
289287
}
290288

@@ -325,6 +323,32 @@ class FirebaseAuthUI private constructor(
325323
_authStateFlow.value = state
326324
}
327325

326+
internal fun updateAuthStateWithResult(result: AuthResult?, defaultIsNewUser: Boolean = false) {
327+
val user = result?.user
328+
if (user != null) {
329+
updateAuthState(
330+
handleAuthUserState(
331+
user = user,
332+
result = result,
333+
isNewUser = result.additionalUserInfo?.isNewUser ?: defaultIsNewUser
334+
)
335+
)
336+
} else {
337+
updateAuthState(AuthState.Idle)
338+
}
339+
}
340+
341+
private fun handleAuthUserState(user: FirebaseUser, result: AuthResult?, isNewUser: Boolean): AuthState {
342+
return if (!user.isEmailVerified &&
343+
user.email != null &&
344+
user.providerData.any { it.providerId == "password" }
345+
) {
346+
AuthState.RequiresEmailVerification(user = user, email = user.email!!)
347+
} else {
348+
AuthState.Success(result = result, user = user, isNewUser = isNewUser)
349+
}
350+
}
351+
328352
/**
329353
* Signs out the current user and clears authentication state.
330354
*

auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProvider+FirebaseAuthUI.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ internal fun FirebaseAuthUI.rememberAnonymousSignInHandler(): () -> Unit {
110110
internal suspend fun FirebaseAuthUI.signInAnonymously() {
111111
try {
112112
updateAuthState(AuthState.Loading("Signing in anonymously..."))
113-
auth.signInAnonymously().await()
114-
updateAuthState(AuthState.Idle)
113+
val result = auth.signInAnonymously().await()
114+
updateAuthStateWithResult(result, defaultIsNewUser = true)
115115
} catch (e: CancellationException) {
116116
val cancelledException = AuthException.AuthCancelledException(
117117
message = "Sign in anonymously was cancelled",

auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ internal suspend fun FirebaseAuthUI.createOrLinkUserWithEmailAndPassword(
197197
}
198198
}
199199

200-
updateAuthState(AuthState.Idle)
200+
updateAuthStateWithResult(result, defaultIsNewUser = true)
201201
return result
202202
} catch (e: FirebaseAuthUserCollisionException) {
203203
// Account collision: email already exists
@@ -431,7 +431,7 @@ internal suspend fun FirebaseAuthUI.signInWithEmailAndPassword(
431431
}
432432
}
433433

434-
updateAuthState(AuthState.Idle)
434+
updateAuthStateWithResult(result)
435435
}
436436
} catch (e: FirebaseAuthMultiFactorException) {
437437
// MFA required - extract resolver and update state
@@ -557,7 +557,7 @@ internal suspend fun FirebaseAuthUI.signInAndLinkWithCredential(
557557
result?.user?.let {
558558
mergeProfile(auth, displayName, photoUrl)
559559
}
560-
updateAuthState(AuthState.Idle)
560+
updateAuthStateWithResult(result)
561561
}
562562
} catch (e: FirebaseAuthMultiFactorException) {
563563
// MFA required - extract resolver and update state
@@ -974,7 +974,7 @@ internal suspend fun FirebaseAuthUI.signInWithEmailLink(
974974
}
975975
// Clear DataStore after success
976976
persistenceManager.clear(context)
977-
updateAuthState(AuthState.Idle)
977+
updateAuthStateWithResult(result)
978978
return result
979979
} catch (e: CancellationException) {
980980
val cancelledException = AuthException.AuthCancelledException(

auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProvider+FirebaseAuthUI.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,6 @@ internal suspend fun FirebaseAuthUI.signInWithProvider(
162162
photoUrl = authResult.user?.photoUrl,
163163
)
164164
}
165-
updateAuthState(AuthState.Idle)
166165
return
167166
}
168167

@@ -195,8 +194,7 @@ internal suspend fun FirebaseAuthUI.signInWithProvider(
195194
android.util.Log.w("OAuthProvider", "Failed to save sign-in preference", e)
196195
}
197196

198-
// Just update state to Idle
199-
updateAuthState(AuthState.Idle)
197+
updateAuthStateWithResult(authResult)
200198
} else {
201199
throw AuthException.UnknownException(
202200
message = "OAuth sign-in did not return a valid credential"

auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,7 @@ private fun AuthSuccessContent(
780780
}
781781
},
782782
state = rememberTooltipState(
783-
initialIsVisible = !configuration.isMfaEnabled
783+
initialIsVisible = false
784784
)
785785
) {
786786
Button(

auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProviderFirebaseAuthUITest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ class AnonymousAuthProviderFirebaseAuthUITest {
110110

111111
verify(mockFirebaseAuth).signInAnonymously()
112112

113-
val finalState = instance.authStateFlow().first { it is AuthState.Idle }
114-
assertThat(finalState).isInstanceOf(AuthState.Idle::class.java)
113+
val finalState = instance.authStateFlow().first { it is AuthState.Success }
114+
assertThat(finalState).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = true))
115115
}
116116

117117
@Test

auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProviderFirebaseAuthUITest.kt

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,4 +1457,151 @@ class EmailAuthProviderFirebaseAuthUITest {
14571457
assertThat(e).isNotNull()
14581458
}
14591459
}
1460+
1461+
@Test
1462+
fun `signInWithEmailAndPassword - emits AuthState Success with non-null result`() = runTest {
1463+
val mockUser = mock(FirebaseUser::class.java)
1464+
val mockAuthResult = mock(AuthResult::class.java)
1465+
`when`(mockAuthResult.user).thenReturn(mockUser)
1466+
val taskCompletionSource = TaskCompletionSource<AuthResult>()
1467+
taskCompletionSource.setResult(mockAuthResult)
1468+
`when`(mockFirebaseAuth.signInWithEmailAndPassword("test@example.com", "Pass@123"))
1469+
.thenReturn(taskCompletionSource.task)
1470+
1471+
val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth)
1472+
val emailProvider = AuthProvider.Email(
1473+
emailLinkActionCodeSettings = null,
1474+
passwordValidationRules = emptyList()
1475+
)
1476+
val config = authUIConfiguration {
1477+
context = applicationContext
1478+
providers { provider(emailProvider) }
1479+
}
1480+
1481+
instance.signInWithEmailAndPassword(
1482+
context = applicationContext,
1483+
config = config,
1484+
email = "test@example.com",
1485+
password = "Pass@123"
1486+
)
1487+
1488+
val state = instance.authStateFlow().first { it !is AuthState.Loading }
1489+
assertThat(state).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = false))
1490+
}
1491+
1492+
@Test
1493+
fun `signInAndLinkWithCredential - emits AuthState Success with non-null result`() = runTest {
1494+
val credential = GoogleAuthProvider.getCredential("google-id-token", null)
1495+
val mockUser = mock(FirebaseUser::class.java)
1496+
val mockAuthResult = mock(AuthResult::class.java)
1497+
`when`(mockAuthResult.user).thenReturn(mockUser)
1498+
val taskCompletionSource = TaskCompletionSource<AuthResult>()
1499+
taskCompletionSource.setResult(mockAuthResult)
1500+
`when`(mockFirebaseAuth.signInWithCredential(credential))
1501+
.thenReturn(taskCompletionSource.task)
1502+
1503+
val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth)
1504+
val emailProvider = AuthProvider.Email(
1505+
emailLinkActionCodeSettings = null,
1506+
passwordValidationRules = emptyList()
1507+
)
1508+
val config = authUIConfiguration {
1509+
context = applicationContext
1510+
providers { provider(emailProvider) }
1511+
}
1512+
1513+
instance.signInAndLinkWithCredential(config = config, credential = credential)
1514+
1515+
val state = instance.authStateFlow().first { it !is AuthState.Loading }
1516+
assertThat(state).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = false))
1517+
}
1518+
1519+
@Test
1520+
fun `createOrLinkUserWithEmailAndPassword - emits AuthState Success with non-null result`() = runTest {
1521+
val mockUser = mock(FirebaseUser::class.java)
1522+
val mockAuthResult = mock(AuthResult::class.java)
1523+
`when`(mockAuthResult.user).thenReturn(mockUser)
1524+
val taskCompletionSource = TaskCompletionSource<AuthResult>()
1525+
taskCompletionSource.setResult(mockAuthResult)
1526+
`when`(mockFirebaseAuth.createUserWithEmailAndPassword("new@example.com", "Pass@123"))
1527+
.thenReturn(taskCompletionSource.task)
1528+
1529+
val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth)
1530+
val emailProvider = AuthProvider.Email(
1531+
emailLinkActionCodeSettings = null,
1532+
passwordValidationRules = emptyList()
1533+
)
1534+
val config = authUIConfiguration {
1535+
context = applicationContext
1536+
providers { provider(emailProvider) }
1537+
}
1538+
1539+
instance.createOrLinkUserWithEmailAndPassword(
1540+
context = applicationContext,
1541+
config = config,
1542+
provider = emailProvider,
1543+
name = null,
1544+
email = "new@example.com",
1545+
password = "Pass@123"
1546+
)
1547+
1548+
val state = instance.authStateFlow().first { it !is AuthState.Loading }
1549+
assertThat(state).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = true))
1550+
}
1551+
1552+
@Test
1553+
fun `signInWithEmailLink - emits AuthState Success with non-null result`() = runTest {
1554+
val mockUser = mock(FirebaseUser::class.java)
1555+
`when`(mockUser.email).thenReturn("test@example.com")
1556+
`when`(mockUser.isAnonymous).thenReturn(false)
1557+
val mockAuthResult = mock(AuthResult::class.java)
1558+
`when`(mockAuthResult.user).thenReturn(mockUser)
1559+
1560+
`when`(mockFirebaseAuth.currentUser).thenReturn(null)
1561+
`when`(mockFirebaseAuth.isSignInWithEmailLink(anyString())).thenReturn(true)
1562+
1563+
val taskCompletionSource = TaskCompletionSource<AuthResult>()
1564+
taskCompletionSource.setResult(mockAuthResult)
1565+
`when`(mockFirebaseAuth.signInWithCredential(any())).thenReturn(taskCompletionSource.task)
1566+
1567+
val provider = AuthProvider.Email(
1568+
isEmailLinkSignInEnabled = true,
1569+
emailLinkActionCodeSettings = ActionCodeSettings.newBuilder()
1570+
.setUrl("https://example.com")
1571+
.setHandleCodeInApp(true)
1572+
.build(),
1573+
passwordValidationRules = emptyList()
1574+
)
1575+
val config = authUIConfiguration {
1576+
context = applicationContext
1577+
providers { provider(provider) }
1578+
}
1579+
1580+
val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth)
1581+
1582+
val mockPersistence = MockPersistenceManager()
1583+
mockPersistence.setSessionRecord(
1584+
EmailLinkPersistenceManager.SessionRecord(
1585+
sessionId = "session123",
1586+
email = "test@example.com",
1587+
anonymousUserId = null,
1588+
credentialForLinking = null
1589+
)
1590+
)
1591+
1592+
val emailLink =
1593+
"https://example.com/__/auth/action?apiKey=key&mode=signIn&oobCode=code&continueUrl=https://example.com?ui_sid=session123"
1594+
1595+
instance.signInWithEmailLink(
1596+
context = applicationContext,
1597+
config = config,
1598+
provider = provider,
1599+
email = "test@example.com",
1600+
emailLink = emailLink,
1601+
persistenceManager = mockPersistence
1602+
)
1603+
1604+
val state = instance.authStateFlow().first { it !is AuthState.Loading }
1605+
assertThat(state).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = false))
1606+
}
14601607
}

auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProviderFirebaseAuthUITest.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,9 @@ class GoogleAuthProviderFirebaseAuthUITest {
180180
// Verify Firebase sign-in was called
181181
verify(mockFirebaseAuth).signInWithCredential(mockCredential)
182182

183-
// Verify state is Idle after success
184-
val finalState = instance.authStateFlow().first()
185-
assertThat(finalState).isEqualTo(AuthState.Idle)
183+
// Verify state is Success (with the real AuthResult) after sign-in
184+
val finalState = instance.authStateFlow().first { it !is AuthState.Loading }
185+
assertThat(finalState).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = false))
186186
}
187187

188188
@Test
@@ -853,8 +853,8 @@ class GoogleAuthProviderFirebaseAuthUITest {
853853
credentialManagerProvider = mockCredentialManagerProvider
854854
)
855855

856-
// Verify final state
857-
val finalState = instance.authStateFlow().first()
858-
assertThat(finalState).isEqualTo(AuthState.Idle)
856+
// Verify final state is Success (with the real AuthResult)
857+
val finalState = instance.authStateFlow().first { it !is AuthState.Loading }
858+
assertThat(finalState).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = false))
859859
}
860860
}

auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProviderFirebaseAuthUITest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ class OAuthProviderFirebaseAuthUITest {
143143
any<OAuthProvider>()
144144
)
145145

146-
// Verify state is Idle after success
147-
val finalState = instance.authStateFlow().first()
148-
assertThat(finalState).isEqualTo(AuthState.Idle)
146+
// Verify state is Success after sign-in
147+
val finalState = instance.authStateFlow().first { it !is AuthState.Loading }
148+
assertThat(finalState).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = false))
149149
}
150150

151151
// =============================================================================================

0 commit comments

Comments
 (0)