Skip to content

Commit d1466d1

Browse files
fix: ensure that when selecting phone or email, it routes straight to that screen (#2311)
1 parent 60edc2d commit d1466d1

6 files changed

Lines changed: 226 additions & 103 deletions

File tree

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

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.google.firebase.Firebase
2525
import com.google.firebase.FirebaseApp
2626
import com.google.firebase.auth.FirebaseAuth
2727
import com.google.firebase.auth.FirebaseAuth.AuthStateListener
28+
import com.google.firebase.auth.FirebaseAuth.IdTokenListener
2829
import com.google.firebase.auth.FirebaseUser
2930
import com.google.firebase.auth.auth
3031
import kotlinx.coroutines.CancellationException
@@ -255,29 +256,8 @@ class FirebaseAuthUI private constructor(
255256
fun authStateFlow(): Flow<AuthState> {
256257
// Create a flow from FirebaseAuth state listener
257258
val firebaseAuthFlow = callbackFlow {
258-
// Set initial state based on current auth state
259-
val initialState = auth.currentUser?.let { user ->
260-
// Check if email verification is required
261-
if (!user.isEmailVerified &&
262-
user.email != null &&
263-
user.providerData.any { it.providerId == "password" }
264-
) {
265-
AuthState.RequiresEmailVerification(
266-
user = user,
267-
email = user.email!!
268-
)
269-
} else {
270-
AuthState.Success(result = null, user = user, isNewUser = false)
271-
}
272-
} ?: AuthState.Idle
273-
274-
trySend(initialState)
275-
276-
// Create auth state listener
277-
val authStateListener = AuthStateListener { firebaseAuth ->
278-
val currentUser = firebaseAuth.currentUser
279-
val state = if (currentUser != null) {
280-
// Check if email verification is required
259+
fun buildState(currentUser: FirebaseUser?): AuthState {
260+
return if (currentUser != null) {
281261
if (!currentUser.isEmailVerified &&
282262
currentUser.email != null &&
283263
currentUser.providerData.any { it.providerId == "password" }
@@ -296,15 +276,31 @@ class FirebaseAuthUI private constructor(
296276
} else {
297277
AuthState.Idle
298278
}
299-
trySend(state)
279+
}
280+
281+
// Set initial state based on current auth state
282+
val initialState = buildState(auth.currentUser)
283+
284+
trySend(initialState)
285+
286+
// Create auth state listener
287+
val authStateListener = AuthStateListener { firebaseAuth ->
288+
trySend(buildState(firebaseAuth.currentUser))
289+
}
290+
291+
// AuthStateListener does not reliably fire for account linking, but IdTokenListener does.
292+
val idTokenListener = IdTokenListener { firebaseAuth ->
293+
trySend(buildState(firebaseAuth.currentUser))
300294
}
301295

302296
// Add listener
303297
auth.addAuthStateListener(authStateListener)
298+
auth.addIdTokenListener(idTokenListener)
304299

305300
// Remove listener when flow collection is cancelled
306301
awaitClose {
307302
auth.removeAuthStateListener(authStateListener)
303+
auth.removeIdTokenListener(idTokenListener)
308304
}
309305
}
310306

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

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import androidx.compose.ui.graphics.Color
5252
import androidx.compose.ui.platform.LocalContext
5353
import androidx.compose.ui.text.style.TextAlign
5454
import androidx.compose.ui.unit.dp
55+
import androidx.navigation.NavGraph.Companion.findStartDestination
5556
import androidx.navigation.compose.NavHost
5657
import androidx.navigation.compose.composable
5758
import androidx.navigation.compose.rememberNavController
@@ -127,6 +128,10 @@ fun FirebaseAuthScreen(
127128
val emailLinkFromDifferentDevice = remember { mutableStateOf<String?>(null) }
128129
val lastSignInPreference =
129130
remember { mutableStateOf<SignInPreferenceManager.SignInPreference?>(null) }
131+
val startRoute = remember(configuration.providers, configuration.isProviderChoiceAlwaysShown) {
132+
getStartRoute(configuration)
133+
}
134+
val skipsMethodPicker = startRoute != AuthRoute.MethodPicker
130135

131136
// Load last sign-in preference on launch
132137
LaunchedEffect(authState) {
@@ -238,7 +243,7 @@ fun FirebaseAuthScreen(
238243
) {
239244
NavHost(
240245
navController = navController,
241-
startDestination = AuthRoute.MethodPicker.route,
246+
startDestination = startRoute.route,
242247
enterTransition = configuration.transitions?.enterTransition ?: {
243248
fadeIn(animationSpec = tween(700))
244249
},
@@ -321,7 +326,9 @@ fun FirebaseAuthScreen(
321326
},
322327
onCancel = {
323328
pendingLinkingCredential.value = null
324-
if (!navController.popBackStack()) {
329+
if (skipsMethodPicker) {
330+
onSignInCancelled()
331+
} else if (!navController.popBackStack()) {
325332
navController.navigate(AuthRoute.MethodPicker.route) {
326333
popUpTo(AuthRoute.MethodPicker.route) { inclusive = true }
327334
launchSingleTop = true
@@ -341,7 +348,9 @@ fun FirebaseAuthScreen(
341348
onSignInFailure(exception)
342349
},
343350
onCancel = {
344-
if (!navController.popBackStack()) {
351+
if (skipsMethodPicker) {
352+
onSignInCancelled()
353+
} else if (!navController.popBackStack()) {
345354
navController.navigate(AuthRoute.MethodPicker.route) {
346355
popUpTo(AuthRoute.MethodPicker.route) { inclusive = true }
347356
launchSingleTop = true
@@ -537,7 +546,7 @@ fun FirebaseAuthScreen(
537546

538547
if (currentRoute != AuthRoute.Success.route) {
539548
navController.navigate(AuthRoute.Success.route) {
540-
popUpTo(AuthRoute.MethodPicker.route) { inclusive = true }
549+
popUpTo(navController.graph.findStartDestination().id) { inclusive = true }
541550
launchSingleTop = true
542551
}
543552
}
@@ -550,7 +559,7 @@ fun FirebaseAuthScreen(
550559
pendingLinkingCredential.value = null
551560
if (currentRoute != AuthRoute.Success.route) {
552561
navController.navigate(AuthRoute.Success.route) {
553-
popUpTo(AuthRoute.MethodPicker.route) { inclusive = true }
562+
popUpTo(navController.graph.findStartDestination().id) { inclusive = true }
554563
launchSingleTop = true
555564
}
556565
}
@@ -569,9 +578,9 @@ fun FirebaseAuthScreen(
569578
pendingResolver.value = null
570579
pendingLinkingCredential.value = null
571580
lastSuccessfulUserId.value = null
572-
if (currentRoute != AuthRoute.MethodPicker.route) {
573-
navController.navigate(AuthRoute.MethodPicker.route) {
574-
popUpTo(AuthRoute.MethodPicker.route) { inclusive = true }
581+
if (currentRoute != startRoute.route) {
582+
navController.navigate(startRoute.route) {
583+
popUpTo(navController.graph.findStartDestination().id) { inclusive = true }
575584
launchSingleTop = true
576585
}
577586
}
@@ -582,9 +591,9 @@ fun FirebaseAuthScreen(
582591
pendingResolver.value = null
583592
pendingLinkingCredential.value = null
584593
lastSuccessfulUserId.value = null
585-
if (currentRoute != AuthRoute.MethodPicker.route) {
586-
navController.navigate(AuthRoute.MethodPicker.route) {
587-
popUpTo(AuthRoute.MethodPicker.route) { inclusive = true }
594+
if (currentRoute != startRoute.route) {
595+
navController.navigate(startRoute.route) {
596+
popUpTo(navController.graph.findStartDestination().id) { inclusive = true }
588597
launchSingleTop = true
589598
}
590599
}
@@ -669,6 +678,18 @@ sealed class AuthRoute(val route: String) {
669678
object MfaChallenge : AuthRoute("auth_mfa_challenge")
670679
}
671680

681+
internal fun getStartRoute(configuration: AuthUIConfiguration): AuthRoute {
682+
if (configuration.isProviderChoiceAlwaysShown || configuration.providers.size != 1) {
683+
return AuthRoute.MethodPicker
684+
}
685+
686+
return when (configuration.providers.single()) {
687+
is AuthProvider.Email -> AuthRoute.Email
688+
is AuthProvider.Phone -> AuthRoute.Phone
689+
else -> AuthRoute.MethodPicker
690+
}
691+
}
692+
672693
data class AuthSuccessUiContext(
673694
val authUI: FirebaseAuthUI,
674695
val stringProvider: AuthUIStringProvider,
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package com.firebase.ui.auth.ui.screens
2+
3+
import android.content.Context
4+
import androidx.test.core.app.ApplicationProvider
5+
import com.firebase.ui.auth.configuration.authUIConfiguration
6+
import com.firebase.ui.auth.configuration.auth_provider.AuthProvider
7+
import com.google.common.truth.Truth.assertThat
8+
import org.junit.Before
9+
import org.junit.Test
10+
import org.junit.runner.RunWith
11+
import org.robolectric.RobolectricTestRunner
12+
import org.robolectric.annotation.Config
13+
14+
@RunWith(RobolectricTestRunner::class)
15+
@Config(manifest = Config.NONE)
16+
class FirebaseAuthScreenRouteTest {
17+
18+
private lateinit var applicationContext: Context
19+
20+
@Before
21+
fun setUp() {
22+
applicationContext = ApplicationProvider.getApplicationContext()
23+
}
24+
25+
@Test
26+
fun `single email provider starts at email route`() {
27+
val configuration = authUIConfiguration {
28+
context = applicationContext
29+
providers {
30+
provider(
31+
AuthProvider.Email(
32+
emailLinkActionCodeSettings = null,
33+
passwordValidationRules = emptyList()
34+
)
35+
)
36+
}
37+
}
38+
39+
assertThat(getStartRoute(configuration)).isEqualTo(AuthRoute.Email)
40+
}
41+
42+
@Test
43+
fun `single phone provider starts at phone route`() {
44+
val configuration = authUIConfiguration {
45+
context = applicationContext
46+
providers {
47+
provider(
48+
AuthProvider.Phone(
49+
defaultNumber = null,
50+
defaultCountryCode = null,
51+
allowedCountries = null
52+
)
53+
)
54+
}
55+
}
56+
57+
assertThat(getStartRoute(configuration)).isEqualTo(AuthRoute.Phone)
58+
}
59+
60+
@Test
61+
fun `single google provider starts at method picker`() {
62+
val configuration = authUIConfiguration {
63+
context = applicationContext
64+
providers {
65+
provider(
66+
AuthProvider.Google(
67+
scopes = emptyList(),
68+
serverClientId = "test-client-id"
69+
)
70+
)
71+
}
72+
}
73+
74+
assertThat(getStartRoute(configuration)).isEqualTo(AuthRoute.MethodPicker)
75+
}
76+
77+
@Test
78+
fun `single email provider shows picker when always shown is enabled`() {
79+
val configuration = authUIConfiguration {
80+
context = applicationContext
81+
providers {
82+
provider(
83+
AuthProvider.Email(
84+
emailLinkActionCodeSettings = null,
85+
passwordValidationRules = emptyList()
86+
)
87+
)
88+
}
89+
isProviderChoiceAlwaysShown = true
90+
}
91+
92+
assertThat(getStartRoute(configuration)).isEqualTo(AuthRoute.MethodPicker)
93+
}
94+
95+
@Test
96+
fun `multiple providers start at method picker`() {
97+
val configuration = authUIConfiguration {
98+
context = applicationContext
99+
providers {
100+
provider(
101+
AuthProvider.Email(
102+
emailLinkActionCodeSettings = null,
103+
passwordValidationRules = emptyList()
104+
)
105+
)
106+
provider(
107+
AuthProvider.Phone(
108+
defaultNumber = null,
109+
defaultCountryCode = null,
110+
allowedCountries = null
111+
)
112+
)
113+
}
114+
}
115+
116+
assertThat(getStartRoute(configuration)).isEqualTo(AuthRoute.MethodPicker)
117+
}
118+
}

e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/AnonymousAuthScreenTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ class AnonymousAuthScreenTest {
167167
@Test
168168
fun `anonymous upgrade enabled links new user sign-up and emits RequiresEmailVerification auth state`() {
169169
val name = "Anonymous Upgrade User"
170-
val email = "anonymousupgrade@example.com"
170+
val email = "anonymous-upgrade-${System.currentTimeMillis()}@example.com"
171171
val password = "Test@123"
172172
val configuration = authUIConfiguration {
173173
context = applicationContext

0 commit comments

Comments
 (0)