diff --git a/.agents/skills/firebaseui-android-getting-started/SKILL.md b/.agents/skills/firebaseui-android-getting-started/SKILL.md index de9bb5d685..b3ad9822ad 100644 --- a/.agents/skills/firebaseui-android-getting-started/SKILL.md +++ b/.agents/skills/firebaseui-android-getting-started/SKILL.md @@ -61,12 +61,12 @@ android { } dependencies { - implementation("com.firebaseui:firebase-ui-auth:10.0.0-beta03") + implementation("com.firebaseui:firebase-ui-auth:10.0.0") - implementation(platform("com.google.firebase:firebase-bom:34.7.0")) + implementation(platform("com.google.firebase:firebase-bom:34.19.0")) implementation("com.google.firebase:firebase-auth") - implementation(platform("androidx.compose:compose-bom:2025.10.00")) + implementation(platform("androidx.compose:compose-bom:2026.09.00")) implementation("androidx.activity:activity-compose") implementation("androidx.compose.ui:ui") implementation("androidx.compose.material3:material3") @@ -78,10 +78,10 @@ Root/plugin configuration varies by project. If `com.google.gms.google-services` Add provider-specific dependencies only when used. Facebook login needs: ```kotlin -implementation("com.facebook.android:facebook-login:18.0.3") +implementation("com.facebook.android:facebook-login:18.3.0") ``` -Minimum expectations from the FirebaseUI Auth docs: Android SDK 21+, Kotlin 1.9+, Compose compiler 1.5+, and Firebase Auth 22.0.0+. Respect stricter versions already present in the user repo. +Minimum expectations from the FirebaseUI Auth docs: Android SDK 23+, Kotlin 2.3+, Jetpack Compose with the Compose compiler plugin applied, and Firebase BoM 34.0.0+. Respect stricter versions already present in the user repo. ## Predefined Auth Screen Template diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 13fa214ab9..41ece5782c 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -2,8 +2,6 @@ [FirebaseUI](https://github.com/firebase/firebaseui-android) Auth is a library built on top of the Firebase Authentication SDK that provides drop-in UI flows for use in your app. -Caution: Version 10.x is currently a **beta release**. This means that the functionality might change in backward-incompatible ways or have limited support. A beta release is not subject to any SLA or deprecation policy. - The recommended sign-in flow uses Compose screens. For apps that still use Activities, see the [Existing Activity-based apps](#existing-activity-based-apps) section. FirebaseUI Auth provides the following benefits: @@ -20,17 +18,39 @@ FirebaseUI Auth provides the following benefits: 1. If you haven't already, [add Firebase to your Android project](https://firebase.google.com/docs/android/setup). 2. In the [Firebase console](https://console.firebase.google.com/), enable the sign-in methods you want to support. -3. Add FirebaseUI Auth to your app module: +3. Check that your app module meets the minimum requirements: + + - Android SDK 23+ (Android 6.0 Marshmallow) + - Kotlin 2.3+ + - Jetpack Compose, with the Compose compiler plugin applied + - Firebase BoM 34.0.0+ + +4. Add the Compose compiler plugin and FirebaseUI Auth to your app module: ```kotlin +plugins { + id("org.jetbrains.kotlin.plugin.compose") +} + +android { + buildFeatures { + compose = true + } +} + dependencies { // Check Maven Central for the latest version: // https://central.sonatype.com/artifact/com.firebaseui/firebase-ui-auth/versions - implementation("com.firebaseui:firebase-ui-auth:10.0.0-beta05") + implementation("com.firebaseui:firebase-ui-auth:10.0.0") + + // Required: Jetpack Compose + implementation(platform("androidx.compose:compose-bom:2026.09.00")) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.material3:material3") // Required only if Facebook login support is required - // Find the latest Facebook SDK releases here: https://goo.gl/Ce5L94 - implementation("com.facebook.android:facebook-android-sdk:8.x") + // Find the latest Facebook SDK releases here: https://github.com/facebook/facebook-android-sdk/releases + implementation("com.facebook.android:facebook-login:18.3.0") } ``` @@ -122,21 +142,29 @@ class MainActivity : ComponentActivity() { setContent { MyAppTheme { - val configuration = authUIConfiguration { - context = applicationContext - theme = AuthUITheme.fromMaterialTheme() - providers { - provider(AuthProvider.Email()) - provider( - AuthProvider.Google( - scopes = listOf("email"), - serverClientId = null, + // Resolved here: the theme is @Composable, the builder lambda is not. + val authTheme = AuthUITheme.fromMaterialTheme() + + val configuration = remember(authTheme) { + authUIConfiguration { + context = applicationContext + theme = authTheme + providers { + provider(AuthProvider.Email()) + provider( + AuthProvider.Google( + scopes = listOf("email"), + serverClientId = null, + ) ) - ) + } } } - if (authUI.isSignedIn()) { + val authState by remember(authUI) { authUI.authStateFlow() } + .collectAsState(initial = AuthState.Idle) + + if (authState is AuthState.Success) { HomeScreen() } else { FirebaseAuthScreen( @@ -149,7 +177,8 @@ class MainActivity : ComponentActivity() { // Sign in failed }, onSignInCancelled = { - finish() + // The user cancelled one sign-in attempt; the screen resets + // on its own. }, ) } @@ -186,16 +215,25 @@ lifecycleScope.launch { ## Customization -FirebaseUI Auth is customizable, and the simplest way to get started is to set a theme directly in `authUIConfiguration`: +FirebaseUI Auth is customizable, and the simplest way to get started is to set a theme on `authUIConfiguration`: ```kotlin -val configuration = authUIConfiguration { - context = applicationContext - providers { - provider(AuthProvider.Email()) - provider(AuthProvider.Google(scopes = listOf("email"), serverClientId = null)) +@Composable +fun rememberAuthConfiguration(): AuthUIConfiguration { + val appContext = LocalContext.current.applicationContext + // Resolved here: the theme is @Composable, the builder lambda is not. + val authTheme = AuthUITheme.Adaptive + + return remember(appContext, authTheme) { + authUIConfiguration { + context = appContext + providers { + provider(AuthProvider.Email()) + provider(AuthProvider.Google(scopes = listOf("email"), serverClientId = null)) + } + theme = authTheme + } } - theme = AuthUITheme.Adaptive } ``` @@ -214,30 +252,48 @@ For full theming and customization details, including theme precedence, provider If your app still uses Activities and the Activity Result API, you can keep an Activity-based launch flow by using `AuthFlowController`: ```kotlin -private val authLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), -) { result -> - if (result.resultCode == RESULT_OK) { - val user = FirebaseAuth.getInstance().currentUser - // ... - } else { - // User cancelled or sign-in failed +class MainActivity : ComponentActivity() { + private lateinit var authController: AuthFlowController + + // A property initializer, so registration happens before the Activity reaches STARTED. + private val authLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + ) { result -> + if (result.resultCode == RESULT_OK) { + val user = FirebaseAuth.getInstance().currentUser + // ... + } else { + // User cancelled or sign-in failed + } } -} -val configuration = authUIConfiguration { - context = applicationContext - providers { - provider(AuthProvider.Email()) - provider( - AuthProvider.Google( - scopes = listOf("email"), - serverClientId = null, - ) - ) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Email()) + provider( + AuthProvider.Google( + scopes = listOf("email"), + serverClientId = null, + ) + ) + } + } + + authController = FirebaseAuthUI.getInstance().createAuthFlow(configuration) + // Only on first creation: a recreated Activity would otherwise launch a second flow. + if (savedInstanceState == null) { + authLauncher.launch(authController.createIntent(this)) + } } -} -val controller = FirebaseAuthUI.getInstance().createAuthFlow(configuration) -authLauncher.launch(controller.createIntent(this)) + override fun onDestroy() { + super.onDestroy() + // Required. + authController.dispose() + } +} ``` diff --git a/README.md b/README.md index 53571acd93..494a72f4fc 100644 --- a/README.md +++ b/README.md @@ -48,21 +48,21 @@ libraries. ```groovy dependencies { // FirebaseUI for Firebase Realtime Database - implementation 'com.firebaseui:firebase-ui-database:10.0.0-beta05' + implementation 'com.firebaseui:firebase-ui-database:10.0.0' // FirebaseUI for Cloud Firestore - implementation 'com.firebaseui:firebase-ui-firestore:10.0.0-beta05' + implementation 'com.firebaseui:firebase-ui-firestore:10.0.0' // FirebaseUI for Firebase Auth - implementation 'com.firebaseui:firebase-ui-auth:10.0.0-beta05' + implementation 'com.firebaseui:firebase-ui-auth:10.0.0' // FirebaseUI for Cloud Storage - implementation 'com.firebaseui:firebase-ui-storage:10.0.0-beta05' + implementation 'com.firebaseui:firebase-ui-storage:10.0.0' } ``` If you're including the `firebase-ui-auth` dependency, there's a little -[more setup](auth/README.md#configuration) required. +[more setup](auth/README.md#setup) required. After the project is synchronized, we're ready to start using Firebase functionality in our app. diff --git a/auth/README.md b/auth/README.md index 613569d90e..e2b99eb074 100644 --- a/auth/README.md +++ b/auth/README.md @@ -62,6 +62,7 @@ Equivalent FirebaseUI libraries are available for [iOS](https://github.com/fireb - [Creating a Completely Custom Theme](#creating-a-completely-custom-theme) - [Provider Button Styling](#provider-button-styling) - [Screen Transitions](#screen-transitions) + - [Beyond Theming](#beyond-theming) ### Advanced 9. [Advanced Features](#advanced-features) @@ -87,7 +88,7 @@ Ensure your application is configured for use with Firebase. See the [Firebase d **Minimum Requirements:** - Android SDK 23+ (Android 6.0 Marshmallow) -- Kotlin 2.0+ +- Kotlin 2.3+ - Jetpack Compose - Firebase BoM 34.0.0+ @@ -98,19 +99,19 @@ Add the FirebaseUI Auth library dependency to your `build.gradle.kts` (Module): ```kotlin dependencies { // FirebaseUI for Auth - implementation("com.firebaseui:firebase-ui-auth:10.0.0-beta05") + implementation("com.firebaseui:firebase-ui-auth:10.0.0") // Required: Firebase Auth - implementation(platform("com.google.firebase:firebase-bom:34.17.0")) + implementation(platform("com.google.firebase:firebase-bom:34.19.0")) implementation("com.google.firebase:firebase-auth") // Required: Jetpack Compose - implementation(platform("androidx.compose:compose-bom:2026.06.01")) + implementation(platform("androidx.compose:compose-bom:2026.09.00")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.material3:material3") // Optional: Facebook Login (if using FacebookAuthProvider) - implementation("com.facebook.android:facebook-login:16.3.0") + implementation("com.facebook.android:facebook-login:18.3.0") } ``` @@ -282,70 +283,73 @@ val authUI = FirebaseAuthUI.create(app = customApp, auth = customAuth) `AuthUIConfiguration` defines all settings for your authentication flow. Use the DSL builder function for easy configuration: ```kotlin -val authTheme = AuthUITheme.fromMaterialTheme() // @Composable — resolve it here, not below +setContent { + val authTheme = AuthUITheme.fromMaterialTheme() // @Composable — resolve it here, not below -val configuration = authUIConfiguration { - // Required: an application Context. Omitting it throws when the block is evaluated. - context = applicationContext + val configuration = authUIConfiguration { + // Required: an application Context. Omitting it throws when the block is evaluated. + context = applicationContext - // Required: Authentication providers - providers { - provider(AuthProvider.Email()) - provider(AuthProvider.Google()) - provider(AuthProvider.Phone()) - } + // Required: Authentication providers + providers { + provider(AuthProvider.Email()) + provider(AuthProvider.Google()) + provider(AuthProvider.Phone()) + } - // Optional: Theme. AuthUITheme.fromMaterialTheme() and AuthUITheme.Adaptive are - // @Composable, so resolve them above the builder and assign the result here. - theme = authTheme + // Optional: Theme. AuthUITheme.fromMaterialTheme() and AuthUITheme.Adaptive are + // @Composable, so resolve them above the builder and assign the result here. + theme = authTheme + + // Optional: Terms of Service and Privacy Policy URLs + tosUrl = "https://example.com/terms" + privacyPolicyUrl = "https://example.com/privacy" - // Optional: Terms of Service and Privacy Policy URLs - tosUrl = "https://example.com/terms" - privacyPolicyUrl = "https://example.com/privacy" + // Optional: App logo. Wrap the source in an AuthUIAsset — a bare ImageVector is a type error. + logo = AuthUIAsset.Vector(Icons.Default.AccountCircle) - // Optional: App logo. Wrap the source in an AuthUIAsset — a bare ImageVector is a type error. - logo = AuthUIAsset.Vector(Icons.Default.AccountCircle) + // Optional: enable the built-in "Manage MFA" button on the signed-in screen (default: true) + isMfaEnabled = true - // Optional: Enable MFA (default: true) - isMfaEnabled = true + // Optional: Enable Credential Manager (default: true) + isCredentialManagerEnabled = true - // Optional: Enable Credential Manager (default: true) - isCredentialManagerEnabled = true + // Optional: Allow anonymous user upgrade (default: false) + isAnonymousUpgradeEnabled = true - // Optional: Allow anonymous user upgrade (default: false) - isAnonymousUpgradeEnabled = true + // Optional: Require display name on sign-up (default: true) + isDisplayNameRequired = true - // Optional: Require display name on sign-up (default: true) - isDisplayNameRequired = true + // Optional: Allow new email accounts (default: true) + isNewEmailAccountsAllowed = true - // Optional: Allow new email accounts (default: true) - isNewEmailAccountsAllowed = true + // Optional: Always show provider choice even with one provider (default: false) + isProviderChoiceAlwaysShown = false - // Optional: Always show provider choice even with one provider (default: false) - isProviderChoiceAlwaysShown = false + // Optional: Custom string provider for localization + stringProvider = MyCustomStringProvider() - // Optional: Custom string provider for localization - stringProvider = MyCustomStringProvider() + // Optional: Locale override. Applies to the default string provider only, so it has no + // effect alongside a custom stringProvider. + locale = Locale.FRENCH - // Optional: Locale override - locale = Locale.FRENCH + // Optional: when a non-anonymous user is already signed in, link the new credential + // onto that account instead of switching accounts (default: false) + isCredentialLinkingEnabled = false - // Optional: when a non-anonymous user is already signed in, link the new credential - // onto that account instead of switching accounts (default: false) - isCredentialLinkingEnabled = false + // Optional: send password-reset links to your own page rather than the Firebase-hosted + // one (default: null) + passwordResetActionCodeSettings = actionCodeSettings { + url = "https://example.com/reset" + handleCodeInApp = true + setAndroidPackageName(packageName, true, null) + } - // Optional: send password-reset links to your own page rather than the Firebase-hosted - // one (default: null) - passwordResetActionCodeSettings = actionCodeSettings { - url = "https://example.com/reset" - handleCodeInApp = true - setAndroidPackageName(packageName, true, null) + // Optional: resolve an email to its providers with the legacy fetchSignInMethodsForEmail + // call. Only useful if email enumeration protection is disabled on your project + // (default: false) + legacyFetchSignInWithEmail = false } - - // Optional: resolve an email to its providers with the legacy fetchSignInMethodsForEmail - // call. Only useful if email enumeration protection is disabled on your project - // (default: false) - legacyFetchSignInWithEmail = false } ``` @@ -405,27 +409,91 @@ override fun onDestroy() { `AuthState` represents the current state of authentication: +The declarations below are the public shape, not a snippet to paste: the library constructs +every state, and bodies are elided. + ```kotlin -sealed class AuthState { - object Idle : AuthState() - data class Loading(val message: String?) : AuthState() - data class Success(val result: AuthResult?, val user: FirebaseUser, val isNewUser: Boolean = false) : AuthState() - data class Error(val exception: AuthException, val isRecoverable: Boolean) : AuthState() - data class RequiresMfa(val resolver: MultiFactorResolver, val hint: String? = null) : AuthState() - data class RequiresEmailVerification(val user: FirebaseUser, val email: String) : AuthState() - data class RequiresProfileCompletion(val user: FirebaseUser, val missingFields: List = emptyList()) : AuthState() - object Cancelled : AuthState() - object Aborted : AuthState() - object PasswordResetLinkSent : AuthState() - object EmailSignInLinkSent : AuthState() - data class SMSAutoVerified(val credential: PhoneAuthCredential) : AuthState() - data class PhoneNumberVerificationRequired( +abstract class AuthState private constructor() { + + /** + * Whether this is a one-off notification a screen shows once and then resets back to + * `AuthState.Idle`, so it does not leak to a screen created later. + */ + abstract val isNotification: Boolean + + class Idle internal constructor() : AuthState() + class Loading(val message: String? = null) : AuthState() + + class Success internal constructor( + val result: AuthResult?, + val user: FirebaseUser, + val isNewUser: Boolean = false, + val reauthenticatedUid: String? = null, + ) : AuthState() + + class Error(val exception: Exception, val isRecoverable: Boolean = true) : AuthState() + + class Cancelled internal constructor() : AuthState() + class Aborted internal constructor() : AuthState() + + class RequiresMfa(val resolver: MultiFactorResolver, val hint: String? = null) : AuthState() + class RequiresEmailVerification(val user: FirebaseUser, val email: String) : AuthState() + class RequiresProfileCompletion( + val user: FirebaseUser, + val missingFields: List = emptyList(), + ) : AuthState() + + class PasswordResetLinkSent : AuthState() + class EmailSignInLinkSent : AuthState() + class SMSAutoVerified(val credential: PhoneAuthCredential) : AuthState() + class PhoneNumberVerificationRequired( val verificationId: String, - val forceResendingToken: PhoneAuthProvider.ForceResendingToken + val forceResendingToken: PhoneAuthProvider.ForceResendingToken, ) : AuthState() + + /** The phases of one reauthentication request. */ + sealed class Reauthentication : AuthState() { + abstract val requestId: String + abstract val userUid: String + + // Constructor is internal; `request` is an internal type. Read-only surface: + class Required : Reauthentication() { + val user: FirebaseUser get() = ... + val reason: String? get() = ... + } + // Every other phase — Authenticating, AttemptFailed, RequiresMfa, + // PhoneNumberVerificationRequired, SmsAutoVerified, PasswordResetLinkSent, + // EmailSignInLinkSent, Succeeded — is `internal`. Match the sealed parent. + } + + companion object { + @JvmStatic val Idle: Idle = Idle() + @JvmStatic val Cancelled: Cancelled = Cancelled() + @JvmStatic val Aborted: Aborted = Aborted() + } } ``` +Four things about this shape change how you write against it: + +- **It is `abstract`, not `sealed`.** A `when` over an `AuthState` is never exhaustive. Used as an + expression it will not compile without an `else`; used as a statement it compiles and then does + nothing for the branches you left out. Write the `else` either way. +- **Nothing is a `data class`.** There is no `copy()` and no destructuring. The states listed + above hand-write `equals`/`hashCode`, so `==` compares by value there. The `Reauthentication` + phases do not follow that rule: `Required` compares on `requestId` alone, ignoring its other + properties, and the `internal` phases inherit identity equality. Match them with `is`, not `==`. +- **`Error.exception` is typed `Exception`, not `AuthException`.** The library puts an + `AuthException` there, but the type does not promise it and the constructor is public, so an + unchecked cast is a crash waiting to happen. Narrow with `as?`, or normalise it with + `AuthException.from(exception, stringProvider)`. +- **`Idle`, `Cancelled` and `Aborted` are classes with `internal` constructors**, exposed as the + companion values above. Read them as `AuthState.Idle`; test them with `is AuthState.Idle` or `==`. + +`Success.reauthenticatedUid` carries the uid a reauthentication just re-proved, and is `null` on an +ordinary sign-in. `AuthState.Reauthentication` covers the whole reauthentication window — see +[Reauthentication](#reauthentication) for what `authStateFlow()` emits while one is running. + ## Authentication Methods ### Email & Password @@ -620,7 +688,7 @@ Enable anonymous authentication to let users use your app without signing in: val configuration = authUIConfiguration { context = applicationContext providers { - provider(AuthProvider.Anonymous()) + provider(AuthProvider.Anonymous) } // Enable anonymous user upgrade @@ -727,6 +795,12 @@ fun AuthenticationScreen() { **FirebaseAuthScreen Parameters:** +Partial. Seven customisation parameters are omitted here and documented under +[Custom UI with Slots](#custom-ui-with-slots): `customMethodPickerLayout`, +`customMethodPickerTermsConfiguration`, `emailContent`, `phoneContent`, `mfaEnrollmentContent`, +`mfaChallengeContent` and `reauthContent`. Each defaults to `null`, which keeps the built-in +behaviour, so none of them is needed to get a working screen. + | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `configuration` | `AuthUIConfiguration` | *Required* | Authentication configuration (providers, theme, etc.) | @@ -1127,31 +1201,63 @@ launcher.launch(intent) ### MFA Configuration -Enable and configure Multi-Factor Authentication: +Both MFA settings shape enrollment; neither affects sign-in. A user who already has a second +factor enrolled is always challenged for it, because Firebase requires it. + +`isMfaEnabled` on the configuration enables the built-in "Manage MFA" button on the signed-in +screen; when false, the button is shown disabled. `MfaConfiguration` sets which factors can be +enrolled and whether enrollment offers a Skip button, and reaches the library as `FirebaseAuthScreen`'s `mfaConfiguration` parameter — it is +**not** a property of `authUIConfiguration`, which is the mistake worth avoiding here. Both have +defaults; set them to change the defaults. ```kotlin -val mfaConfig = MfaConfiguration( - // Allowed MFA factors (default: [Sms, Totp]) - allowedFactors = listOf(MfaFactor.Sms, MfaFactor.Totp), +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + MyAppTheme { + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Email()) + } + // Enables the "Manage MFA" button. Defaults to true. + isMfaEnabled = true + } - // Optional: Require MFA enrollment (default: false) - requireEnrollment = false, + val mfaConfig = MfaConfiguration( + // Allowed MFA factors (default: [Sms, Totp]) + allowedFactors = listOf(MfaFactor.Sms, MfaFactor.Totp), - // Optional: restrict the SMS enrollment step's country selector, as ISO 3166-1 alpha-2 - // codes (default: null, no restriction). Independent of the phone sign-in provider's own - // allowedCountries — an SMS second factor is configured separately from phone sign-in. - allowedCountries = listOf("US", "CA", "GB") -) + // Optional: remove the Skip button from enrollment (default: false) + requireEnrollment = false, -val configuration = authUIConfiguration { - context = applicationContext - providers { - provider(AuthProvider.Email()) + // Optional: restrict the SMS enrollment step's country selector, as + // ISO 3166-1 alpha-2 codes (default: null, no restriction). Independent of + // the phone sign-in provider's own allowedCountries — an SMS second factor + // is configured separately from phone sign-in. + allowedCountries = listOf("US", "CA", "GB") + ) + + FirebaseAuthScreen( + configuration = configuration, + mfaConfiguration = mfaConfig, + onSignInSuccess = { result -> navigateToHome() }, + onSignInFailure = { exception -> showError(exception) }, + onSignInCancelled = { } + ) + } + } } - isMfaEnabled = true } ``` +`mfaConfiguration` defaults to `MfaConfiguration()`, so omitting it gives SMS and TOTP, optional +enrollment, and no country restriction. The [low-level API](#low-level-api-advanced) has no way to +pass one: the activity `AuthFlowController` launches composes `FirebaseAuthScreen` with that +default. `isMfaEnabled` on the configuration still applies on both paths. + ### MFA Enrollment Prompt users to enroll in MFA after sign-in: @@ -1274,7 +1380,8 @@ FirebaseAuthScreen( // The resolver arrives on AuthState.RequiresMfa, not on the exception. showMfaChallengePrompt() } - } + }, + onSignInCancelled = { /* ... */ } ) ``` @@ -1303,7 +1410,7 @@ fun ManualMfaChallenge(resolver: MultiFactorResolver) { FirebaseUI Auth provides flexible theming options to match your app's design: - **`AuthUITheme.Default`** / **`AuthUITheme.DefaultDark`** / **`AuthUITheme.Adaptive`** - Pre-configured Material Design 3 themes -- **`.copy()`** - Customize specific properties of the default themes (data class) +- **`.copy()`** - Customize specific properties of the default themes - **`fromMaterialTheme()`** - Inherit from your app's existing Material Theme - **Custom theme** - Full control over colors, typography, shapes, and provider button styles @@ -1329,15 +1436,17 @@ val configuration = authUIConfiguration { `AuthUITheme.Adaptive` automatically switches between light and dark themes based on the system setting: ```kotlin -val adaptiveTheme = AuthUITheme.Adaptive // @Composable getter — read it outside the builder +setContent { + val adaptiveTheme = AuthUITheme.Adaptive // @Composable getter — read it outside the builder -val configuration = authUIConfiguration { - context = applicationContext - providers { - provider(AuthProvider.Email()) - provider(AuthProvider.Google()) + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Email()) + provider(AuthProvider.Google()) + } + theme = adaptiveTheme } - theme = adaptiveTheme } ``` @@ -1384,20 +1493,24 @@ FirebaseUI Auth supports two theming patterns with clear precedence rules: The simplest approach is to set the theme only in `authUIConfiguration`: ```kotlin -val adaptiveTheme = AuthUITheme.Adaptive +setContent { + val adaptiveTheme = AuthUITheme.Adaptive -val configuration = authUIConfiguration { - context = applicationContext - providers { - provider(AuthProvider.Email()) + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Email()) + } + theme = adaptiveTheme // Set theme here } - theme = adaptiveTheme // Set theme here -} -FirebaseAuthScreen( - configuration = configuration, - onSignInSuccess = { /* ... */ } -) + FirebaseAuthScreen( + configuration = configuration, + onSignInSuccess = { /* ... */ }, + onSignInFailure = { /* ... */ }, + onSignInCancelled = { /* ... */ } + ) +} ``` **When to use:** This is the recommended pattern for most use cases. It's simple and explicit. @@ -1407,22 +1520,26 @@ FirebaseAuthScreen( You can also wrap `FirebaseAuthScreen` with `AuthUITheme`: ```kotlin -val adaptiveTheme = AuthUITheme.Adaptive +setContent { + val adaptiveTheme = AuthUITheme.Adaptive -val configuration = authUIConfiguration { - context = applicationContext - providers { - provider(AuthProvider.Email()) + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Email()) + } + theme = adaptiveTheme // Theme in configuration } - theme = adaptiveTheme // Theme in configuration -} -AuthUITheme(theme = adaptiveTheme) { // Optional wrapper - Surface(color = MaterialTheme.colorScheme.background) { - FirebaseAuthScreen( - configuration = configuration, - onSignInSuccess = { /* ... */ } - ) + AuthUITheme(theme = adaptiveTheme) { // Optional wrapper + Surface(color = MaterialTheme.colorScheme.background) { + FirebaseAuthScreen( + configuration = configuration, + onSignInSuccess = { /* ... */ }, + onSignInFailure = { /* ... */ }, + onSignInCancelled = { /* ... */ } + ) + } } } ``` @@ -1435,42 +1552,48 @@ Understanding which theme applies is important: 1. **Configuration theme takes precedence:** ```kotlin - val configuration = authUIConfiguration { - context = applicationContext - providers { provider(AuthProvider.Email()) } - theme = AuthUITheme.Default // LIGHT theme - } - - AuthUITheme(theme = AuthUITheme.DefaultDark) { // DARK wrapper - FirebaseAuthScreen(configuration, ...) + setContent { + val configuration = authUIConfiguration { + context = applicationContext + providers { provider(AuthProvider.Email()) } + theme = AuthUITheme.Default // LIGHT theme + } + + AuthUITheme(theme = AuthUITheme.DefaultDark) { // DARK wrapper + FirebaseAuthScreen(configuration, ...) + } + // Result: FirebaseAuthScreen uses LIGHT theme (from configuration) } - // Result: FirebaseAuthScreen uses LIGHT theme (from configuration) ``` 2. **Wrapper as fallback:** ```kotlin - val configuration = authUIConfiguration { - context = applicationContext - providers { provider(AuthProvider.Email()) } - // theme not specified (null) + setContent { + val configuration = authUIConfiguration { + context = applicationContext + providers { provider(AuthProvider.Email()) } + // theme not specified (null) + } + + AuthUITheme(theme = AuthUITheme.DefaultDark) { // DARK wrapper + FirebaseAuthScreen(configuration, ...) + } + // Result: FirebaseAuthScreen inherits DARK theme from wrapper } - - AuthUITheme(theme = AuthUITheme.DefaultDark) { // DARK wrapper - FirebaseAuthScreen(configuration, ...) - } - // Result: FirebaseAuthScreen inherits DARK theme from wrapper ``` 3. **Ultimate fallback:** ```kotlin - val configuration = authUIConfiguration { - context = applicationContext - providers { provider(AuthProvider.Email()) } - // theme not specified (null) + setContent { + val configuration = authUIConfiguration { + context = applicationContext + providers { provider(AuthProvider.Email()) } + // theme not specified (null) + } + + FirebaseAuthScreen(configuration, ...) // No wrapper + // Result: Uses AuthUITheme.Default (light theme) } - - FirebaseAuthScreen(configuration, ...) // No wrapper - // Result: Uses AuthUITheme.Default (light theme) ``` **Best Practice:** For clarity and consistency, always set `theme` in `authUIConfiguration`. Use the wrapper only if you have additional UI outside `FirebaseAuthScreen`. @@ -1497,7 +1620,9 @@ fun App() { FirebaseAuthScreen( configuration = configuration, - onSignInSuccess = { /* ... */ } + onSignInSuccess = { /* ... */ }, + onSignInFailure = { /* ... */ }, + onSignInCancelled = { /* ... */ } ) } } @@ -1506,17 +1631,19 @@ fun App() { You can also customize while inheriting: ```kotlin -val authTheme = AuthUITheme.fromMaterialTheme( - providerButtonShape = RoundedCornerShape(16.dp) // Override button shape -) +setContent { + val authTheme = AuthUITheme.fromMaterialTheme( + providerButtonShape = RoundedCornerShape(16.dp) // Override button shape + ) -val configuration = authUIConfiguration { - context = applicationContext - providers { - provider(AuthProvider.Google()) - provider(AuthProvider.Facebook()) + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Google()) + provider(AuthProvider.Facebook()) + } + theme = authTheme } - theme = authTheme } ``` @@ -1578,38 +1705,42 @@ val configuration = authUIConfiguration { **Option 2: Using `fromMaterialTheme()`:** ```kotlin -val authTheme = AuthUITheme.fromMaterialTheme( - providerButtonShape = RoundedCornerShape(16.dp) -) +setContent { + val authTheme = AuthUITheme.fromMaterialTheme( + providerButtonShape = RoundedCornerShape(16.dp) + ) -val configuration = authUIConfiguration { - context = applicationContext - providers { - provider(AuthProvider.Google()) - provider(AuthProvider.Facebook()) + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Google()) + provider(AuthProvider.Facebook()) + } + theme = authTheme } - theme = authTheme } ``` **Option 3: Creating custom theme:** ```kotlin -val customTheme = AuthUITheme( - colorScheme = MaterialTheme.colorScheme, - typography = MaterialTheme.typography, - shapes = MaterialTheme.shapes, - providerButtonShape = RoundedCornerShape(12.dp) -) +setContent { + val customTheme = AuthUITheme( + colorScheme = MaterialTheme.colorScheme, + typography = MaterialTheme.typography, + shapes = MaterialTheme.shapes, + providerButtonShape = RoundedCornerShape(12.dp) + ) -val configuration = authUIConfiguration { - context = applicationContext - providers { - provider(AuthProvider.Google()) - provider(AuthProvider.Facebook()) - provider(AuthProvider.Email()) + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Google()) + provider(AuthProvider.Facebook()) + provider(AuthProvider.Email()) + } + theme = customTheme } - theme = customTheme } ``` @@ -1649,25 +1780,27 @@ val configuration = authUIConfiguration { **Using `fromMaterialTheme()`:** ```kotlin -val customProviderStyles = mapOf( - "google.com" to ProviderStyleDefaults.Google.copy( - shape = RoundedCornerShape(8.dp), - elevation = 4.dp +setContent { + val customProviderStyles = mapOf( + "google.com" to ProviderStyleDefaults.Google.copy( + shape = RoundedCornerShape(8.dp), + elevation = 4.dp + ) ) -) -val authTheme = AuthUITheme.fromMaterialTheme( - providerButtonShape = RoundedCornerShape(12.dp), - providerStyles = customProviderStyles -) + val authTheme = AuthUITheme.fromMaterialTheme( + providerButtonShape = RoundedCornerShape(12.dp), + providerStyles = customProviderStyles + ) -val configuration = authUIConfiguration { - context = applicationContext - providers { - provider(AuthProvider.Google()) - provider(AuthProvider.Facebook()) + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Google()) + provider(AuthProvider.Facebook()) + } + theme = authTheme } - theme = authTheme } ``` @@ -1711,17 +1844,19 @@ val configuration = authUIConfiguration { Override the colors used by the top app bar shown on auth screens: ```kotlin -val customTheme = AuthUITheme.Default.copy( - topAppBarColors = TopAppBarDefaults.topAppBarColors( - containerColor = Color(0xFF2E7D32), - scrolledContainerColor = Color(0xFF2E7D32), +setContent { + val customTheme = AuthUITheme.Default.copy( + topAppBarColors = TopAppBarDefaults.topAppBarColors( + containerColor = Color(0xFF2E7D32), + scrolledContainerColor = Color(0xFF2E7D32), + ) ) -) -val configuration = authUIConfiguration { - context = applicationContext - providers { provider(AuthProvider.Email()) } - theme = customTheme + val configuration = authUIConfiguration { + context = applicationContext + providers { provider(AuthProvider.Email()) } + theme = customTheme + } } ``` @@ -1867,6 +2002,12 @@ val configuration = authUIConfiguration { > (`NavigationEvent.EDGE_LEFT`, `EDGE_RIGHT` or `EDGE_NONE`) and runs when the gesture *starts*, > so a side effect placed in it fires even for gestures the user goes on to cancel. +### Beyond Theming + +`AuthUITheme` restyles the built-in screens but keeps their layout. To replace a screen's layout entirely, use the `FirebaseAuthScreen` slot parameters described in [Custom UI with Slots](#custom-ui-with-slots). They swap in your own composables while FirebaseUI keeps the authentication logic. + +The demo app's [`fullcustomization`](../app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/) sample uses both: an `AuthUITheme.Default.copy(...)` theme, plus custom UI for every screen slot, from the method picker through MFA and reauthentication to the signed-in screen. + ## Advanced Features ### Anonymous User Upgrade @@ -1874,26 +2015,30 @@ val configuration = authUIConfiguration { Seamlessly upgrade anonymous users to permanent accounts: ```kotlin -// 1. Configure anonymous authentication with upgrade enabled -val configuration = authUIConfiguration { - context = applicationContext - providers { - provider(AuthProvider.Anonymous()) - provider(AuthProvider.Email()) - provider(AuthProvider.Google()) +setContent { + // 1. Configure anonymous authentication with upgrade enabled + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Anonymous) + provider(AuthProvider.Email()) + provider(AuthProvider.Google()) + } + isAnonymousUpgradeEnabled = true } - isAnonymousUpgradeEnabled = true -} -// 2. When user wants to create a permanent account, show auth UI -// The library automatically upgrades the anonymous account if one exists -FirebaseAuthScreen( - configuration = configuration, - onSignInSuccess = { result -> - // Anonymous account has been upgraded (if user was anonymous)! - Toast.makeText(this, "Account created!", Toast.LENGTH_SHORT).show() - } -) + // 2. When user wants to create a permanent account, show auth UI + // The library automatically upgrades the anonymous account if one exists + FirebaseAuthScreen( + configuration = configuration, + onSignInSuccess = { result -> + // Anonymous account has been upgraded (if user was anonymous)! + Toast.makeText(this, "Account created!", Toast.LENGTH_SHORT).show() + }, + onSignInFailure = { /* ... */ }, + onSignInCancelled = { /* ... */ } + ) +} ``` ### Email Link Sign-In @@ -2054,44 +2199,32 @@ val configuration = authUIConfiguration { Every input and button on the auth screens carries a stable, public test tag, and FirebaseUI exposes those tags as Android resource ids automatically — no setup required in your app. This is what lets [Firebase Test Lab's Robo test](https://firebase.google.com/docs/test-lab/android/robo-ux-test) and the Google Play Console's pre-launch report drive a real sign-in during automated testing, instead of typing into the wrong field or getting stuck on a screen it can't navigate. -**Why this matters:** a crawler that can't tell which field is the password will happily type a username into it, then hammer "sign in" and "forgot password" until your test account is buried in reset emails. Every field and button below resolves to one unambiguous resource id, so a crawler — or your own instrumented test — can target it directly. - -**Tag reference.** Tags are grouped by screen; import `com.firebase.ui.auth.ui.FirebaseAuthTestTags`. - -| Screen | Constant | Resource id | -|---|---|---| -| Sign in | `SignIn.EMAIL_FIELD` | `fui_sign_in_email_field` | -| | `SignIn.PASSWORD_FIELD` | `fui_sign_in_password_field` | -| | `SignIn.SIGN_IN_BUTTON` | `fui_sign_in_sign_in_button` | -| | `SignIn.SIGN_UP_BUTTON` | `fui_sign_in_sign_up_button` | -| | `SignIn.FORGOT_PASSWORD_BUTTON` | `fui_sign_in_forgot_password_button` | -| | `SignIn.EMAIL_LINK_BUTTON` | `fui_sign_in_email_link_button` | -| Sign up | `SignUp.NAME_FIELD` | `fui_sign_up_name_field` | -| | `SignUp.EMAIL_FIELD` | `fui_sign_up_email_field` | -| | `SignUp.PASSWORD_FIELD` | `fui_sign_up_password_field` | -| | `SignUp.CONFIRM_PASSWORD_FIELD` | `fui_sign_up_confirm_password_field` | -| | `SignUp.SIGN_UP_BUTTON` | `fui_sign_up_sign_up_button` | -| Password recovery | `ResetPassword.EMAIL_FIELD` | `fui_reset_password_email_field` | -| | `ResetPassword.SEND_BUTTON` | `fui_reset_password_send_button` | -| | `ResetPassword.DISMISS_BUTTON` | `fui_reset_password_dismiss_button` | -| Email link sign-in | `EmailLink.EMAIL_FIELD` | `fui_email_link_email_field` | -| | `EmailLink.SEND_LINK_BUTTON` | `fui_email_link_send_link_button` | -| | `EmailLink.DISMISS_BUTTON` | `fui_email_link_dismiss_button` | -| Phone number entry | `PhoneNumber.PHONE_NUMBER_FIELD` | `fui_phone_number_phone_number_field` | -| | `PhoneNumber.COUNTRY_SELECTOR_BUTTON` | `fui_phone_number_country_selector_button` | -| | `PhoneNumber.SEND_CODE_BUTTON` | `fui_phone_number_send_code_button` | -| SMS verification | `VerificationCode.CODE_FIELD` | `fui_verification_code_code_field` | -| | `VerificationCode.VERIFY_BUTTON` | `fui_verification_code_verify_button` | -| | `VerificationCode.RESEND_CODE_BUTTON` | `fui_verification_code_resend_code_button` | -| | `VerificationCode.CHANGE_PHONE_NUMBER_BUTTON` | `fui_verification_code_change_phone_number_button` | -| MFA sign-in challenge | `MfaChallenge.CODE_FIELD` | `fui_mfa_challenge_code_field` | -| | `MfaChallenge.VERIFY_BUTTON` | `fui_mfa_challenge_verify_button` | -| Re-authentication | `Reauth.PASSWORD_FIELD` | `fui_reauth_password_field` | -| | `Reauth.VERIFY_BUTTON` | `fui_reauth_verify_button` | -| | `Reauth.DISMISS_BUTTON` | `fui_reauth_dismiss_button` | -| Method picker | `MethodPicker.PROVIDER_LIST` | `fui_method_picker_provider_list` | -| | `MethodPicker.CONTINUE_AS_BUTTON` | `fui_method_picker_continue_as_button` | -| Country selector | `CountrySelector.COUNTRY_LIST` | `fui_country_selector_country_list` | +**Why this matters:** a crawler that can't tell which field is the password will happily type a username into it, then hammer "sign in" and "forgot password" until your test account is buried in reset emails. Every tagged field and button resolves to one unambiguous resource id, so a crawler — or your own instrumented test — can target it directly. + +**Tag reference.** `com.firebase.ui.auth.ui.FirebaseAuthTestTags` is the list. It groups its +constants into one nested object per screen or component, today: + +`MethodPicker` · `CountrySelector` · `SignIn` · `SignUp` · `ResetPassword` · `EmailLink` · +`PhoneNumber` · `VerificationCode` · `MfaChallenge` · `MfaEnrollment` · `Reauth` · `ErrorRecovery` · +`TermsAndPrivacy` + +**The naming rule.** Every constant's value — and therefore the resource id it resolves to — is its +own name, mechanically: + +``` +fui__ +``` + +So `SignIn.EMAIL_FIELD` is `fui_sign_in_email_field`, and `MfaEnrollment.VERIFY_TOTP_BUTTON` is +`fui_mfa_enrollment_verify_totp_button`. Every constant follows it, with no exceptions. The +`fui__` half is machine-checked: `FirebaseAuthTestTagsTest` walks the object reflectively and +fails the build if a value is not a valid `fui_` resource name, does not carry its group as a +prefix, is not unique, or is nested at the wrong depth. The constant-name half is convention, so +treat the rule as a reliable way to read a value you already have rather than as a licence to +hand-assemble one you have not checked. + +Read the constants off `FirebaseAuthTestTags` rather than off a copy — in an IDE it autocompletes, +and each constant's KDoc says which control it is attached to. `VerificationCode.CODE_FIELD` and `MfaChallenge.CODE_FIELD` each name the whole six-digit input rather than an individual digit box: the field accepts a complete code in a single `ACTION_SET_TEXT`/`performTextInput` call and distributes it across the digit boxes, so one Robo directive or one `performTextInput("123456")` types the entire code. @@ -2127,16 +2260,34 @@ gcloud firebase test android run \ --device model=MediumPhone.arm,version=34 ``` -This is exactly the mechanism a **Play Console pre-launch report** uses, under **Test and release → Testing → Pre-launch report → Settings → Test account credentials**; the resource ids above are what you enter there for the username and password fields. +This is exactly the mechanism a **Play Console pre-launch report** uses, under **Test and release → Testing → Pre-launch report → Settings → Test account credentials**; `fui_sign_in_email_field` and `fui_sign_in_password_field` are what you enter there for the username and password fields. Verified with a real Firebase Test Lab Robo run against the sign-in screen (August 2026): the crawler resolved `fui_sign_in_email_field` and `fui_sign_in_password_field` as `android.widget.EditText` nodes, typed the directive values into both, and submitted via `fui_sign_in_sign_in_button` — along the way also navigating by resource id through sign-up, password recovery, and phone entry, confirming the tagging works generally rather than only where a directive points. Robo's crawling behavior is Google's, not ours, and can change independently of this library; treat this as a snapshot of current behavior rather than a permanent guarantee. -Renaming or removing a tag, or changing the resource id it resolves to, is a breaking change to FirebaseUI's public API — not an internal detail — so a value documented here will not change without a major version bump. +Every constant on `FirebaseAuthTestTags` is public API. Renaming or removing one, changing the value it holds, or changing the resource id that value resolves to is a breaking change — not an internal detail — and will not happen without a major version bump. That covers the whole object, including constants added after this document was written. ### Sign Out & Account Deletion **Sign Out:** +`signOut` does more than `FirebaseAuth.signOut()`. After signing out of Firebase it also clears the +provider-side session for two of the providers, when they are linked to the account, so the next +sign-in genuinely prompts instead of silently reusing the old one: + +- **Google** — clears the Credential Manager state, so the account picker reappears rather than + auto-selecting the previous account. +- **Facebook** — logs out of the Facebook `LoginManager` session. + +No other provider is cleared: a linked Twitter, GitHub, Microsoft or Apple session is left to that +provider's own web session. + +The Facebook step is guarded by a classpath probe, not by the account. Facebook is a `compileOnly` +dependency, so an app that doesn't offer Facebook sign-in has no Facebook SDK at runtime — yet the +account's provider data can still list `facebook.com` if it was linked on another platform. If the +SDK isn't on the classpath, that step is skipped and sign-out still succeeds. + +This all happens inside the `signOut` call below; there is nothing extra to wire up. + ```kotlin @Composable fun SettingsScreen() { @@ -2191,14 +2342,13 @@ Button( FirebaseUI includes default English strings. To add custom localization: ```kotlin -// AuthUIStringProvider declares ~170 abstract `val`s, so override properties, not functions, -// and expect to supply every one — DefaultAuthUIStringProvider is final and cannot be subclassed. -// For most apps, translating the library's own string resources is the lighter option. -class SpanishStringProvider : AuthUIStringProvider { +// Delegate to the defaults and override only the strings you want to change. +class SpanishStringProvider( + defaults: AuthUIStringProvider, +) : AuthUIStringProvider by defaults { override val signInWithEmail = "Iniciar sesión con correo" override val signInWithGoogle = "Iniciar sesión con Google" override val invalidEmailAddress = "Correo inválido" - // ... every other member of AuthUIStringProvider } val configuration = authUIConfiguration { @@ -2206,8 +2356,9 @@ val configuration = authUIConfiguration { providers { provider(AuthProvider.Email()) } - stringProvider = SpanishStringProvider(context) - locale = Locale("es", "ES") + stringProvider = SpanishStringProvider( + DefaultAuthUIStringProvider(applicationContext, Locale("es", "ES")) + ) } ``` @@ -2252,11 +2403,20 @@ One limit worth knowing: `FirebaseAuthUI.signOut`, `withReauth` and `delete` tak ## Error Handling -FirebaseUI provides a comprehensive exception hierarchy: +FirebaseUI provides a comprehensive exception hierarchy. `AuthException` is `abstract`, not +`sealed`, so a `when` over it is never exhaustive — **always end with an `else`**, and treat that +branch as the one that runs for anything this list does not name. + +Most `AuthException` types carry a user-facing `message`, translated wherever the library maps the +Firebase error code (see [Error message resolution](#error-message-resolution)). The six email-link +types are the exception and must not be shown that way, as the branch for them below explains. +Match a specific type when you need a *different action* — offer linking, navigate back, log. If +you only need to show text, the `else` branch covers everything outside those six. ```kotlin FirebaseAuthScreen( configuration = configuration, + onSignInSuccess = { /* ... */ }, onSignInFailure = { exception -> when (exception) { is AuthException.NetworkException -> { @@ -2271,12 +2431,23 @@ FirebaseAuthScreen( is AuthException.WeakPasswordException -> { showSnackbar("Password is too weak. Please use a stronger password.") } + is AuthException.PasswordPolicyViolationException -> { + // `message` already lists each failing constraint, newline-separated, so show + // it rather than generic copy. Translation is partial by design: a constraint + // the library recognises is translated, one it does not is kept as the server's + // English. `failingRequirements` has the raw sentences if you render them. + showSnackbar(exception.message.orEmpty()) + } is AuthException.EmailAlreadyInUseException -> { showSnackbar("An account already exists with this email.") } is AuthException.TooManyRequestsException -> { showSnackbar("Too many attempts. Please try again later.") } + is AuthException.PhoneVerificationCooldownException -> { + // Carries how long is left, so you can say something more useful than "later" + showSnackbar("Try again in ${exception.cooldownSeconds}s.") + } is AuthException.MfaRequiredException -> { // Handled automatically by FirebaseAuthScreen // or show custom MFA challenge @@ -2285,16 +2456,64 @@ FirebaseAuthScreen( // Account needs to be linked showAccountLinkingDialog(exception) } + is AuthException.DifferentSignInMethodRequiredException -> { + // The account already uses another provider. Carries `suggestedSignInMethod` + // so you can send the user straight to it. + showSnackbar("Sign in with ${exception.suggestedSignInMethod} instead.") + } + is AuthException.SignInMethodUnavailableException -> { + // The account exists, but this method can't be used on it. Not recoverable — + // offer another way in rather than a retry. + showSnackbar(exception.message.orEmpty()) + } + is AuthException.MisconfigurationException -> { + // A setup problem in your project, not something the user can fix. The + // diagnostic is on `cause`; `message` is generic translated copy. + Log.e(TAG, "Check the Firebase console", exception.cause) + showSnackbar(exception.message.orEmpty()) + } is AuthException.AuthCancelledException -> { // User cancelled the flow navigateBack() } + // Email link sign-in's own failure modes. Do NOT show `message` for these six: + // they bake untranslated English into their constructors. Take the copy from your + // AuthUIStringProvider instead: + // + // InvalidEmailLinkException -> emailLinkInvalidLinkMessage + // EmailLinkWrongDeviceException -> emailLinkWrongDeviceMessage + // EmailLinkDifferentAnonymousUserException -> emailLinkDifferentAnonymousUserMessage + // EmailLinkPromptForEmailException -> emailLinkPromptForEmailMessage + // EmailMismatchException -> emailMismatchMessage + // EmailLinkCrossDeviceLinkingException -> emailLinkCrossDeviceLinkingMessage( + // exception.providerName + // ?: emailProvider) + // + // Two also need an action, not just copy: EmailLinkPromptForEmailException wants + // the email collected and the link re-submitted, and + // EmailLinkCrossDeviceLinkingException wants the user to sign in with + // `providerName` before linking. + is AuthException.InvalidEmailLinkException, + is AuthException.EmailLinkWrongDeviceException, + is AuthException.EmailLinkCrossDeviceLinkingException, + is AuthException.EmailLinkPromptForEmailException, + is AuthException.EmailLinkDifferentAnonymousUserException, + is AuthException.EmailMismatchException -> { + showEmailLinkRecovery(exception) + } is AuthException.UnknownException -> { showSnackbar("An unexpected error occurred: ${exception.message}") Log.e(TAG, "Auth error", exception) } + // Not optional in practice: AuthException is not sealed, so a future release can + // add a type. Without this branch, that type would fail silently. + else -> { + showSnackbar(exception.message.orEmpty()) + Log.e(TAG, "Auth error", exception) + } } - } + }, + onSignInCancelled = { /* ... */ } ) ``` diff --git a/auth/src/main/java/com/firebase/ui/auth/AuthException.kt b/auth/src/main/java/com/firebase/ui/auth/AuthException.kt index eefefb00b6..8f2da5f492 100644 --- a/auth/src/main/java/com/firebase/ui/auth/AuthException.kt +++ b/auth/src/main/java/com/firebase/ui/auth/AuthException.kt @@ -329,7 +329,7 @@ abstract class AuthException( */ class InvalidEmailLinkException( cause: Throwable? = null - ) : AuthException("You are are attempting to sign in with an invalid email link", cause) + ) : AuthException("You are attempting to sign in with an invalid email link", cause) /** * The email link is being used on a different device than where it was requested. @@ -398,7 +398,7 @@ abstract class AuthException( */ class EmailMismatchException( cause: Throwable? = null - ) : AuthException("You are are attempting to sign in a different email " + + ) : AuthException("You are attempting to sign in with a different email " + "than previously provided", cause) companion object { @@ -1019,8 +1019,10 @@ abstract class AuthException( } } + private val NUMBER_PATTERN = Regex("\\d+") + private fun firstNumberIn(text: String): Int? = - Regex("\\d+").find(text)?.value?.toIntOrNull() + NUMBER_PATTERN.find(text)?.value?.toIntOrNull() // Finds the [...] content that immediately follows PASSWORD_DOES_NOT_MEET_REQUIREMENTS // in both FirebaseException and FirebaseAuthWeakPasswordException messages. diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 2f69eff1a1..560ddd0b9c 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -1,5 +1,5 @@ object Config { - const val version = "10.0.0-beta05" + const val version = "10.0.0" val submodules = listOf("auth", "common", "firestore", "database", "storage") object SdkVersions { diff --git a/docs/upgrade-to-10.0.md b/docs/upgrade-to-10.0.md index 2f91b706b8..222045a560 100644 --- a/docs/upgrade-to-10.0.md +++ b/docs/upgrade-to-10.0.md @@ -1,7 +1,6 @@ # Migration Guide: FirebaseUI Auth 9.x to 10.x This guide helps you migrate from FirebaseUI Auth 9.x (View-based) to 10.x (Compose-based). -Note that 10.x is a beta release. ## Overview @@ -50,10 +49,10 @@ dependencies { // FirebaseUI Auth // Check Maven Central for the latest version: // https://central.sonatype.com/artifact/com.firebaseui/firebase-ui-auth/versions - implementation("com.firebaseui:firebase-ui-auth:10.0.0-beta05") + implementation("com.firebaseui:firebase-ui-auth:10.0.0") // Required: Jetpack Compose - implementation(platform("androidx.compose:compose-bom:2026.06.01")) + implementation(platform("androidx.compose:compose-bom:2026.09.00")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.material3:material3") } @@ -517,7 +516,7 @@ val configuration = authUIConfiguration { ## Checklist -- [ ] Updated dependency to `firebase-ui-auth:10.0.0-beta05` +- [ ] Updated dependency to `firebase-ui-auth:10.0.0` - [ ] Migrated to Jetpack Compose - [ ] Converted Activities to ComponentActivities with `setContent {}` - [ ] Replaced `createSignInIntentBuilder()` with `authUIConfiguration {}` diff --git a/gradle.properties b/gradle.properties index 90ee729300..5f5ea18e3e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ android.nonTransitiveRClass=false android.builtInKotlin=false android.newDsl=false GROUP=com.firebaseui -VERSION_NAME=10.0.0-beta05 +VERSION_NAME=10.0.0 POM_PACKAGING=aar POM_DESCRIPTION=FirebaseUI for Android diff --git a/okf-bundle/repo-tooling/index.md b/okf-bundle/repo-tooling/index.md index 509493047c..5277439af8 100644 --- a/okf-bundle/repo-tooling/index.md +++ b/okf-bundle/repo-tooling/index.md @@ -13,7 +13,7 @@ timestamp: 2026-07-31T00:00:00Z - Root build: `build.gradle.kts`, `settings.gradle`, `gradle.properties`, Wrapper via `./gradlew` - Version catalog: `gradle/libs.versions.toml` - Shared constants: `buildSrc/src/main/kotlin/Config.kt` - - `Config.version` (e.g. `10.0.0-beta04`) + - `Config.version` (e.g. `10.0.0`) - `Config.submodules` = `auth`, `common`, `firestore`, `database`, `storage` - `Config.SdkVersions` — compile / target / min - Checkstyle: `library/quality/checkstyle.xml` diff --git a/storage/README.md b/storage/README.md index 8c76b69058..7b1e66b9f9 100644 --- a/storage/README.md +++ b/storage/README.md @@ -28,10 +28,10 @@ If you're not already using Glide in your application, add the following depende to your `app/build.gradle` file: ```groovy -// Find the latest Glide releases here: https://goo.gl/LpksbR -implementation 'com.github.bumptech.glide:glide:4.x' +// Find the latest Glide releases here: https://github.com/bumptech/glide/releases +implementation 'com.github.bumptech.glide:glide:5.0.9' // If you're using Kotlin (and therefore, kapt), use kapt instead of annotationProcessor -annotationProcessor 'com.github.bumptech.glide:compiler:4.x' +annotationProcessor 'com.github.bumptech.glide:compiler:5.0.9' ``` To load an image from a `StorageReference`, first register an `AppGlideModule`: