Skip to content

Commit 473610f

Browse files
committed
e2e tests anonymous sign in and auto upgrade when enabled
1 parent 1a2d438 commit 473610f

File tree

11 files changed

+558
-168
lines changed

11 files changed

+558
-168
lines changed

auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUIConfiguration.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvidersBui
2121
import com.firebase.ui.auth.compose.configuration.auth_provider.Provider
2222
import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider
2323
import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider
24+
import com.firebase.ui.auth.compose.configuration.theme.AuthUIAsset
2425
import com.firebase.ui.auth.compose.configuration.theme.AuthUITheme
2526
import com.google.firebase.auth.ActionCodeSettings
2627
import java.util.Locale
@@ -43,7 +44,7 @@ class AuthUIConfigurationBuilder {
4344
var isAnonymousUpgradeEnabled: Boolean = false
4445
var tosUrl: String? = null
4546
var privacyPolicyUrl: String? = null
46-
var logo: ImageVector? = null
47+
var logo: AuthUIAsset? = null
4748
var passwordResetActionCodeSettings: ActionCodeSettings? = null
4849
var isNewEmailAccountsAllowed: Boolean = true
4950
var isDisplayNameRequired: Boolean = true
@@ -171,7 +172,7 @@ class AuthUIConfiguration(
171172
/**
172173
* The logo to display on the authentication screens.
173174
*/
174-
val logo: ImageVector? = null,
175+
val logo: AuthUIAsset? = null,
175176

176177
/**
177178
* Configuration for sending email reset link.

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,39 @@
11
package com.firebase.ui.auth.compose.configuration.auth_provider
22

3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.remember
5+
import androidx.compose.runtime.rememberCoroutineScope
36
import com.firebase.ui.auth.compose.AuthException
47
import com.firebase.ui.auth.compose.AuthState
58
import com.firebase.ui.auth.compose.FirebaseAuthUI
69
import kotlinx.coroutines.CancellationException
10+
import kotlinx.coroutines.launch
711
import kotlinx.coroutines.tasks.await
812

13+
/**
14+
* Creates a remembered launcher function for anonymous sign-in.
15+
*
16+
* @return A launcher function that starts the anonymous sign-in flow when invoked
17+
*
18+
* @see signInAnonymously
19+
* @see createOrLinkUserWithEmailAndPassword for upgrading anonymous accounts
20+
*/
21+
@Composable
22+
internal fun FirebaseAuthUI.rememberAnonymousSignInHandler(): () -> Unit {
23+
val coroutineScope = rememberCoroutineScope()
24+
return remember(this) {
25+
{
26+
coroutineScope.launch {
27+
try {
28+
signInAnonymously()
29+
} catch (e: Exception) {
30+
// Error already handled via auth state flow in signInAnonymously()
31+
// No additional action needed - ErrorRecoveryDialog will show automatically
32+
}
33+
}
34+
}
35+
}
36+
}
937

1038
/**
1139
* Signs in a user anonymously with Firebase Authentication.
@@ -75,7 +103,7 @@ import kotlinx.coroutines.tasks.await
75103
* @see createOrLinkUserWithEmailAndPassword for email/password upgrade
76104
* @see signInWithPhoneAuthCredential for phone authentication upgrade
77105
*/
78-
suspend fun FirebaseAuthUI.signInAnonymously() {
106+
internal suspend fun FirebaseAuthUI.signInAnonymously() {
79107
try {
80108
updateAuthState(AuthState.Loading("Signing in anonymously..."))
81109
auth.signInAnonymously().await()

auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/AuthProvider.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,8 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
285285
* An interface to wrap the static `EmailAuthProvider.getCredential` method to make it testable.
286286
* @suppress
287287
*/
288-
internal interface CredentialProvider {
288+
// TODO(demolaf): make this internal
289+
interface CredentialProvider {
289290
fun getCredential(email: String, password: String): AuthCredential
290291
}
291292

@@ -635,8 +636,7 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
635636
* An interface to wrap the static `FacebookAuthProvider.getCredential` method to make it testable.
636637
* @suppress
637638
*/
638-
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
639-
interface CredentialProvider {
639+
internal interface CredentialProvider {
640640
fun getCredential(token: String): AuthCredential
641641
}
642642

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ import kotlinx.coroutines.tasks.await
108108
* }
109109
* ```
110110
*/
111-
internal suspend fun FirebaseAuthUI.createOrLinkUserWithEmailAndPassword(
111+
// TODO(demolaf): make this internal
112+
suspend fun FirebaseAuthUI.createOrLinkUserWithEmailAndPassword(
112113
context: Context,
113114
config: AuthUIConfiguration,
114115
provider: AuthProvider.Email,
@@ -684,8 +685,7 @@ internal suspend fun FirebaseAuthUI.sendSignInLinkToEmail(
684685
*
685686
* @see sendSignInLinkToEmail for sending the initial email link
686687
*/
687-
// TODO(demolaf: make this internal when done testing email link sign in with composeapp
688-
suspend fun FirebaseAuthUI.signInWithEmailLink(
688+
internal suspend fun FirebaseAuthUI.signInWithEmailLink(
689689
context: Context,
690690
config: AuthUIConfiguration,
691691
provider: AuthProvider.Email,

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,8 @@ import kotlinx.coroutines.launch
5050
*
5151
* @see signInWithFacebook
5252
*/
53-
// TODO(demolaf): make this internal after testing with compose app
5453
@Composable
55-
fun FirebaseAuthUI.rememberSignInWithFacebookLauncher(
54+
internal fun FirebaseAuthUI.rememberSignInWithFacebookLauncher(
5655
context: Context,
5756
config: AuthUIConfiguration,
5857
provider: AuthProvider.Facebook,

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

Lines changed: 106 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import androidx.compose.material3.MaterialTheme
2828
import androidx.compose.material3.Scaffold
2929
import androidx.compose.material3.Surface
3030
import androidx.compose.material3.Text
31-
import androidx.compose.material3.TextButton
3231
import androidx.compose.runtime.Composable
3332
import androidx.compose.runtime.LaunchedEffect
3433
import androidx.compose.runtime.collectAsState
@@ -50,10 +49,10 @@ import com.firebase.ui.auth.compose.FirebaseAuthUI
5049
import com.firebase.ui.auth.compose.configuration.AuthUIConfiguration
5150
import com.firebase.ui.auth.compose.configuration.MfaConfiguration
5251
import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvider
52+
import com.firebase.ui.auth.compose.configuration.auth_provider.rememberAnonymousSignInHandler
5353
import com.firebase.ui.auth.compose.configuration.auth_provider.rememberSignInWithFacebookLauncher
5454
import com.firebase.ui.auth.compose.configuration.auth_provider.signInWithEmailLink
5555
import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider
56-
import com.firebase.ui.auth.compose.configuration.theme.AuthUIAsset
5756
import com.firebase.ui.auth.compose.ui.components.ErrorRecoveryDialog
5857
import com.firebase.ui.auth.compose.ui.method_picker.AuthMethodPicker
5958
import com.firebase.ui.auth.compose.ui.screens.phone.PhoneAuthScreen
@@ -97,9 +96,14 @@ fun FirebaseAuthScreen(
9796
val pendingLinkingCredential = remember { mutableStateOf<AuthCredential?>(null) }
9897
val pendingResolver = remember { mutableStateOf<MultiFactorResolver?>(null) }
9998

99+
val anonymousProvider = configuration.providers.filterIsInstance<AuthProvider.Anonymous>().firstOrNull()
100100
val emailProvider = configuration.providers.filterIsInstance<AuthProvider.Email>().firstOrNull()
101101
val facebookProvider = configuration.providers.filterIsInstance<AuthProvider.Facebook>().firstOrNull()
102-
val logoAsset = configuration.logo?.let { AuthUIAsset.Vector(it) }
102+
val logoAsset = configuration.logo
103+
104+
val onSignInAnonymously = anonymousProvider?.let {
105+
authUI.rememberAnonymousSignInHandler()
106+
}
103107

104108
val onSignInWithFacebook = facebookProvider?.let {
105109
authUI.rememberSignInWithFacebookLauncher(
@@ -109,103 +113,6 @@ fun FirebaseAuthScreen(
109113
)
110114
}
111115

112-
// Handle email link sign-in (deep links)
113-
LaunchedEffect(emailLink) {
114-
if (emailLink != null && emailProvider != null) {
115-
try {
116-
EmailLinkPersistenceManager.retrieveSessionRecord(context)?.email?.let { email ->
117-
authUI.signInWithEmailLink(
118-
context = context,
119-
config = configuration,
120-
provider = emailProvider,
121-
email = email,
122-
emailLink = emailLink
123-
)
124-
}
125-
} catch (e: Exception) {
126-
Log.e("FirebaseAuthScreen", "Failed to complete email link sign-in", e)
127-
}
128-
129-
if (navController.currentBackStackEntry?.destination?.route != AuthRoute.Email.route) {
130-
navController.navigate(AuthRoute.Email.route)
131-
}
132-
}
133-
}
134-
135-
// Synchronise auth state changes with navigation stack.
136-
LaunchedEffect(authState) {
137-
val state = authState
138-
val currentRoute = navController.currentBackStackEntry?.destination?.route
139-
when (state) {
140-
is AuthState.Success -> {
141-
pendingResolver.value = null
142-
pendingLinkingCredential.value = null
143-
144-
state.result?.let { result ->
145-
if (state.user.uid != lastSuccessfulUserId.value) {
146-
onSignInSuccess(result)
147-
lastSuccessfulUserId.value = state.user.uid
148-
}
149-
}
150-
151-
if (currentRoute != AuthRoute.Success.route) {
152-
navController.navigate(AuthRoute.Success.route) {
153-
popUpTo(AuthRoute.MethodPicker.route) { inclusive = true }
154-
launchSingleTop = true
155-
}
156-
}
157-
}
158-
159-
is AuthState.RequiresEmailVerification,
160-
is AuthState.RequiresProfileCompletion -> {
161-
pendingResolver.value = null
162-
pendingLinkingCredential.value = null
163-
if (currentRoute != AuthRoute.Success.route) {
164-
navController.navigate(AuthRoute.Success.route) {
165-
popUpTo(AuthRoute.MethodPicker.route) { inclusive = true }
166-
launchSingleTop = true
167-
}
168-
}
169-
}
170-
171-
is AuthState.RequiresMfa -> {
172-
pendingResolver.value = state.resolver
173-
if (currentRoute != AuthRoute.MfaChallenge.route) {
174-
navController.navigate(AuthRoute.MfaChallenge.route) {
175-
launchSingleTop = true
176-
}
177-
}
178-
}
179-
180-
is AuthState.Cancelled -> {
181-
pendingResolver.value = null
182-
pendingLinkingCredential.value = null
183-
lastSuccessfulUserId.value = null
184-
if (currentRoute != AuthRoute.MethodPicker.route) {
185-
navController.navigate(AuthRoute.MethodPicker.route) {
186-
popUpTo(AuthRoute.MethodPicker.route) { inclusive = true }
187-
launchSingleTop = true
188-
}
189-
}
190-
onSignInCancelled()
191-
}
192-
193-
is AuthState.Idle -> {
194-
pendingResolver.value = null
195-
pendingLinkingCredential.value = null
196-
lastSuccessfulUserId.value = null
197-
if (currentRoute != AuthRoute.MethodPicker.route) {
198-
navController.navigate(AuthRoute.MethodPicker.route) {
199-
popUpTo(AuthRoute.MethodPicker.route) { inclusive = true }
200-
launchSingleTop = true
201-
}
202-
}
203-
}
204-
205-
else -> Unit
206-
}
207-
}
208-
209116
Scaffold(modifier = modifier) { innerPadding ->
210117
Surface(
211118
modifier = Modifier
@@ -224,6 +131,8 @@ fun FirebaseAuthScreen(
224131
privacyPolicyUrl = configuration.privacyPolicyUrl,
225132
onProviderSelected = { provider ->
226133
when (provider) {
134+
is AuthProvider.Anonymous -> onSignInAnonymously?.invoke()
135+
227136
is AuthProvider.Email -> {
228137
navController.navigate(AuthRoute.Email.route)
229138
}
@@ -379,6 +288,103 @@ fun FirebaseAuthScreen(
379288
}
380289
}
381290

291+
// Handle email link sign-in (deep links)
292+
LaunchedEffect(emailLink) {
293+
if (emailLink != null && emailProvider != null) {
294+
try {
295+
EmailLinkPersistenceManager.retrieveSessionRecord(context)?.email?.let { email ->
296+
authUI.signInWithEmailLink(
297+
context = context,
298+
config = configuration,
299+
provider = emailProvider,
300+
email = email,
301+
emailLink = emailLink
302+
)
303+
}
304+
} catch (e: Exception) {
305+
Log.e("FirebaseAuthScreen", "Failed to complete email link sign-in", e)
306+
}
307+
308+
if (navController.currentBackStackEntry?.destination?.route != AuthRoute.Email.route) {
309+
navController.navigate(AuthRoute.Email.route)
310+
}
311+
}
312+
}
313+
314+
// Synchronise auth state changes with navigation stack.
315+
LaunchedEffect(authState) {
316+
val state = authState
317+
val currentRoute = navController.currentBackStackEntry?.destination?.route
318+
when (state) {
319+
is AuthState.Success -> {
320+
pendingResolver.value = null
321+
pendingLinkingCredential.value = null
322+
323+
state.result?.let { result ->
324+
if (state.user.uid != lastSuccessfulUserId.value) {
325+
onSignInSuccess(result)
326+
lastSuccessfulUserId.value = state.user.uid
327+
}
328+
}
329+
330+
if (currentRoute != AuthRoute.Success.route) {
331+
navController.navigate(AuthRoute.Success.route) {
332+
popUpTo(AuthRoute.MethodPicker.route) { inclusive = true }
333+
launchSingleTop = true
334+
}
335+
}
336+
}
337+
338+
is AuthState.RequiresEmailVerification,
339+
is AuthState.RequiresProfileCompletion -> {
340+
pendingResolver.value = null
341+
pendingLinkingCredential.value = null
342+
if (currentRoute != AuthRoute.Success.route) {
343+
navController.navigate(AuthRoute.Success.route) {
344+
popUpTo(AuthRoute.MethodPicker.route) { inclusive = true }
345+
launchSingleTop = true
346+
}
347+
}
348+
}
349+
350+
is AuthState.RequiresMfa -> {
351+
pendingResolver.value = state.resolver
352+
if (currentRoute != AuthRoute.MfaChallenge.route) {
353+
navController.navigate(AuthRoute.MfaChallenge.route) {
354+
launchSingleTop = true
355+
}
356+
}
357+
}
358+
359+
is AuthState.Cancelled -> {
360+
pendingResolver.value = null
361+
pendingLinkingCredential.value = null
362+
lastSuccessfulUserId.value = null
363+
if (currentRoute != AuthRoute.MethodPicker.route) {
364+
navController.navigate(AuthRoute.MethodPicker.route) {
365+
popUpTo(AuthRoute.MethodPicker.route) { inclusive = true }
366+
launchSingleTop = true
367+
}
368+
}
369+
onSignInCancelled()
370+
}
371+
372+
is AuthState.Idle -> {
373+
pendingResolver.value = null
374+
pendingLinkingCredential.value = null
375+
lastSuccessfulUserId.value = null
376+
if (currentRoute != AuthRoute.MethodPicker.route) {
377+
navController.navigate(AuthRoute.MethodPicker.route) {
378+
popUpTo(AuthRoute.MethodPicker.route) { inclusive = true }
379+
launchSingleTop = true
380+
}
381+
}
382+
}
383+
384+
else -> Unit
385+
}
386+
}
387+
382388
val errorState = authState as? AuthState.Error
383389
if (isErrorDialogVisible.value && errorState != null) {
384390
ErrorRecoveryDialog(

0 commit comments

Comments
 (0)