diff --git a/auth/src/main/AndroidManifest.xml b/auth/src/main/AndroidManifest.xml index 16055e162..347c621c2 100644 --- a/auth/src/main/AndroidManifest.xml +++ b/auth/src/main/AndroidManifest.xml @@ -122,27 +122,6 @@ - - - - - - - - - - - - - - Unit { + val coroutineScope = rememberCoroutineScope() + return remember(this) { + { + coroutineScope.launch { + try { + signInAnonymously() + } catch (e: AuthException) { + // Already an AuthException, don't re-wrap it + updateAuthState(AuthState.Error(e)) + } catch (e: Exception) { + val authException = AuthException.from(e) + updateAuthState(AuthState.Error(authException)) + } + } + } + } +} + +/** + * Signs in a user anonymously with Firebase Authentication. + * + * This method creates a temporary anonymous user account that can be used for testing + * or as a starting point for users who want to try the app before creating a permanent + * account. Anonymous users can later be upgraded to permanent accounts by linking + * credentials (email/password, social providers, phone, etc.). + * + * **Flow:** + * 1. Updates auth state to loading with "Signing in anonymously..." message + * 2. Calls Firebase Auth's `signInAnonymously()` method + * 3. Updates auth state to idle on success + * 4. Handles cancellation and converts exceptions to [AuthException] types + * + * **Anonymous Account Benefits:** + * - No user data collection required + * - Immediate access to app features + * - Can be upgraded to permanent account later + * - Useful for guest users and app trials + * + * **Account Upgrade:** + * Anonymous accounts can be upgraded to permanent accounts by calling methods like: + * - [signInAndLinkWithCredential] with email/password or social credentials + * - [createOrLinkUserWithEmailAndPassword] for email/password accounts + * - [signInWithPhoneAuthCredential] for phone authentication + * + * **Example: Basic anonymous sign-in** + * ```kotlin + * try { + * firebaseAuthUI.signInAnonymously() + * // User is now signed in anonymously + * // Show app content or prompt for account creation + * } catch (e: AuthException.AuthCancelledException) { + * // User cancelled the sign-in process + * } catch (e: AuthException.NetworkException) { + * // Network error occurred + * } + * ``` + * + * **Example: Anonymous sign-in with upgrade flow** + * ```kotlin + * // Step 1: Sign in anonymously + * firebaseAuthUI.signInAnonymously() + * + * // Step 2: Later, upgrade to permanent account + * try { + * firebaseAuthUI.createOrLinkUserWithEmailAndPassword( + * context = context, + * config = authUIConfig, + * provider = emailProvider, + * name = "John Doe", + * email = "john@example.com", + * password = "SecurePass123!" + * ) + * // Anonymous account upgraded to permanent email/password account + * } catch (e: AuthException.AccountLinkingRequiredException) { + * // Email already exists - show account linking UI + * } + * ``` + * + * @throws AuthException.AuthCancelledException if the coroutine is cancelled + * @throws AuthException.NetworkException if a network error occurs + * @throws AuthException.UnknownException for other authentication errors + * + * @see signInAndLinkWithCredential for upgrading anonymous accounts + * @see createOrLinkUserWithEmailAndPassword for email/password upgrade + * @see signInWithPhoneAuthCredential for phone authentication upgrade + */ +internal suspend fun FirebaseAuthUI.signInAnonymously() { + try { + updateAuthState(AuthState.Loading("Signing in anonymously...")) + auth.signInAnonymously().await() + updateAuthState(AuthState.Idle) + } catch (e: CancellationException) { + val cancelledException = AuthException.AuthCancelledException( + message = "Sign in anonymously was cancelled", + cause = e + ) + updateAuthState(AuthState.Error(cancelledException)) + throw cancelledException + } catch (e: AuthException) { + updateAuthState(AuthState.Error(e)) + throw e + } catch (e: Exception) { + val authException = AuthException.from(e) + updateAuthState(AuthState.Error(authException)) + throw authException + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/AuthProvider.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/AuthProvider.kt index 50101f7e0..13d4728a4 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/AuthProvider.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/AuthProvider.kt @@ -635,8 +635,7 @@ abstract class AuthProvider(open val providerId: String, open val name: String) * An interface to wrap the static `FacebookAuthProvider.getCredential` method to make it testable. * @suppress */ - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - interface CredentialProvider { + internal interface CredentialProvider { fun getCredential(token: String): AuthCredential } diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt index f010ab06c..5e9784e9a 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt @@ -697,8 +697,7 @@ internal suspend fun FirebaseAuthUI.sendSignInLinkToEmail( * * @see sendSignInLinkToEmail for sending the initial email link */ -// TODO(demolaf: make this internal when done testing email link sign in with composeapp -suspend fun FirebaseAuthUI.signInWithEmailLink( +internal suspend fun FirebaseAuthUI.signInWithEmailLink( context: Context, config: AuthUIConfiguration, provider: AuthProvider.Email, diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt index 3ff865af0..ff8548579 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt @@ -51,7 +51,7 @@ import kotlinx.coroutines.launch * @see signInWithFacebook */ @Composable -fun FirebaseAuthUI.rememberSignInWithFacebookLauncher( +internal fun FirebaseAuthUI.rememberSignInWithFacebookLauncher( context: Context, config: AuthUIConfiguration, provider: AuthProvider.Facebook, diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/ui/method_picker/AuthMethodPicker.kt b/auth/src/main/java/com/firebase/ui/auth/compose/ui/method_picker/AuthMethodPicker.kt index 6082e9f1b..5bb2cfd05 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/ui/method_picker/AuthMethodPicker.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/ui/method_picker/AuthMethodPicker.kt @@ -122,10 +122,10 @@ fun AuthMethodPicker( } } AnnotatedStringResource( + modifier = Modifier.padding(vertical = 16.dp, horizontal = 16.dp), context = context, inPreview = inPreview, previewText = "By continuing, you accept our Terms of Service and Privacy Policy.", - modifier = Modifier.padding(vertical = 16.dp), id = R.string.fui_tos_and_pp, links = arrayOf( "Terms of Service" to (termsOfServiceUrl ?: ""), diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/ui/screens/FirebaseAuthScreen.kt b/auth/src/main/java/com/firebase/ui/auth/compose/ui/screens/FirebaseAuthScreen.kt index 076f5de87..1d021f41b 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/ui/screens/FirebaseAuthScreen.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/ui/screens/FirebaseAuthScreen.kt @@ -28,7 +28,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -50,11 +49,11 @@ import com.firebase.ui.auth.compose.FirebaseAuthUI import com.firebase.ui.auth.compose.configuration.AuthUIConfiguration import com.firebase.ui.auth.compose.configuration.MfaConfiguration import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.compose.configuration.auth_provider.rememberAnonymousSignInHandler import com.firebase.ui.auth.compose.configuration.auth_provider.rememberSignInWithFacebookLauncher import com.firebase.ui.auth.compose.configuration.auth_provider.signInWithEmailLink import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider -import com.firebase.ui.auth.compose.configuration.theme.AuthUIAsset import com.firebase.ui.auth.compose.ui.components.ErrorRecoveryDialog import com.firebase.ui.auth.compose.ui.method_picker.AuthMethodPicker import com.firebase.ui.auth.compose.ui.screens.phone.PhoneAuthScreen @@ -98,9 +97,14 @@ fun FirebaseAuthScreen( val pendingLinkingCredential = remember { mutableStateOf(null) } val pendingResolver = remember { mutableStateOf(null) } + val anonymousProvider = configuration.providers.filterIsInstance().firstOrNull() val emailProvider = configuration.providers.filterIsInstance().firstOrNull() val facebookProvider = configuration.providers.filterIsInstance().firstOrNull() - val logoAsset = configuration.logo?.let { AuthUIAsset.Vector(it) } + val logoAsset = configuration.logo + + val onSignInAnonymously = anonymousProvider?.let { + authUI.rememberAnonymousSignInHandler() + } val onSignInWithFacebook = facebookProvider?.let { authUI.rememberSignInWithFacebookLauncher( @@ -128,6 +132,8 @@ fun FirebaseAuthScreen( privacyPolicyUrl = configuration.privacyPolicyUrl, onProviderSelected = { provider -> when (provider) { + is AuthProvider.Anonymous -> onSignInAnonymously?.invoke() + is AuthProvider.Email -> { navController.navigate(AuthRoute.Email.route) } @@ -226,6 +232,9 @@ fun FirebaseAuthScreen( Log.e("FirebaseAuthScreen", "Failed to refresh user", e) } } + }, + onNavigate = { route -> + navController.navigate(route.route) } ) } @@ -431,7 +440,7 @@ fun FirebaseAuthScreen( } } -private sealed class AuthRoute(val route: String) { +sealed class AuthRoute(val route: String) { object MethodPicker : AuthRoute("auth_method_picker") object Email : AuthRoute("auth_email") object Phone : AuthRoute("auth_phone") @@ -445,7 +454,8 @@ data class AuthSuccessUiContext( val stringProvider: AuthUIStringProvider, val onSignOut: () -> Unit, val onManageMfa: () -> Unit, - val onReloadUser: () -> Unit + val onReloadUser: () -> Unit, + val onNavigate: (AuthRoute) -> Unit, ) @Composable diff --git a/auth/src/test/java/com/firebase/ui/auth/compose/configuration/AuthUIConfigurationTest.kt b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/AuthUIConfigurationTest.kt index 4190a7bf0..b5e306b40 100644 --- a/auth/src/test/java/com/firebase/ui/auth/compose/configuration/AuthUIConfigurationTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/AuthUIConfigurationTest.kt @@ -23,6 +23,7 @@ import com.firebase.ui.auth.R import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvider import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.theme.AuthUIAsset import com.firebase.ui.auth.compose.configuration.theme.AuthUITheme import com.google.common.truth.Truth.assertThat import com.google.firebase.auth.actionCodeSettings @@ -98,6 +99,7 @@ class AuthUIConfigurationTest { url = "https://example.com/verify" handleCodeInApp = true } + val logoAsset = AuthUIAsset.Vector(Icons.Default.AccountCircle) val config = authUIConfiguration { context = applicationContext @@ -122,7 +124,7 @@ class AuthUIConfigurationTest { isAnonymousUpgradeEnabled = true tosUrl = "https://example.com/tos" privacyPolicyUrl = "https://example.com/privacy" - logo = Icons.Default.AccountCircle + logo = logoAsset passwordResetActionCodeSettings = customPasswordResetActionCodeSettings isNewEmailAccountsAllowed = false isDisplayNameRequired = false @@ -139,7 +141,7 @@ class AuthUIConfigurationTest { assertThat(config.isAnonymousUpgradeEnabled).isTrue() assertThat(config.tosUrl).isEqualTo("https://example.com/tos") assertThat(config.privacyPolicyUrl).isEqualTo("https://example.com/privacy") - assertThat(config.logo).isEqualTo(Icons.Default.AccountCircle) + assertThat(config.logo).isEqualTo(logoAsset) assertThat(config.passwordResetActionCodeSettings) .isEqualTo(customPasswordResetActionCodeSettings) assertThat(config.isNewEmailAccountsAllowed).isFalse() diff --git a/auth/src/test/java/com/firebase/ui/auth/compose/configuration/auth_provider/AnonymousAuthProviderFirebaseAuthUITest.kt b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/auth_provider/AnonymousAuthProviderFirebaseAuthUITest.kt new file mode 100644 index 000000000..c36899a63 --- /dev/null +++ b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/auth_provider/AnonymousAuthProviderFirebaseAuthUITest.kt @@ -0,0 +1,316 @@ +/* + * Copyright 2025 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.compose.configuration.auth_provider + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.firebase.ui.auth.compose.AuthException +import com.firebase.ui.auth.compose.AuthState +import com.firebase.ui.auth.compose.FirebaseAuthUI +import com.firebase.ui.auth.compose.configuration.authUIConfiguration +import com.google.android.gms.tasks.TaskCompletionSource +import com.google.common.truth.Truth.assertThat +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseNetworkException +import com.google.firebase.FirebaseOptions +import com.google.firebase.auth.AuthCredential +import com.google.firebase.auth.AuthResult +import com.google.firebase.auth.EmailAuthProvider +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseAuthUserCollisionException +import com.google.firebase.auth.FirebaseUser +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class AnonymousAuthProviderFirebaseAuthUITest { + + @Mock + private lateinit var mockFirebaseAuth: FirebaseAuth + + private lateinit var firebaseApp: FirebaseApp + private lateinit var applicationContext: Context + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + + FirebaseAuthUI.clearInstanceCache() + + applicationContext = ApplicationProvider.getApplicationContext() + + FirebaseApp.getApps(applicationContext).forEach { app -> + app.delete() + } + + firebaseApp = FirebaseApp.initializeApp( + applicationContext, + FirebaseOptions.Builder() + .setApiKey("fake-api-key") + .setApplicationId("fake-app-id") + .setProjectId("fake-project-id") + .build() + ) + } + + @After + fun tearDown() { + FirebaseAuthUI.clearInstanceCache() + try { + firebaseApp.delete() + } catch (_: Exception) { + // Ignore if already deleted + } + } + + // ============================================================================================= + // signInAnonymously Tests + // ============================================================================================= + + @Test + fun `signInAnonymously - successful anonymous sign in`() = runTest { + val mockUser = mock(FirebaseUser::class.java) + `when`(mockUser.isAnonymous).thenReturn(true) + val mockAuthResult = mock(AuthResult::class.java) + `when`(mockAuthResult.user).thenReturn(mockUser) + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setResult(mockAuthResult) + `when`(mockFirebaseAuth.signInAnonymously()) + .thenReturn(taskCompletionSource.task) + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + + instance.signInAnonymously() + + verify(mockFirebaseAuth).signInAnonymously() + + val finalState = instance.authStateFlow().first { it is AuthState.Idle } + assertThat(finalState).isInstanceOf(AuthState.Idle::class.java) + } + + @Test + fun `signInAnonymously - handles network error`() = runTest { + val networkException = FirebaseNetworkException("Network error") + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setException(networkException) + `when`(mockFirebaseAuth.signInAnonymously()) + .thenReturn(taskCompletionSource.task) + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + + try { + instance.signInAnonymously() + assertThat(false).isTrue() // Should not reach here + } catch (e: AuthException.NetworkException) { + assertThat(e.cause).isEqualTo(networkException) + } + + val currentState = instance.authStateFlow().first { it is AuthState.Error } + assertThat(currentState).isInstanceOf(AuthState.Error::class.java) + val errorState = currentState as AuthState.Error + assertThat(errorState.exception).isInstanceOf(AuthException.NetworkException::class.java) + } + + @Test + fun `signInAnonymously - handles cancellation`() = runTest { + val cancellationException = CancellationException("Operation cancelled") + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setException(cancellationException) + `when`(mockFirebaseAuth.signInAnonymously()) + .thenReturn(taskCompletionSource.task) + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + + try { + instance.signInAnonymously() + assertThat(false).isTrue() // Should not reach here + } catch (e: AuthException.AuthCancelledException) { + assertThat(e.message).contains("cancelled") + assertThat(e.cause).isInstanceOf(CancellationException::class.java) + } + + val currentState = instance.authStateFlow().first { it is AuthState.Error } + assertThat(currentState).isInstanceOf(AuthState.Error::class.java) + val errorState = currentState as AuthState.Error + assertThat(errorState.exception).isInstanceOf(AuthException.AuthCancelledException::class.java) + } + + @Test + fun `signInAnonymously - handles generic exception`() = runTest { + val genericException = RuntimeException("Something went wrong") + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setException(genericException) + `when`(mockFirebaseAuth.signInAnonymously()) + .thenReturn(taskCompletionSource.task) + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + + try { + instance.signInAnonymously() + assertThat(false).isTrue() // Should not reach here + } catch (e: AuthException.UnknownException) { + assertThat(e.cause).isEqualTo(genericException) + } + + val currentState = instance.authStateFlow().first { it is AuthState.Error } + assertThat(currentState).isInstanceOf(AuthState.Error::class.java) + val errorState = currentState as AuthState.Error + assertThat(errorState.exception).isInstanceOf(AuthException.UnknownException::class.java) + } + + // ============================================================================================= + // Anonymous Account Upgrade Tests + // ============================================================================================= + + @Test + fun `Upgrade anonymous account with email and password when isAnonymousUpgradeEnabled`() = runTest { + val mockAnonymousUser = mock(FirebaseUser::class.java) + `when`(mockAnonymousUser.isAnonymous).thenReturn(true) + `when`(mockFirebaseAuth.currentUser).thenReturn(mockAnonymousUser) + + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setResult(mock(AuthResult::class.java)) + `when`(mockAnonymousUser.linkWithCredential(ArgumentMatchers.any(AuthCredential::class.java))) + .thenReturn(taskCompletionSource.task) + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + val emailProvider = AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + val config = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Anonymous) + provider(emailProvider) + } + isAnonymousUpgradeEnabled = true + } + + instance.createOrLinkUserWithEmailAndPassword( + context = applicationContext, + config = config, + provider = emailProvider, + name = null, + email = "test@example.com", + password = "Pass@123" + ) + + verify(mockAnonymousUser).linkWithCredential(ArgumentMatchers.any(AuthCredential::class.java)) + } + + @Test + fun `Upgrade anonymous account throws AccountLinkingRequiredException on collision`() = runTest { + val mockAnonymousUser = mock(FirebaseUser::class.java) + `when`(mockAnonymousUser.isAnonymous).thenReturn(true) + `when`(mockAnonymousUser.email).thenReturn(null) + `when`(mockFirebaseAuth.currentUser).thenReturn(mockAnonymousUser) + + val collisionException = mock(FirebaseAuthUserCollisionException::class.java) + `when`(collisionException.errorCode).thenReturn("ERROR_EMAIL_ALREADY_IN_USE") + `when`(collisionException.email).thenReturn("test@example.com") + + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setException(collisionException) + `when`(mockAnonymousUser.linkWithCredential(ArgumentMatchers.any(AuthCredential::class.java))) + .thenReturn(taskCompletionSource.task) + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + val emailProvider = AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + val config = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Anonymous) + provider(emailProvider) + } + isAnonymousUpgradeEnabled = true + } + + try { + instance.createOrLinkUserWithEmailAndPassword( + context = applicationContext, + config = config, + provider = emailProvider, + name = null, + email = "test@example.com", + password = "Pass@123" + ) + assertThat(false).isTrue() // Should not reach here + } catch (e: AuthException.AccountLinkingRequiredException) { + assertThat(e.cause).isEqualTo(collisionException) + assertThat(e.email).isEqualTo("test@example.com") + assertThat(e.credential).isNotNull() + } + + val currentState = instance.authStateFlow().first { it is AuthState.Error } + assertThat(currentState).isInstanceOf(AuthState.Error::class.java) + val errorState = currentState as AuthState.Error + assertThat(errorState.exception).isInstanceOf(AuthException.AccountLinkingRequiredException::class.java) + } + + @Test + fun `Upgrade anonymous account with credential when isAnonymousUpgradeEnabled`() = runTest { + val mockAnonymousUser = mock(FirebaseUser::class.java) + `when`(mockAnonymousUser.isAnonymous).thenReturn(true) + `when`(mockFirebaseAuth.currentUser).thenReturn(mockAnonymousUser) + + val credential = EmailAuthProvider.getCredential("test@example.com", "Pass@123") + val mockAuthResult = mock(AuthResult::class.java) + `when`(mockAuthResult.user).thenReturn(mockAnonymousUser) + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setResult(mockAuthResult) + `when`(mockAnonymousUser.linkWithCredential(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(AuthProvider.Anonymous) + provider(emailProvider) + } + isAnonymousUpgradeEnabled = true + } + + val result = instance.signInAndLinkWithCredential( + config = config, + credential = credential + ) + + assertThat(result).isNotNull() + verify(mockAnonymousUser).linkWithCredential(credential) + } +} diff --git a/composeapp/src/main/AndroidManifest.xml b/composeapp/src/main/AndroidManifest.xml index fccb57d77..83e2dd1a5 100644 --- a/composeapp/src/main/AndroidManifest.xml +++ b/composeapp/src/main/AndroidManifest.xml @@ -22,6 +22,21 @@ + + + + + + + + + + + diff --git a/composeapp/src/main/java/com/firebase/composeapp/MainActivity.kt b/composeapp/src/main/java/com/firebase/composeapp/MainActivity.kt index 28d9bf068..4c0bbf1ee 100644 --- a/composeapp/src/main/java/com/firebase/composeapp/MainActivity.kt +++ b/composeapp/src/main/java/com/firebase/composeapp/MainActivity.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -25,6 +26,7 @@ import com.firebase.ui.auth.compose.FirebaseAuthUI import com.firebase.ui.auth.compose.configuration.PasswordRule import com.firebase.ui.auth.compose.configuration.authUIConfiguration import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.compose.configuration.theme.AuthUIAsset import com.firebase.ui.auth.compose.configuration.theme.AuthUITheme import com.firebase.ui.auth.compose.ui.screens.EmailSignInLinkHandlerActivity import com.firebase.ui.auth.compose.ui.screens.FirebaseAuthScreen @@ -41,6 +43,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + enableEdgeToEdge() FirebaseApp.initializeApp(applicationContext) val authUI = FirebaseAuthUI.getInstance() @@ -51,172 +54,196 @@ class MainActivity : ComponentActivity() { val emailLink = intent.getStringExtra(EmailSignInLinkHandlerActivity.EXTRA_EMAIL_LINK) - val configuration = authUIConfiguration { - context = applicationContext - providers { - provider( - AuthProvider.Email( - isDisplayNameRequired = true, - isEmailLinkForceSameDeviceEnabled = true, - isEmailLinkSignInEnabled = false, - emailLinkActionCodeSettings = actionCodeSettings { - url = "https://temp-test-aa342.firebaseapp.com" - handleCodeInApp = true - setAndroidPackageName( - "com.firebase.composeapp", - true, - null + setContent { + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Anonymous) + provider( + AuthProvider.Email( + isDisplayNameRequired = true, + isEmailLinkForceSameDeviceEnabled = true, + isEmailLinkSignInEnabled = false, + emailLinkActionCodeSettings = actionCodeSettings { + url = "https://temp-test-aa342.firebaseapp.com" + handleCodeInApp = true + setAndroidPackageName( + "com.firebase.composeapp", + true, + null + ) + }, + isNewAccountsAllowed = true, + minimumPasswordLength = 8, + passwordValidationRules = listOf( + PasswordRule.MinimumLength(8), + PasswordRule.RequireLowercase, + PasswordRule.RequireUppercase, ) - }, - isNewAccountsAllowed = true, - minimumPasswordLength = 8, - passwordValidationRules = listOf( - PasswordRule.MinimumLength(8), - PasswordRule.RequireLowercase, - PasswordRule.RequireUppercase, ) ) - ) - provider( - AuthProvider.Phone( - defaultNumber = null, - defaultCountryCode = null, - allowedCountries = emptyList(), - smsCodeLength = 6, - timeout = 120L, - isInstantVerificationEnabled = true + provider( + AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = null, + allowedCountries = emptyList(), + smsCodeLength = 6, + timeout = 120L, + isInstantVerificationEnabled = true + ) ) - ) - provider( - AuthProvider.Facebook( - applicationId = "792556260059222" + provider( + AuthProvider.Facebook( + applicationId = "792556260059222" + ) ) - ) + } + logo = AuthUIAsset.Resource(R.drawable.firebase_auth_120dp) + tosUrl = "https://policies.google.com/terms?hl=en-NG&fg=1" + privacyPolicyUrl = "https://policies.google.com/privacy?hl=en-NG&fg=1" } - tosUrl = "https://policies.google.com/terms?hl=en-NG&fg=1" - privacyPolicyUrl = "https://policies.google.com/privacy?hl=en-NG&fg=1" - } - setContent { - AuthUITheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - FirebaseAuthScreen( - configuration = configuration, - authUI = authUI, - emailLink = emailLink, - onSignInSuccess = { result -> - Log.d("MainActivity", "Authentication success: ${result.user?.uid}") - }, - onSignInFailure = { exception: AuthException -> - Log.e("MainActivity", "Authentication failed", exception) - }, - onSignInCancelled = { - Log.d("MainActivity", "Authentication cancelled") - }, - authenticatedContent = { state, uiContext -> - AppAuthenticatedContent(state, uiContext) - } - ) + setContent { + AuthUITheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + FirebaseAuthScreen( + configuration = configuration, + authUI = authUI, + emailLink = emailLink, + onSignInSuccess = { result -> + Log.d("MainActivity", "Authentication success: ${result.user?.uid}") + }, + onSignInFailure = { exception: AuthException -> + Log.e("MainActivity", "Authentication failed", exception) + }, + onSignInCancelled = { + Log.d("MainActivity", "Authentication cancelled") + }, + authenticatedContent = { state, uiContext -> + AppAuthenticatedContent(state, uiContext) + } + ) + } } } } } -} -@Composable -private fun AppAuthenticatedContent( - state: AuthState, - uiContext: AuthSuccessUiContext -) { - val stringProvider = uiContext.stringProvider - when (state) { - is AuthState.Success -> { - val user = uiContext.authUI.getCurrentUser() - val identifier = user?.email ?: user?.phoneNumber ?: user?.uid.orEmpty() - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - if (identifier.isNotBlank()) { + @Composable + private fun AppAuthenticatedContent( + state: AuthState, + uiContext: AuthSuccessUiContext + ) { + val stringProvider = uiContext.stringProvider + when (state) { + is AuthState.Success -> { + val user = uiContext.authUI.getCurrentUser() + val identifier = user?.email ?: user?.phoneNumber ?: user?.uid.orEmpty() + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + if (identifier.isNotBlank()) { + Text( + text = stringProvider.signedInAs(identifier), + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(16.dp)) + } Text( - text = stringProvider.signedInAs(identifier), + "isAnonymous - ${state.user.isAnonymous}", textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.height(16.dp)) - } + Text( + "Providers - ${state.user.providerData.map { it.providerId }}", + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + if (state.user.isAnonymous) { + Button( + onClick = { - Button(onClick = uiContext.onManageMfa) { - Text(stringProvider.manageMfaAction) - } - Spacer(modifier = Modifier.height(8.dp)) - Button(onClick = uiContext.onSignOut) { - Text(stringProvider.signOutAction) + } + ) { + Text("Upgrade with Email") + } + } + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = uiContext.onManageMfa) { + Text(stringProvider.manageMfaAction) + } + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = uiContext.onSignOut) { + Text(stringProvider.signOutAction) + } } } - } - is AuthState.RequiresEmailVerification -> { - val email = uiContext.authUI.getCurrentUser()?.email ?: stringProvider.emailProvider - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = stringProvider.verifyEmailInstruction(email), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium - ) - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = { uiContext.authUI.getCurrentUser()?.sendEmailVerification() }) { - Text(stringProvider.resendVerificationEmailAction) - } - Spacer(modifier = Modifier.height(8.dp)) - Button(onClick = uiContext.onReloadUser) { - Text(stringProvider.verifiedEmailAction) - } - Spacer(modifier = Modifier.height(8.dp)) - Button(onClick = uiContext.onSignOut) { - Text(stringProvider.signOutAction) + is AuthState.RequiresEmailVerification -> { + val email = uiContext.authUI.getCurrentUser()?.email ?: stringProvider.emailProvider + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringProvider.verifyEmailInstruction(email), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { + uiContext.authUI.getCurrentUser()?.sendEmailVerification() + }) { + Text(stringProvider.resendVerificationEmailAction) + } + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = uiContext.onReloadUser) { + Text(stringProvider.verifiedEmailAction) + } + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = uiContext.onSignOut) { + Text(stringProvider.signOutAction) + } } } - } - is AuthState.RequiresProfileCompletion -> { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = stringProvider.profileCompletionMessage, - textAlign = TextAlign.Center - ) - if (state.missingFields.isNotEmpty()) { - Spacer(modifier = Modifier.height(12.dp)) + is AuthState.RequiresProfileCompletion -> { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { Text( - text = stringProvider.profileMissingFieldsMessage(state.missingFields.joinToString()), + text = stringProvider.profileCompletionMessage, textAlign = TextAlign.Center ) - } - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = uiContext.onSignOut) { - Text(stringProvider.signOutAction) + if (state.missingFields.isNotEmpty()) { + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = stringProvider.profileMissingFieldsMessage(state.missingFields.joinToString()), + textAlign = TextAlign.Center + ) + } + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = uiContext.onSignOut) { + Text(stringProvider.signOutAction) + } } } - } - else -> { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - CircularProgressIndicator() + else -> { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + CircularProgressIndicator() + } } } } diff --git a/e2eTest/src/test/java/com/firebase/ui/auth/compose/ui/screens/AnonymousAuthScreenTest.kt b/e2eTest/src/test/java/com/firebase/ui/auth/compose/ui/screens/AnonymousAuthScreenTest.kt new file mode 100644 index 000000000..db2d6971d --- /dev/null +++ b/e2eTest/src/test/java/com/firebase/ui/auth/compose/ui/screens/AnonymousAuthScreenTest.kt @@ -0,0 +1,342 @@ +package com.firebase.ui.auth.compose.ui.screens + +import android.content.Context +import android.os.Looper +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.test.core.app.ApplicationProvider +import com.firebase.ui.auth.compose.AuthException +import com.firebase.ui.auth.compose.AuthState +import com.firebase.ui.auth.compose.FirebaseAuthUI +import com.firebase.ui.auth.compose.configuration.AuthUIConfiguration +import com.firebase.ui.auth.compose.configuration.authUIConfiguration +import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.compose.testutil.AUTH_STATE_WAIT_TIMEOUT_MS +import com.firebase.ui.auth.compose.testutil.EmulatorAuthApi +import com.google.common.truth.Truth.assertThat +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config + +@Config(sdk = [34]) +@RunWith(RobolectricTestRunner::class) +class AnonymousAuthScreenTest { + @get:Rule + val composeTestRule = createComposeRule() + + private lateinit var applicationContext: Context + + private lateinit var stringProvider: AuthUIStringProvider + + lateinit var authUI: FirebaseAuthUI + private lateinit var emulatorApi: EmulatorAuthApi + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + + applicationContext = ApplicationProvider.getApplicationContext() + + stringProvider = DefaultAuthUIStringProvider(applicationContext) + + // Clear any existing Firebase apps + FirebaseApp.getApps(applicationContext).forEach { app -> + app.delete() + } + + // Initialize default FirebaseApp + val firebaseApp = FirebaseApp.initializeApp( + applicationContext, + FirebaseOptions.Builder() + .setApiKey("fake-api-key") + .setApplicationId("fake-app-id") + .setProjectId("fake-project-id") + .build() + ) + + authUI = FirebaseAuthUI.getInstance() + authUI.auth.useEmulator("127.0.0.1", 9099) + + emulatorApi = EmulatorAuthApi( + projectId = firebaseApp.options.projectId + ?: throw IllegalStateException("Project ID is required for emulator interactions"), + emulatorHost = "127.0.0.1", + emulatorPort = 9099 + ) + + // Clear emulator data + emulatorApi.clearEmulatorData() + } + + @After + fun tearDown() { + // Clean up after each test to prevent test pollution + FirebaseAuthUI.clearInstanceCache() + + // Clear emulator data + emulatorApi.clearEmulatorData() + } + + @Test + fun `anonymous sign-in emits Success auth state`() { + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Anonymous) + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + ) + } + } + + // Track auth state changes + var currentAuthState: AuthState = AuthState.Idle + + composeTestRule.setContent { + TestAuthScreen(configuration = configuration) + val authState by authUI.authStateFlow().collectAsState(AuthState.Idle) + currentAuthState = authState + } + + // Wait for the navigation to settle and UI to be ready + composeTestRule.waitForIdle() + shadowOf(Looper.getMainLooper()).idle() + + composeTestRule.onNodeWithText(stringProvider.signInAnonymously) + .assertIsDisplayed() + .performClick() + composeTestRule.waitForIdle() + + println("TEST: Pumping looper after click...") + shadowOf(Looper.getMainLooper()).idle() + + // Wait for auth state to transition to Success + println("TEST: Waiting for auth state change... Current state: $currentAuthState") + composeTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) { + shadowOf(Looper.getMainLooper()).idle() + println( + "TEST: Auth state during wait: $currentAuthState, isAnonymous" + + " - ${authUI.auth.currentUser?.isAnonymous}" + ) + currentAuthState is AuthState.Success + } + + composeTestRule.onNodeWithText("isAnonymous - true") + .assertIsDisplayed() + + // Verify the auth state and user properties + println("TEST: Verifying final auth state: $currentAuthState") + assertThat(currentAuthState) + .isInstanceOf(AuthState.Success::class.java) + assertThat(authUI.auth.currentUser).isNotNull() + assertThat(authUI.auth.currentUser!!.isAnonymous).isEqualTo(true) + } + + @Test + fun `anonymous upgrade enabled links new user sign-up and emits RequiresEmailVerification auth state`() { + val name = "Anonymous Upgrade User" + val email = "anonymousupgrade@example.com" + val password = "Test@123" + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Anonymous) + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + ) + } + isAnonymousUpgradeEnabled = true + } + + // Track auth state changes + var currentAuthState: AuthState = AuthState.Idle + + composeTestRule.setContent { + TestAuthScreen( + configuration = configuration, + name = name, + email = email, + password = password, + ) + val authState by authUI.authStateFlow().collectAsState(AuthState.Idle) + currentAuthState = authState + } + + // Wait for the navigation to settle and UI to be ready + composeTestRule.waitForIdle() + shadowOf(Looper.getMainLooper()).idle() + + composeTestRule.onNodeWithText(stringProvider.signInAnonymously) + .assertIsDisplayed() + .performClick() + composeTestRule.waitForIdle() + + println("TEST: Pumping looper after click...") + shadowOf(Looper.getMainLooper()).idle() + + // Wait for auth state to transition to Success + println("TEST: Waiting for auth state change... Current state: $currentAuthState") + composeTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) { + shadowOf(Looper.getMainLooper()).idle() + println( + "TEST: Auth state during wait: $currentAuthState, isAnonymous" + + " - ${authUI.auth.currentUser?.isAnonymous}" + ) + currentAuthState is AuthState.Success + } + + composeTestRule.onNodeWithText("isAnonymous - true") + .assertIsDisplayed() + + assertThat(authUI.auth.currentUser!!.isAnonymous).isEqualTo(true) + + val anonymousUserUID = authUI.auth.currentUser!!.uid + + composeTestRule.onNodeWithText("Upgrade with Email") + .performClick() + + composeTestRule.waitForIdle() + + composeTestRule.onNodeWithText(stringProvider.signupPageTitle.uppercase()) + .assertIsDisplayed() + .performClick() + composeTestRule.onNodeWithText(stringProvider.emailHint) + .assertIsDisplayed() + .performTextInput(email) + composeTestRule.onNodeWithText(stringProvider.nameHint) + .assertIsDisplayed() + .performTextInput(name) + composeTestRule.onNodeWithText(stringProvider.passwordHint) + .performScrollTo() + .assertIsDisplayed() + .performTextInput(password) + composeTestRule.onNodeWithText(stringProvider.confirmPasswordHint) + .performScrollTo() + .assertIsDisplayed() + .performTextInput(password) + composeTestRule.onNodeWithText(stringProvider.signupPageTitle.uppercase()) + .performScrollTo() + .assertIsDisplayed() + .performClick() + + composeTestRule.waitForIdle() + println("TEST: Pumping looper after click...") + shadowOf(Looper.getMainLooper()).idle() + + // Wait for auth state to transition to RequiresEmailVerification + println("TEST: Waiting for auth state change... Current state: $currentAuthState") + composeTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) { + shadowOf(Looper.getMainLooper()).idle() + println("TEST: Auth state during wait: $currentAuthState") + currentAuthState is AuthState.RequiresEmailVerification + } + + // Verify the auth state and user properties + println( + "TEST: Verifying final auth state: $currentAuthState, " + + "anonymous user uid - $anonymousUserUID, linked user uid - " + + authUI.auth.currentUser!!.uid + ) + assertThat(currentAuthState) + .isInstanceOf(AuthState.RequiresEmailVerification::class.java) + assertThat(authUI.auth.currentUser).isNotNull() + assertThat(authUI.auth.currentUser!!.uid).isEqualTo(anonymousUserUID) + assertThat(authUI.auth.currentUser!!.isAnonymous).isEqualTo(false) + assertThat(authUI.auth.currentUser!!.email) + .isEqualTo(email) + } + + @Composable + private fun TestAuthScreen( + configuration: AuthUIConfiguration, + name: String = "", + email: String = "", + password: String = "", + ) { + authUI.signOut(applicationContext) + composeTestRule.waitForIdle() + shadowOf(Looper.getMainLooper()).idle() + + FirebaseAuthScreen( + configuration = configuration, + authUI = authUI, + onSignInSuccess = { result -> }, + onSignInFailure = { exception: AuthException -> }, + onSignInCancelled = {}, + authenticatedContent = { state, uiContext -> + when (state) { + is AuthState.Success -> { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + "Authenticated User - (Success): ${state.user.email}", + textAlign = TextAlign.Center + ) + Text( + "UID - ${state.user.uid}", + textAlign = TextAlign.Center + ) + Text( + "isAnonymous - ${state.user.isAnonymous}", + textAlign = TextAlign.Center + ) + Text( + "Providers - " + + "${state.user.providerData.map { it.providerId }}", + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + if (state.user.isAnonymous) { + Button( + onClick = { + uiContext.onNavigate(AuthRoute.Email) + } + ) { + Text("Upgrade with Email") + } + } + } + } + } + } + ) + } +} \ No newline at end of file diff --git a/e2eTest/src/test/java/com/firebase/ui/auth/compose/ui/screens/EmailAuthScreenTest.kt b/e2eTest/src/test/java/com/firebase/ui/auth/compose/ui/screens/EmailAuthScreenTest.kt index f6b4cf7a1..77cbc8893 100644 --- a/e2eTest/src/test/java/com/firebase/ui/auth/compose/ui/screens/EmailAuthScreenTest.kt +++ b/e2eTest/src/test/java/com/firebase/ui/auth/compose/ui/screens/EmailAuthScreenTest.kt @@ -121,7 +121,7 @@ class EmailAuthScreenTest { } composeTestRule.setContent { - FirebaseAuthScreen(configuration = configuration) + TestAuthScreen(configuration = configuration) } composeTestRule.onNodeWithText(stringProvider.signInDefault) @@ -156,7 +156,7 @@ class EmailAuthScreenTest { var currentAuthState: AuthState = AuthState.Idle composeTestRule.setContent { - FirebaseAuthScreen(configuration = configuration) + TestAuthScreen(configuration = configuration) val authState by authUI.authStateFlow().collectAsState(AuthState.Idle) currentAuthState = authState } @@ -242,7 +242,7 @@ class EmailAuthScreenTest { var currentAuthState: AuthState = AuthState.Idle composeTestRule.setContent { - FirebaseAuthScreen(configuration = configuration) + TestAuthScreen(configuration = configuration) val authState by authUI.authStateFlow().collectAsState(AuthState.Idle) currentAuthState = authState } @@ -309,7 +309,7 @@ class EmailAuthScreenTest { var currentAuthState: AuthState = AuthState.Idle composeTestRule.setContent { - FirebaseAuthScreen(configuration = configuration) + TestAuthScreen(configuration = configuration) val authState by authUI.authStateFlow().collectAsState(AuthState.Idle) currentAuthState = authState } @@ -391,7 +391,7 @@ class EmailAuthScreenTest { var currentAuthState: AuthState = AuthState.Idle composeTestRule.setContent { - FirebaseAuthScreen(configuration = configuration) + TestAuthScreen(configuration = configuration) val authState by authUI.authStateFlow().collectAsState(AuthState.Idle) currentAuthState = authState } @@ -484,7 +484,7 @@ class EmailAuthScreenTest { var currentAuthState: AuthState = AuthState.Idle composeTestRule.setContent { - FirebaseAuthScreen(configuration = configuration) + TestAuthScreen(configuration = configuration) val authState by authUI.authStateFlow().collectAsState(AuthState.Idle) currentAuthState = authState } @@ -534,7 +534,7 @@ class EmailAuthScreenTest { } @Composable - private fun FirebaseAuthScreen( + private fun TestAuthScreen( configuration: AuthUIConfiguration, onSuccess: ((AuthResult) -> Unit) = {}, onError: ((AuthException) -> Unit) = {}, diff --git a/e2eTest/src/test/java/com/firebase/ui/auth/compose/ui/screens/PhoneAuthScreenTest.kt b/e2eTest/src/test/java/com/firebase/ui/auth/compose/ui/screens/PhoneAuthScreenTest.kt index c9490dbb2..6eac57e8b 100644 --- a/e2eTest/src/test/java/com/firebase/ui/auth/compose/ui/screens/PhoneAuthScreenTest.kt +++ b/e2eTest/src/test/java/com/firebase/ui/auth/compose/ui/screens/PhoneAuthScreenTest.kt @@ -122,7 +122,7 @@ class PhoneAuthScreenTest { } composeTestRule.setContent { - FirebaseAuthScreen(configuration = configuration) + TestAuthScreen(configuration = configuration) } composeTestRule.onNodeWithText(stringProvider.enterPhoneNumberTitle) @@ -151,7 +151,7 @@ class PhoneAuthScreenTest { var currentAuthState: AuthState = AuthState.Idle composeTestRule.setContent { - FirebaseAuthScreen(configuration = configuration) + TestAuthScreen(configuration = configuration) val authState by authUI.authStateFlow().collectAsState(AuthState.Idle) currentAuthState = authState } @@ -279,7 +279,7 @@ class PhoneAuthScreenTest { } composeTestRule.setContent { - FirebaseAuthScreen(configuration = configuration) + TestAuthScreen(configuration = configuration) } // Send verification code to get to verification screen @@ -326,7 +326,7 @@ class PhoneAuthScreenTest { } composeTestRule.setContent { - FirebaseAuthScreen(configuration = configuration) + TestAuthScreen(configuration = configuration) } // The country selector should show the default country's dial code (GB = +44) @@ -355,7 +355,7 @@ class PhoneAuthScreenTest { } composeTestRule.setContent { - FirebaseAuthScreen(configuration = configuration) + TestAuthScreen(configuration = configuration) } // Send verification code @@ -394,7 +394,7 @@ class PhoneAuthScreenTest { } composeTestRule.setContent { - FirebaseAuthScreen(configuration = configuration) + TestAuthScreen(configuration = configuration) } // The send verification code button should be enabled since phone number is pre-filled @@ -404,7 +404,7 @@ class PhoneAuthScreenTest { } @Composable - private fun FirebaseAuthScreen( + private fun TestAuthScreen( configuration: AuthUIConfiguration, onSuccess: ((AuthResult) -> Unit) = {}, onError: ((AuthException) -> Unit) = {},