Skip to content
21 changes: 0 additions & 21 deletions auth/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -122,27 +122,6 @@
<data android:scheme="@string/facebook_login_protocol_scheme" />
</intent-filter>
</activity>

<!-- Email Link Sign-In Handler Activity for Compose -->
<!-- This activity handles deep links for passwordless email authentication -->
<!-- The host is automatically read from firebase_web_host in config.xml -->
<!-- NOTE: firebase_web_host must be lowercase (e.g., project-id.firebaseapp.com) -->
<activity
android:name=".compose.ui.screens.EmailSignInLinkHandlerActivity"
android:label=""
android:exported="true"
android:theme="@style/FirebaseUI.Transparent">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="@string/firebase_web_host"
tools:ignore="AppLinksAutoVerify,AppLinkUrlError" />
</intent-filter>
</activity>

<provider
android:name=".data.client.AuthUiInitProvider"
android:authorities="${applicationId}.authuiinitprovider"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvidersBui
import com.firebase.ui.auth.compose.configuration.auth_provider.Provider
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.firebase.auth.ActionCodeSettings
import java.util.Locale
Expand All @@ -43,7 +44,7 @@ class AuthUIConfigurationBuilder {
var isAnonymousUpgradeEnabled: Boolean = false
var tosUrl: String? = null
var privacyPolicyUrl: String? = null
var logo: ImageVector? = null
var logo: AuthUIAsset? = null
var passwordResetActionCodeSettings: ActionCodeSettings? = null
var isNewEmailAccountsAllowed: Boolean = true
var isDisplayNameRequired: Boolean = true
Expand Down Expand Up @@ -171,7 +172,7 @@ class AuthUIConfiguration(
/**
* The logo to display on the authentication screens.
*/
val logo: ImageVector? = null,
val logo: AuthUIAsset? = null,

/**
* Configuration for sending email reset link.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.firebase.ui.auth.compose.configuration.auth_provider

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import com.firebase.ui.auth.compose.AuthException
import com.firebase.ui.auth.compose.AuthState
import com.firebase.ui.auth.compose.FirebaseAuthUI
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await

/**
* Creates a remembered launcher function for anonymous sign-in.
*
* @return A launcher function that starts the anonymous sign-in flow when invoked
*
* @see signInAnonymously
* @see createOrLinkUserWithEmailAndPassword for upgrading anonymous accounts
*/
@Composable
internal fun FirebaseAuthUI.rememberAnonymousSignInHandler(): () -> Unit {
val coroutineScope = rememberCoroutineScope()
return remember(this) {
{
coroutineScope.launch {
try {
signInAnonymously()
} catch (e: Exception) {
// Error already handled via auth state flow in signInAnonymously()
// No additional action needed - ErrorRecoveryDialog will show automatically
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we rethrow the error? Or remove the try catch?

}
}
}
}
}

/**
* 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 = "[email protected]",
* 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,8 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
* An interface to wrap the static `EmailAuthProvider.getCredential` method to make it testable.
* @suppress
*/
internal interface CredentialProvider {
// TODO(demolaf): make this internal
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do it in this PR?

interface CredentialProvider {
fun getCredential(email: String, password: String): AuthCredential
}

Expand Down Expand Up @@ -635,8 +636,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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ import kotlinx.coroutines.tasks.await
* }
* ```
*/
internal suspend fun FirebaseAuthUI.createOrLinkUserWithEmailAndPassword(
// TODO(demolaf): make this internal
suspend fun FirebaseAuthUI.createOrLinkUserWithEmailAndPassword(
context: Context,
config: AuthUIConfiguration,
provider: AuthProvider.Email,
Expand Down Expand Up @@ -684,8 +685,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?: ""),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -98,9 +97,14 @@ fun FirebaseAuthScreen(
val pendingLinkingCredential = remember { mutableStateOf<AuthCredential?>(null) }
val pendingResolver = remember { mutableStateOf<MultiFactorResolver?>(null) }

val anonymousProvider = configuration.providers.filterIsInstance<AuthProvider.Anonymous>().firstOrNull()
val emailProvider = configuration.providers.filterIsInstance<AuthProvider.Email>().firstOrNull()
val facebookProvider = configuration.providers.filterIsInstance<AuthProvider.Facebook>().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(
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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()
Expand Down
Loading