From 7caeae2d4b55d1fb7a6083b6fcc059baa3178c54 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:56:14 +0100 Subject: [PATCH 01/15] docs(auth): correct the auth README against the GA API surface --- auth/README.md | 313 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 238 insertions(+), 75 deletions(-) diff --git a/auth/README.md b/auth/README.md index 613569d90..b0e0c9bdd 100644 --- a/auth/README.md +++ b/auth/README.md @@ -406,26 +406,86 @@ override fun onDestroy() { `AuthState` represents the current state of authentication: ```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 + + class Required internal constructor(...) : Reauthentication() { + val user: FirebaseUser + val reason: String? + } + // Every other phase — Authenticating, AttemptFailed, RequiresMfa, + // PhoneNumberVerificationRequired, SmsAutoVerified, PasswordResetLinkSent, + // EmailSignInLinkSent, Succeeded — is `internal`. Match the sealed parent. + } + + companion object { + @JvmStatic val Idle: Idle + @JvmStatic val Cancelled: Cancelled + @JvmStatic val 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 @@ -727,6 +787,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 +1193,59 @@ launcher.launch(intent) ### MFA Configuration -Enable and configure Multi-Factor Authentication: +MFA is configured in two places. `isMfaEnabled` on the configuration turns the feature on or off; +`MfaConfiguration` tunes it, 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, so MFA works without either; 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()) + } + // Turns the feature on. 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: Require MFA 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: @@ -2054,44 +2148,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 +2209,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,8 +2291,11 @@ 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. +// AuthUIStringProvider is an interface. Most members are `val`s, and a smaller set are `fun`s +// taking a value to interpolate (passwordTooShort(minimumLength: Int), +// signedInAs(userIdentifier: String), ...). +// Only a handful of either have a default, so expect to implement nearly all of them — +// DefaultAuthUIStringProvider is final and cannot be subclassed or partially overridden. // For most apps, translating the library's own string resources is the lighter option. class SpanishStringProvider : AuthUIStringProvider { override val signInWithEmail = "Iniciar sesión con correo" @@ -2206,7 +2309,7 @@ val configuration = authUIConfiguration { providers { provider(AuthProvider.Email()) } - stringProvider = SpanishStringProvider(context) + stringProvider = SpanishStringProvider() locale = Locale("es", "ES") } ``` @@ -2252,7 +2355,14 @@ 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. + +Every `AuthException` carries a user-facing `message`, translated wherever the library maps the +Firebase error code (see [Error message resolution](#error-message-resolution)). 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 below covers the whole hierarchy on its own. ```kotlin FirebaseAuthScreen( @@ -2271,12 +2381,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,14 +2406,56 @@ 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. Resolve them through the + // string provider instead. `ErrorRecoveryDialog`'s `getRecoveryMessage` is the + // worked mapping, and two entries do not follow the others' naming: the copy for + // EmailMismatchException is `emailMismatchMessage`, with no emailLink prefix, and + // EmailLinkCrossDeviceLinkingException's is a function taking the provider name, + // `emailLinkCrossDeviceLinkingMessage(providerName)`. + // + // 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) + } } } ) From 6ba69de777350acd12fab3d14286205cc8b07d1b Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Tue, 22 Sep 2026 11:26:02 +0100 Subject: [PATCH 02/15] fix(auth): repair the broken copy in two email-link exception messages --- auth/src/main/java/com/firebase/ui/auth/AuthException.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 eefefb00b..195107ac5 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 { From f0883e55fc479e057a5bf3d51e01ab48dd825eb9 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:09:24 +0100 Subject: [PATCH 03/15] docs(auth): address review on the auth README corrections --- auth/README.md | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/auth/README.md b/auth/README.md index b0e0c9bdd..62f76325f 100644 --- a/auth/README.md +++ b/auth/README.md @@ -405,6 +405,9 @@ 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 abstract class AuthState private constructor() { @@ -449,9 +452,10 @@ abstract class AuthState private constructor() { abstract val requestId: String abstract val userUid: String - class Required internal constructor(...) : Reauthentication() { - val user: FirebaseUser - val reason: 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, @@ -459,9 +463,9 @@ abstract class AuthState private constructor() { } companion object { - @JvmStatic val Idle: Idle - @JvmStatic val Cancelled: Cancelled - @JvmStatic val Aborted: Aborted + @JvmStatic val Idle: Idle = Idle() + @JvmStatic val Cancelled: Cancelled = Cancelled() + @JvmStatic val Aborted: Aborted = Aborted() } } ``` @@ -2359,10 +2363,11 @@ FirebaseUI provides a comprehensive exception hierarchy. `AuthException` is `abs `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. -Every `AuthException` carries a user-facing `message`, translated wherever the library maps the -Firebase error code (see [Error message resolution](#error-message-resolution)). 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 below covers the whole hierarchy on its own. +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( @@ -2427,12 +2432,17 @@ FirebaseAuthScreen( navigateBack() } // Email link sign-in's own failure modes. Do NOT show `message` for these six: - // they bake untranslated English into their constructors. Resolve them through the - // string provider instead. `ErrorRecoveryDialog`'s `getRecoveryMessage` is the - // worked mapping, and two entries do not follow the others' naming: the copy for - // EmailMismatchException is `emailMismatchMessage`, with no emailLink prefix, and - // EmailLinkCrossDeviceLinkingException's is a function taking the provider name, - // `emailLinkCrossDeviceLinkingMessage(providerName)`. + // 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 From 0108b777cd3ddf0e2e35908bea1bb05fe10bd31c Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:49:59 +0100 Subject: [PATCH 04/15] docs(auth): point the theming section at the slot APIs --- auth/README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/auth/README.md b/auth/README.md index 62f76325f..0ee5ece14 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) @@ -1401,7 +1402,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 @@ -1965,6 +1966,18 @@ 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/FullCustomizationDemoActivity.kt) +sample uses both: an `AuthUITheme.Default.copy(...)` theme, plus custom UI for every slot, from the +method picker through MFA and reauthentication to the signed-in screen. + ## Advanced Features ### Anonymous User Upgrade From e5aad3640af0f22e010b410f1ed4abe4a15412be Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:03:50 +0100 Subject: [PATCH 05/15] docs(auth): tighten the beyond theming wording and link --- auth/README.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/auth/README.md b/auth/README.md index 0ee5ece14..8a3fb20ed 100644 --- a/auth/README.md +++ b/auth/README.md @@ -1968,15 +1968,9 @@ val configuration = authUIConfiguration { ### 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/FullCustomizationDemoActivity.kt) -sample uses both: an `AuthUITheme.Default.copy(...)` theme, plus custom UI for every slot, from the -method picker through MFA and reauthentication to the signed-in screen. +`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 From 69e55bf0101c68219c2411e3224747423159fccf Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Mon, 2 Nov 2026 16:07:47 +0100 Subject: [PATCH 06/15] docs: correct the getting-started guide against the GA API surface --- GETTING_STARTED.md | 116 ++++++++++++++++++++++++++++++++------------- 1 file changed, 83 insertions(+), 33 deletions(-) diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 13fa214ab..2da4ab694 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -20,17 +20,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.0+ + - 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") + // 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") + implementation("com.facebook.android:facebook-login:18.3.0") } ``` @@ -122,9 +144,12 @@ class MainActivity : ComponentActivity() { setContent { MyAppTheme { + // Resolved here: the theme is @Composable, the builder lambda is not. + val authTheme = AuthUITheme.fromMaterialTheme() + val configuration = authUIConfiguration { context = applicationContext - theme = AuthUITheme.fromMaterialTheme() + theme = authTheme providers { provider(AuthProvider.Email()) provider( @@ -136,7 +161,10 @@ class MainActivity : ComponentActivity() { } } - if (authUI.isSignedIn()) { + val authState by remember(authUI) { authUI.authStateFlow() } + .collectAsState(initial = AuthState.Idle) + + if (authState is AuthState.Success) { HomeScreen() } else { FirebaseAuthScreen( @@ -186,16 +214,23 @@ 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 authUIConfiguration { + context = appContext + providers { + provider(AuthProvider.Email()) + provider(AuthProvider.Google(scopes = listOf("email"), serverClientId = null)) + } + theme = authTheme } - theme = AuthUITheme.Adaptive } ``` @@ -214,30 +249,45 @@ 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) + authLauncher.launch(authController.createIntent(this)) } -} -val controller = FirebaseAuthUI.getInstance().createAuthFlow(configuration) -authLauncher.launch(controller.createIntent(this)) + override fun onDestroy() { + super.onDestroy() + // Required. + authController.dispose() + } +} ``` From a5c93f83de914e39eb03ecb568bae0deba2c4b10 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:43:18 +0100 Subject: [PATCH 07/15] docs: address review on the getting-started guide --- GETTING_STARTED.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 2da4ab694..306964164 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -223,13 +223,15 @@ fun rememberAuthConfiguration(): AuthUIConfiguration { // Resolved here: the theme is @Composable, the builder lambda is not. val authTheme = AuthUITheme.Adaptive - return authUIConfiguration { - context = appContext - providers { - provider(AuthProvider.Email()) - provider(AuthProvider.Google(scopes = listOf("email"), serverClientId = null)) + return remember(appContext, authTheme) { + authUIConfiguration { + context = appContext + providers { + provider(AuthProvider.Email()) + provider(AuthProvider.Google(scopes = listOf("email"), serverClientId = null)) + } + theme = authTheme } - theme = authTheme } } ``` @@ -281,7 +283,10 @@ class MainActivity : ComponentActivity() { } authController = FirebaseAuthUI.getInstance().createAuthFlow(configuration) - authLauncher.launch(authController.createIntent(this)) + // Only on first creation: a recreated Activity would otherwise launch a second flow. + if (savedInstanceState == null) { + authLauncher.launch(authController.createIntent(this)) + } } override fun onDestroy() { From 2958b14933d68ac7b0b594ffbb62c3cec961c62d Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:53:51 +0100 Subject: [PATCH 08/15] docs(auth): give the composable theme snippets a composable scope --- auth/README.md | 276 ++++++++++++++++++++++++++----------------------- 1 file changed, 146 insertions(+), 130 deletions(-) diff --git a/auth/README.md b/auth/README.md index 8a3fb20ed..b56000723 100644 --- a/auth/README.md +++ b/auth/README.md @@ -283,70 +283,72 @@ 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 MFA (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 - 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 + } } ``` @@ -1428,15 +1430,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 } ``` @@ -1483,20 +1487,22 @@ 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 = { /* ... */ } + ) +} ``` **When to use:** This is the recommended pattern for most use cases. It's simple and explicit. @@ -1506,22 +1512,24 @@ 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 = { /* ... */ } + ) + } } } ``` @@ -1605,17 +1613,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 } ``` @@ -1677,17 +1687,19 @@ 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 } ``` @@ -1748,25 +1760,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 } ``` @@ -1979,26 +1993,28 @@ The demo app's [`fullcustomization`](../app/src/main/java/com/firebaseui/android 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() + } + ) +} ``` ### Email Link Sign-In From 5ee117037554e4b543c4f62761f7388b18e2b723 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:03:48 +0100 Subject: [PATCH 09/15] docs: drop the beta markers now that 10.0.0 has shipped --- .../skills/firebaseui-android-getting-started/SKILL.md | 2 +- GETTING_STARTED.md | 4 +--- README.md | 8 ++++---- auth/README.md | 2 +- docs/upgrade-to-10.0.md | 5 ++--- okf-bundle/repo-tooling/index.md | 2 +- 6 files changed, 10 insertions(+), 13 deletions(-) diff --git a/.agents/skills/firebaseui-android-getting-started/SKILL.md b/.agents/skills/firebaseui-android-getting-started/SKILL.md index de9bb5d68..a462ba8b0 100644 --- a/.agents/skills/firebaseui-android-getting-started/SKILL.md +++ b/.agents/skills/firebaseui-android-getting-started/SKILL.md @@ -61,7 +61,7 @@ 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("com.google.firebase:firebase-auth") diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 306964164..d14141713 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: @@ -43,7 +41,7 @@ android { 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")) diff --git a/README.md b/README.md index 53571acd9..5c873fc28 100644 --- a/README.md +++ b/README.md @@ -48,16 +48,16 @@ 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' } ``` diff --git a/auth/README.md b/auth/README.md index b56000723..db0b52a17 100644 --- a/auth/README.md +++ b/auth/README.md @@ -99,7 +99,7 @@ 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")) diff --git a/docs/upgrade-to-10.0.md b/docs/upgrade-to-10.0.md index 2f91b706b..467c430e6 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,7 +49,7 @@ 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")) @@ -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/okf-bundle/repo-tooling/index.md b/okf-bundle/repo-tooling/index.md index 509493047..5277439af 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` From 2f675fb05ebcf4e0ec8d596e3a18d42eae89d998 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:06:47 +0100 Subject: [PATCH 10/15] chore: set the version to 10.0.0 to match the published release --- buildSrc/src/main/kotlin/Config.kt | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 2f69eff1a..560ddd0b9 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/gradle.properties b/gradle.properties index 90ee72930..5f5ea18e3 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 From 376c5ebb21e4e3dde224e7d6cc5d0434c1c851ef Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Tue, 3 Nov 2026 15:23:53 +0100 Subject: [PATCH 11/15] docs: address the fresh-eyes review of the auth docs --- .../SKILL.md | 2 +- GETTING_STARTED.md | 5 +- README.md | 2 +- auth/README.md | 161 ++++++++++-------- 4 files changed, 96 insertions(+), 74 deletions(-) diff --git a/.agents/skills/firebaseui-android-getting-started/SKILL.md b/.agents/skills/firebaseui-android-getting-started/SKILL.md index a462ba8b0..3eb30df2d 100644 --- a/.agents/skills/firebaseui-android-getting-started/SKILL.md +++ b/.agents/skills/firebaseui-android-getting-started/SKILL.md @@ -81,7 +81,7 @@ Add provider-specific dependencies only when used. Facebook login needs: implementation("com.facebook.android:facebook-login:18.0.3") ``` -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 d14141713..afab7ba90 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -21,7 +21,7 @@ FirebaseUI Auth provides the following benefits: 3. Check that your app module meets the minimum requirements: - Android SDK 23+ (Android 6.0 Marshmallow) - - Kotlin 2.0+ + - Kotlin 2.3+ - Jetpack Compose, with the Compose compiler plugin applied - Firebase BoM 34.0.0+ @@ -175,7 +175,8 @@ class MainActivity : ComponentActivity() { // Sign in failed }, onSignInCancelled = { - finish() + // The user backed out of one provider; the screen returns to the + // method picker on its own. }, ) } diff --git a/README.md b/README.md index 5c873fc28..494a72f4f 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ dependencies { ``` 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 db0b52a17..3f56f8f27 100644 --- a/auth/README.md +++ b/auth/README.md @@ -88,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+ @@ -308,7 +308,7 @@ setContent { // Optional: App logo. Wrap the source in an AuthUIAsset — a bare ImageVector is a type error. logo = AuthUIAsset.Vector(Icons.Default.AccountCircle) - // Optional: Enable MFA (default: true) + // Optional: show the built-in "Manage MFA" entry on the signed-in screen (default: true) isMfaEnabled = true // Optional: Enable Credential Manager (default: true) @@ -1200,10 +1200,14 @@ launcher.launch(intent) ### MFA Configuration -MFA is configured in two places. `isMfaEnabled` on the configuration turns the feature on or off; -`MfaConfiguration` tunes it, 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, so MFA works without either; set them to change the defaults. +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 shows or hides the built-in "Manage MFA" entry on the +signed-in screen. `MfaConfiguration` sets which factors can be enrolled and whether the enrollment flow +can be skipped, 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 class MainActivity : ComponentActivity() { @@ -1217,7 +1221,7 @@ class MainActivity : ComponentActivity() { providers { provider(AuthProvider.Email()) } - // Turns the feature on. Defaults to true. + // Shows the "Manage MFA" entry. Defaults to true. isMfaEnabled = true } @@ -1225,7 +1229,7 @@ class MainActivity : ComponentActivity() { // Allowed MFA factors (default: [Sms, Totp]) allowedFactors = listOf(MfaFactor.Sms, MfaFactor.Totp), - // Optional: Require MFA enrollment (default: false) + // Optional: remove the Skip button from enrollment (default: false) requireEnrollment = false, // Optional: restrict the SMS enrollment step's country selector, as @@ -1375,7 +1379,8 @@ FirebaseAuthScreen( // The resolver arrives on AuthState.RequiresMfa, not on the exception. showMfaChallengePrompt() } - } + }, + onSignInCancelled = { /* ... */ } ) ``` @@ -1500,7 +1505,9 @@ setContent { FirebaseAuthScreen( configuration = configuration, - onSignInSuccess = { /* ... */ } + onSignInSuccess = { /* ... */ }, + onSignInFailure = { /* ... */ }, + onSignInCancelled = { /* ... */ } ) } ``` @@ -1527,7 +1534,9 @@ setContent { Surface(color = MaterialTheme.colorScheme.background) { FirebaseAuthScreen( configuration = configuration, - onSignInSuccess = { /* ... */ } + onSignInSuccess = { /* ... */ }, + onSignInFailure = { /* ... */ }, + onSignInCancelled = { /* ... */ } ) } } @@ -1542,42 +1551,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 + 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) } - - AuthUITheme(theme = AuthUITheme.DefaultDark) { // DARK wrapper - FirebaseAuthScreen(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) - } - - AuthUITheme(theme = AuthUITheme.DefaultDark) { // DARK wrapper - FirebaseAuthScreen(configuration, ...) + 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 } - // 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`. @@ -1604,7 +1619,9 @@ fun App() { FirebaseAuthScreen( configuration = configuration, - onSignInSuccess = { /* ... */ } + onSignInSuccess = { /* ... */ }, + onSignInFailure = { /* ... */ }, + onSignInCancelled = { /* ... */ } ) } } @@ -1706,21 +1723,23 @@ setContent { **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 } ``` @@ -1824,17 +1843,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 + } } ``` @@ -2012,7 +2033,9 @@ setContent { onSignInSuccess = { result -> // Anonymous account has been upgraded (if user was anonymous)! Toast.makeText(this, "Account created!", Toast.LENGTH_SHORT).show() - } + }, + onSignInFailure = { /* ... */ }, + onSignInCancelled = { /* ... */ } ) } ``` @@ -2318,17 +2341,13 @@ Button( FirebaseUI includes default English strings. To add custom localization: ```kotlin -// AuthUIStringProvider is an interface. Most members are `val`s, and a smaller set are `fun`s -// taking a value to interpolate (passwordTooShort(minimumLength: Int), -// signedInAs(userIdentifier: String), ...). -// Only a handful of either have a default, so expect to implement nearly all of them — -// DefaultAuthUIStringProvider is final and cannot be subclassed or partially overridden. -// 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 { @@ -2336,7 +2355,7 @@ val configuration = authUIConfiguration { providers { provider(AuthProvider.Email()) } - stringProvider = SpanishStringProvider() + stringProvider = SpanishStringProvider(DefaultAuthUIStringProvider(applicationContext)) locale = Locale("es", "ES") } ``` @@ -2395,6 +2414,7 @@ you only need to show text, the `else` branch covers everything outside those si ```kotlin FirebaseAuthScreen( configuration = configuration, + onSignInSuccess = { /* ... */ }, onSignInFailure = { exception -> when (exception) { is AuthException.NetworkException -> { @@ -2490,7 +2510,8 @@ FirebaseAuthScreen( Log.e(TAG, "Auth error", exception) } } - } + }, + onSignInCancelled = { /* ... */ } ) ``` From 62f528874025ab0d8500201de9964eebfea86a86 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Tue, 3 Nov 2026 15:28:30 +0100 Subject: [PATCH 12/15] docs(auth): correct the Anonymous provider reference and the MFA and locale notes --- GETTING_STARTED.md | 4 ++-- auth/README.md | 22 ++++++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index afab7ba90..a90291b68 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -175,8 +175,8 @@ class MainActivity : ComponentActivity() { // Sign in failed }, onSignInCancelled = { - // The user backed out of one provider; the screen returns to the - // method picker on its own. + // The user cancelled one sign-in attempt; the screen resets + // on its own. }, ) } diff --git a/auth/README.md b/auth/README.md index 3f56f8f27..d06187414 100644 --- a/auth/README.md +++ b/auth/README.md @@ -308,7 +308,7 @@ setContent { // Optional: App logo. Wrap the source in an AuthUIAsset — a bare ImageVector is a type error. logo = AuthUIAsset.Vector(Icons.Default.AccountCircle) - // Optional: show the built-in "Manage MFA" entry on the signed-in screen (default: true) + // Optional: enable the built-in "Manage MFA" button on the signed-in screen (default: true) isMfaEnabled = true // Optional: Enable Credential Manager (default: true) @@ -329,7 +329,8 @@ setContent { // Optional: Custom string provider for localization stringProvider = MyCustomStringProvider() - // Optional: Locale override + // Optional: Locale override. Applies to the default string provider only, so it has no + // effect alongside a custom stringProvider. locale = Locale.FRENCH // Optional: when a non-anonymous user is already signed in, link the new credential @@ -687,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 @@ -1203,9 +1204,9 @@ launcher.launch(intent) 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 shows or hides the built-in "Manage MFA" entry on the -signed-in screen. `MfaConfiguration` sets which factors can be enrolled and whether the enrollment flow -can be skipped, and reaches the library as `FirebaseAuthScreen`'s `mfaConfiguration` parameter — it is +`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. @@ -1221,7 +1222,7 @@ class MainActivity : ComponentActivity() { providers { provider(AuthProvider.Email()) } - // Shows the "Manage MFA" entry. Defaults to true. + // Enables the "Manage MFA" button. Defaults to true. isMfaEnabled = true } @@ -2019,7 +2020,7 @@ setContent { val configuration = authUIConfiguration { context = applicationContext providers { - provider(AuthProvider.Anonymous()) + provider(AuthProvider.Anonymous) provider(AuthProvider.Email()) provider(AuthProvider.Google()) } @@ -2355,8 +2356,9 @@ val configuration = authUIConfiguration { providers { provider(AuthProvider.Email()) } - stringProvider = SpanishStringProvider(DefaultAuthUIStringProvider(applicationContext)) - locale = Locale("es", "ES") + stringProvider = SpanishStringProvider( + DefaultAuthUIStringProvider(applicationContext, Locale("es", "ES")) + ) } ``` From 12c01cc49a8ea9befb95ee48521388f42ed91af7 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Tue, 3 Nov 2026 15:42:55 +0100 Subject: [PATCH 13/15] perf(auth): compile the password-requirement number pattern once --- auth/src/main/java/com/firebase/ui/auth/AuthException.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 195107ac5..8f2da5f49 100644 --- a/auth/src/main/java/com/firebase/ui/auth/AuthException.kt +++ b/auth/src/main/java/com/firebase/ui/auth/AuthException.kt @@ -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. From 1022f4a1ed82459f073bd27488a9d791777a4fa8 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:06:48 +0100 Subject: [PATCH 14/15] docs: pin the documented dependency versions to the ones 10.0.0 ships with --- .agents/skills/firebaseui-android-getting-started/SKILL.md | 6 +++--- GETTING_STARTED.md | 2 +- auth/README.md | 6 +++--- docs/upgrade-to-10.0.md | 2 +- storage/README.md | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.agents/skills/firebaseui-android-getting-started/SKILL.md b/.agents/skills/firebaseui-android-getting-started/SKILL.md index 3eb30df2d..b3ad9822a 100644 --- a/.agents/skills/firebaseui-android-getting-started/SKILL.md +++ b/.agents/skills/firebaseui-android-getting-started/SKILL.md @@ -63,10 +63,10 @@ android { dependencies { 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,7 +78,7 @@ 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 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. diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index a90291b68..5b605a639 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -49,7 +49,7 @@ dependencies { implementation("androidx.compose.material3:material3") // Required only if Facebook login support is required - // Find the latest Facebook SDK releases here: https://goo.gl/Ce5L94 + // Find the latest Facebook SDK releases here: https://github.com/facebook/facebook-android-sdk/releases implementation("com.facebook.android:facebook-login:18.3.0") } ``` diff --git a/auth/README.md b/auth/README.md index d06187414..e2b99eb07 100644 --- a/auth/README.md +++ b/auth/README.md @@ -102,16 +102,16 @@ dependencies { 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") } ``` diff --git a/docs/upgrade-to-10.0.md b/docs/upgrade-to-10.0.md index 467c430e6..222045a56 100644 --- a/docs/upgrade-to-10.0.md +++ b/docs/upgrade-to-10.0.md @@ -52,7 +52,7 @@ dependencies { 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") } diff --git a/storage/README.md b/storage/README.md index 8c76b6905..7b1e66b9f 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`: From 2cc5515cb6589601ff5faa8f58ae3991c8cb526d Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:04:34 +0100 Subject: [PATCH 15/15] docs: remember the getting-started configuration across auth state changes --- GETTING_STARTED.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 5b605a639..41ece5782 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -145,17 +145,19 @@ class MainActivity : ComponentActivity() { // Resolved here: the theme is @Composable, the builder lambda is not. val authTheme = AuthUITheme.fromMaterialTheme() - val configuration = authUIConfiguration { - context = applicationContext - theme = authTheme - providers { - provider(AuthProvider.Email()) - provider( - AuthProvider.Google( - scopes = listOf("email"), - serverClientId = null, + val configuration = remember(authTheme) { + authUIConfiguration { + context = applicationContext + theme = authTheme + providers { + provider(AuthProvider.Email()) + provider( + AuthProvider.Google( + scopes = listOf("email"), + serverClientId = null, + ) ) - ) + } } }