From 96b834ee7d91e98697ccaeeba8e7f56960c62c2d Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:45:11 +0100 Subject: [PATCH 1/6] docs(auth): correct the auth README against the current API --- auth/README.md | 212 +++++++++++++++++++++++++------------------------ 1 file changed, 107 insertions(+), 105 deletions(-) diff --git a/auth/README.md b/auth/README.md index 1b0c62339..a52bad265 100644 --- a/auth/README.md +++ b/auth/README.md @@ -86,10 +86,10 @@ Equivalent FirebaseUI libraries are available for [iOS](https://github.com/fireb Ensure your application is configured for use with Firebase. See the [Firebase documentation](https://firebase.google.com/docs/android/setup) for setup instructions. **Minimum Requirements:** -- Android SDK 21+ (Android 5.0 Lollipop) -- Kotlin 1.9+ -- Jetpack Compose (Compiler 1.5+) -- Firebase Auth 22.0.0+ +- Android SDK 23+ (Android 6.0 Marshmallow) +- Kotlin 2.0+ +- Jetpack Compose +- Firebase BoM 34.0.0+ ### Installation @@ -105,7 +105,7 @@ dependencies { implementation("com.google.firebase:firebase-auth") // Required: Jetpack Compose - implementation(platform("androidx.compose:compose-bom:2024.01.00")) + implementation(platform("androidx.compose:compose-bom:2026.06.01")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.material3:material3") @@ -164,6 +164,7 @@ class MainActivity : ComponentActivity() { setContent { MyAppTheme { val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Email()) provider(AuthProvider.Google()) @@ -262,7 +263,7 @@ val authUI = FirebaseAuthUI.getInstance(customApp) // Or create with custom auth (for multi-tenancy) val customAuth = Firebase.auth(customApp) -val authUI = FirebaseAuthUI.create(auth = customAuth) +val authUI = FirebaseAuthUI.create(app = customApp, auth = customAuth) ``` **Key Methods:** @@ -282,6 +283,9 @@ val authUI = FirebaseAuthUI.create(auth = customAuth) ```kotlin val configuration = authUIConfiguration { + // Required: an application Context. Omitting it throws when the block is evaluated. + context = applicationContext + // Required: Authentication providers providers { provider(AuthProvider.Email()) @@ -322,6 +326,22 @@ val configuration = authUIConfiguration { // Optional: Locale override locale = Locale.FRENCH + + // Optional: link a new credential onto the signed-in account instead of switching + // accounts when the email already exists (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 + } + + // 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 } ``` @@ -332,29 +352,35 @@ val configuration = authUIConfiguration { ```kotlin val controller = authUI.createAuthFlow(configuration) -lifecycleScope.launch { - // Start the flow - val state = controller.start() +val authLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() +) { /* the flow finished; inspect FirebaseAuth.currentUser or the result extras */ } - when (state) { - is AuthState.Success -> { - // Handle success - val user = state.result.user - } - is AuthState.Error -> { - // Handle error - Log.e(TAG, "Auth failed", state.exception) - } - is AuthState.Cancelled -> { - // User cancelled a single sign-in attempt (e.g. dismissed the - // Credential Manager sheet, backed out of MFA); the flow stays open - } - is AuthState.Aborted -> { - // Flow was ended via controller.cancel() - finish() - } - else -> { - // Handle other states (RequiresMfa, RequiresEmailVerification, etc.) +authLauncher.launch(controller.createIntent(this)) + +// Follow the flow in detail by collecting its state +lifecycleScope.launch { + controller.authStateFlow.collect { state -> + when (state) { + is AuthState.Success -> { + // Handle success + val user = state.user + } + is AuthState.Error -> { + // Handle error + Log.e(TAG, "Auth failed", state.exception) + } + is AuthState.Cancelled -> { + // User cancelled a single sign-in attempt (e.g. dismissed the + // Credential Manager sheet, backed out of MFA); the flow stays open + } + is AuthState.Aborted -> { + // Flow was ended via controller.cancel() + finish() + } + else -> { + // Handle other states (RequiresMfa, RequiresEmailVerification, etc.) + } } } } @@ -435,7 +461,8 @@ val emailProvider = AuthProvider.Email( ) val configuration = authUIConfiguration { - providers = listOf(emailProvider) + context = applicationContext + providers { provider(emailProvider) } } ``` @@ -462,6 +489,7 @@ val phoneProvider = AuthProvider.Phone( ) val configuration = authUIConfiguration { + context = applicationContext providers { provider(phoneProvider) } @@ -485,6 +513,7 @@ val googleProvider = AuthProvider.Google( ) val configuration = authUIConfiguration { + context = applicationContext providers { provider(googleProvider) } @@ -505,6 +534,7 @@ val facebookProvider = AuthProvider.Facebook( ) val configuration = authUIConfiguration { + context = applicationContext providers { provider(facebookProvider) } @@ -565,6 +595,7 @@ val appleProvider = AuthProvider.Apple( ) val configuration = authUIConfiguration { + context = applicationContext providers { provider(twitterProvider) provider(githubProvider) @@ -581,6 +612,7 @@ Enable anonymous authentication to let users use your app without signing in: ```kotlin val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Anonymous()) } @@ -622,6 +654,7 @@ val lineProvider = AuthProvider.GenericOAuth( ) val configuration = authUIConfiguration { + context = applicationContext providers { provider(lineProvider) } @@ -638,6 +671,7 @@ The high-level API provides a complete, opinionated authentication experience wi @Composable fun AuthenticationScreen() { val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Email()) provider(AuthProvider.Google()) @@ -741,11 +775,16 @@ For maximum control, use the `AuthFlowController`: class AuthActivity : ComponentActivity() { private lateinit var controller: AuthFlowController + private val authLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { /* the flow finished */ } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val authUI = FirebaseAuthUI.getInstance() val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Email()) provider(AuthProvider.Google()) @@ -753,10 +792,10 @@ class AuthActivity : ComponentActivity() { } controller = authUI.createAuthFlow(configuration) + authLauncher.launch(controller.createIntent(this)) lifecycleScope.launch { - val state = controller.start() - handleAuthState(state) + controller.authStateFlow.collect { handleAuthState(it) } } } @@ -764,7 +803,7 @@ class AuthActivity : ComponentActivity() { when (state) { is AuthState.Success -> { // Successfully signed in - val user = state.result.user + val user = state.user startActivity(Intent(this, MainActivity::class.java)) finish() } @@ -1060,6 +1099,7 @@ The armed reauthentication lives on the process-cached `FirebaseAuthUI`, so it s ```kotlin val reauth = authUI.createReauthFlow( configuration = authUIConfiguration { + context = applicationContext // Providers are automatically filtered to those linked to the current user }, ) @@ -1088,6 +1128,7 @@ val mfaConfig = MfaConfiguration( ) val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Email()) } @@ -1225,20 +1266,15 @@ Or handle manually: fun ManualMfaChallenge(resolver: MultiFactorResolver) { MfaChallengeScreen( resolver = resolver, - onChallengeComplete = { assertion -> - // Complete sign-in with the assertion - lifecycleScope.launch { - try { - val result = resolver.resolveSignIn(assertion) - navigateToHome() - } catch (e: Exception) { - showError(e) - } - } + auth = FirebaseAuth.getInstance(), + onSuccess = { result -> + // The library resolved the challenge; the user is signed in + navigateToHome() }, onCancel = { navigateBack() - } + }, + onError = { showError(it) } ) } ``` @@ -1258,6 +1294,7 @@ FirebaseUI provides pre-configured themes for light and dark modes: ```kotlin val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Email()) provider(AuthProvider.Google()) @@ -1274,6 +1311,7 @@ val configuration = authUIConfiguration { ```kotlin val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Email()) provider(AuthProvider.Google()) @@ -1372,6 +1410,7 @@ Understanding which theme applies is important: 1. **Configuration theme takes precedence:** ```kotlin val configuration = authUIConfiguration { + context = applicationContext theme = AuthUITheme.Default // LIGHT theme } @@ -1384,6 +1423,7 @@ Understanding which theme applies is important: 2. **Wrapper as fallback:** ```kotlin val configuration = authUIConfiguration { + context = applicationContext // theme not specified (null) } @@ -1396,6 +1436,7 @@ Understanding which theme applies is important: 3. **Ultimate fallback:** ```kotlin val configuration = authUIConfiguration { + context = applicationContext // theme not specified (null) } @@ -1414,6 +1455,7 @@ Use `fromMaterialTheme()` to automatically inherit your app's Material Design th fun App() { MyAppTheme { // Your existing Material3 theme val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Email()) } @@ -1432,6 +1474,7 @@ You can also customize while inheriting: ```kotlin val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Google()) provider(AuthProvider.Facebook()) @@ -1467,6 +1510,7 @@ val customTheme = AuthUITheme( ) val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Email()) } @@ -1486,7 +1530,12 @@ val customTheme = AuthUITheme.Default.copy( ) val configuration = authUIConfiguration { - providers = listOf(AuthProvider.Google(), AuthProvider.Facebook(), AuthProvider.Email()) + context = applicationContext + providers { + provider(AuthProvider.Google()) + provider(AuthProvider.Facebook()) + provider(AuthProvider.Email()) + } theme = customTheme } ``` @@ -1495,6 +1544,7 @@ val configuration = authUIConfiguration { ```kotlin val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Google()) provider(AuthProvider.Facebook()) @@ -1516,6 +1566,7 @@ val customTheme = AuthUITheme( ) val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Google()) provider(AuthProvider.Facebook()) @@ -1549,6 +1600,7 @@ val customTheme = AuthUITheme.Default.copy( ) val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Google()) provider(AuthProvider.Facebook()) @@ -1568,6 +1620,7 @@ val customProviderStyles = mapOf( ) val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Google()) provider(AuthProvider.Facebook()) @@ -1604,6 +1657,7 @@ val customTheme = AuthUITheme.Default.copy( ) val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Google()) // Uses custom shape (24.dp) provider(AuthProvider.Facebook()) // Uses custom shape (8.dp) @@ -1626,6 +1680,7 @@ val customTheme = AuthUITheme.Default.copy( ) val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Email()) } theme = customTheme } @@ -1729,6 +1784,7 @@ 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()) @@ -1764,6 +1820,7 @@ val emailProvider = AuthProvider.Email( ) val configuration = authUIConfiguration { + context = applicationContext providers { provider(emailProvider) } @@ -1893,6 +1950,7 @@ Credential Manager is enabled by default. To disable: ```kotlin val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Email()) } @@ -2050,6 +2108,7 @@ class SpanishStringProvider(context: Context) : AuthUIStringProvider { } val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Email()) } @@ -2126,6 +2185,7 @@ var errorState by remember { mutableStateOf(null) } errorState?.let { error -> ErrorRecoveryDialog( error = error, + stringProvider = DefaultAuthUIStringProvider(context), onRetry = { // Retry the authentication errorState = null @@ -2148,67 +2208,9 @@ errorState?.let { error -> ## Migration Guide -### From FirebaseUI Auth 9.x (View-based) - -The new Compose library has a completely different architecture. Here's how to migrate: - -**Old (9.x - View/Activity based):** - -```java -// Old approach with startActivityForResult -Intent signInIntent = AuthUI.getInstance() - .createSignInIntentBuilder() - .setAvailableProviders(Arrays.asList( - new AuthUI.IdpConfig.EmailBuilder().build(), - new AuthUI.IdpConfig.GoogleBuilder().build() - )) - .setTheme(R.style.AppTheme) - .build(); - -signInLauncher.launch(signInIntent); -``` - -**New (10.x - Compose based):** - -```kotlin -// New approach with Composable -val configuration = authUIConfiguration { - providers { - provider(AuthProvider.Email()) - provider(AuthProvider.Google()) - } - theme = AuthUITheme.fromMaterialTheme() -} - -FirebaseAuthScreen( - configuration = configuration, - onSignInSuccess = { result -> /* ... */ }, - onSignInFailure = { exception -> /* ... */ }, - onSignInCancelled = { /* ... */ } -) -``` - -**Key Changes:** - -1. **Pure Compose** - No more Activities or Intents, everything is Composable -2. **Configuration DSL** - Use `authUIConfiguration {}` instead of `createSignInIntentBuilder()` -3. **Provider Builders** - `AuthProvider.Email()` instead of `IdpConfig.EmailBuilder().build()` -4. **Callbacks** - Direct callback parameters instead of `ActivityResultLauncher` -5. **Theming** - `AuthUITheme` instead of `R.style` theme resources -6. **State Management** - Reactive `Flow` instead of `AuthStateListener` - -**Migration Checklist:** - -- [ ] Update dependency to `firebase-ui-auth:10.0.0-beta02` -- [ ] Convert Activities to Composables -- [ ] Replace Intent-based flow with `FirebaseAuthScreen` -- [ ] Update configuration from builder pattern to DSL -- [ ] Replace theme resources with `AuthUITheme` -- [ ] Update error handling from result codes to `AuthException` -- [ ] Remove `ActivityResultLauncher` and use direct callbacks -- [ ] Update sign-out/delete to use suspend functions - -For a complete migration example, see the [migration guide](../docs/upgrade-to-10.0.md). +Migrating from 9.x? [docs/upgrade-to-10.0.md](../docs/upgrade-to-10.0.md) is the full guide — +dependencies, provider configuration, theming, sign-out and deletion, auth-state observation, and +the Activity-based route for apps that can't use Compose everywhere. --- From df41623cfc64f8281e139b940a902b37e3fd6ff9 Mon Sep 17 00:00:00 2001 From: demolaf Date: Thu, 3 Sep 2026 13:39:02 +0100 Subject: [PATCH 2/6] docs(auth): update Screen Transitions samples to the Navigation 3 AuthUITransitions --- auth/README.md | 80 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 16 deletions(-) diff --git a/auth/README.md b/auth/README.md index a52bad265..2bf4090cf 100644 --- a/auth/README.md +++ b/auth/README.md @@ -1690,13 +1690,16 @@ If left unset (`null`), the top app bar falls back to colors derived from `color ### Screen Transitions -Customize the animations when navigating between screens using the `AuthUITransitions` object: +Customize the animations when navigating between screens using the `AuthUITransitions` object. +Each spec is an `AnimatedContentTransitionScope>` receiver returning one +`ContentTransform`, so the enter and exit halves are paired with `togetherWith`: **Slide animations:** ```kotlin import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith import com.firebase.ui.auth.configuration.AuthUITransitions val configuration = authUIConfiguration { @@ -1705,10 +1708,14 @@ val configuration = authUIConfiguration { provider(AuthProvider.Google()) } transitions = AuthUITransitions( - enterTransition = { slideInHorizontally { it } }, // Slide in from right - exitTransition = { slideOutHorizontally { -it } }, // Slide out to left - popEnterTransition = { slideInHorizontally { -it } }, // Slide in from left - popExitTransition = { slideOutHorizontally { it } } // Slide out to right + // Slide in from right, slide out to left + transitionSpec = { slideInHorizontally { it } togetherWith slideOutHorizontally { -it } }, + // Slide in from left, slide out to right + popTransitionSpec = { slideInHorizontally { -it } togetherWith slideOutHorizontally { it } }, + // Predictive back falls back to the default cross-fade if left unset, so mirror the pop + predictivePopTransitionSpec = { + slideInHorizontally { -it } togetherWith slideOutHorizontally { it } + } ) } ``` @@ -1718,6 +1725,7 @@ val configuration = authUIConfiguration { ```kotlin import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import com.firebase.ui.auth.configuration.AuthUITransitions val configuration = authUIConfiguration { @@ -1725,10 +1733,9 @@ val configuration = authUIConfiguration { provider(AuthProvider.Phone()) } transitions = AuthUITransitions( - enterTransition = { fadeIn() }, - exitTransition = { fadeOut() }, - popEnterTransition = { fadeIn() }, - popExitTransition = { fadeOut() } + transitionSpec = { fadeIn() togetherWith fadeOut() }, + popTransitionSpec = { fadeIn() togetherWith fadeOut() }, + predictivePopTransitionSpec = { fadeIn() togetherWith fadeOut() } ) } ``` @@ -1740,6 +1747,7 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut +import androidx.compose.animation.togetherWith import com.firebase.ui.auth.configuration.AuthUITransitions val configuration = authUIConfiguration { @@ -1747,10 +1755,14 @@ val configuration = authUIConfiguration { provider(AuthProvider.Facebook()) } transitions = AuthUITransitions( - enterTransition = { fadeIn() + scaleIn(initialScale = 0.9f) }, - exitTransition = { fadeOut() + scaleOut(targetScale = 0.9f) }, - popEnterTransition = { fadeIn() + scaleIn(initialScale = 0.9f) }, - popExitTransition = { fadeOut() + scaleOut(targetScale = 0.9f) } + transitionSpec = { + fadeIn() + scaleIn(initialScale = 0.9f) togetherWith + fadeOut() + scaleOut(targetScale = 0.9f) + }, + popTransitionSpec = { + fadeIn() + scaleIn(initialScale = 0.9f) togetherWith + fadeOut() + scaleOut(targetScale = 0.9f) + } ) } ``` @@ -1760,20 +1772,56 @@ val configuration = authUIConfiguration { ```kotlin import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import com.firebase.ui.auth.configuration.AuthUITransitions + +val configuration = authUIConfiguration { + providers { + provider(AuthProvider.Email()) + } + transitions = AuthUITransitions( + // Slide up on the way in, slide down on the way out + transitionSpec = { slideInVertically { it } togetherWith slideOutVertically { -it } } + ) +} +``` + +**Per-destination animations:** + +Read `authRoute()` off `initialState` / `targetState` to vary the animation by the screen being +navigated to or from: + +```kotlin +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith import com.firebase.ui.auth.configuration.AuthUITransitions +import com.firebase.ui.auth.ui.screens.AuthRoute +import com.firebase.ui.auth.ui.screens.authRoute val configuration = authUIConfiguration { providers { provider(AuthProvider.Email()) } transitions = AuthUITransitions( - enterTransition = { slideInVertically { it } }, // Slide up - exitTransition = { slideOutVertically { -it } } // Slide down + transitionSpec = { + if (targetState.authRoute() is AuthRoute.Success) { + fadeIn() togetherWith fadeOut() + } else { + slideInHorizontally { it } togetherWith slideOutHorizontally { -it } + } + } ) } ``` -> **Note:** If not specified, default fade in/out transitions with 700ms duration are used. +> **Note:** Each spec is independent. Any one left unset falls back to the library's default +> 700ms cross-fade — `predictivePopTransitionSpec` included, which does *not* fall back to +> `popTransitionSpec`. `predictivePopTransitionSpec` also receives the swipe edge +> (`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. ## Advanced Features From 156752114e446793fcc233045a9235c30a80dae7 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:38:18 +0100 Subject: [PATCH 3/6] docs(auth): describe the vertical slide sample's exit direction accurately --- auth/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/README.md b/auth/README.md index 2bf4090cf..dd8b97943 100644 --- a/auth/README.md +++ b/auth/README.md @@ -1780,7 +1780,7 @@ val configuration = authUIConfiguration { provider(AuthProvider.Email()) } transitions = AuthUITransitions( - // Slide up on the way in, slide down on the way out + // A vertical push: the new step rises from the bottom as the old one leaves via the top transitionSpec = { slideInVertically { it } togetherWith slideOutVertically { -it } } ) } From 939441be1aa9f741d474492c6b7b3fe724060110 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:21:52 +0100 Subject: [PATCH 4/6] docs(auth): set the required context in the transition samples --- auth/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/auth/README.md b/auth/README.md index dd8b97943..fb6bf8dda 100644 --- a/auth/README.md +++ b/auth/README.md @@ -1703,6 +1703,7 @@ import androidx.compose.animation.togetherWith import com.firebase.ui.auth.configuration.AuthUITransitions val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Email()) provider(AuthProvider.Google()) @@ -1729,6 +1730,7 @@ import androidx.compose.animation.togetherWith import com.firebase.ui.auth.configuration.AuthUITransitions val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Phone()) } @@ -1751,6 +1753,7 @@ import androidx.compose.animation.togetherWith import com.firebase.ui.auth.configuration.AuthUITransitions val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Facebook()) } @@ -1776,6 +1779,7 @@ import androidx.compose.animation.togetherWith import com.firebase.ui.auth.configuration.AuthUITransitions val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Email()) } @@ -1802,6 +1806,7 @@ import com.firebase.ui.auth.ui.screens.AuthRoute import com.firebase.ui.auth.ui.screens.authRoute val configuration = authUIConfiguration { + context = applicationContext providers { provider(AuthProvider.Email()) } From 28e0b6c30f77285a6d6605ab41c458c2bddc533c Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:38:34 +0100 Subject: [PATCH 5/6] docs(auth): resolve the theme and context outside the configuration builder --- auth/README.md | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/auth/README.md b/auth/README.md index fb6bf8dda..fb75a5cc7 100644 --- a/auth/README.md +++ b/auth/README.md @@ -282,6 +282,8 @@ 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 + val configuration = authUIConfiguration { // Required: an application Context. Omitting it throws when the block is evaluated. context = applicationContext @@ -293,8 +295,9 @@ val configuration = authUIConfiguration { provider(AuthProvider.Phone()) } - // Optional: Theme configuration - theme = AuthUITheme.fromMaterialTheme() + // 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" @@ -352,6 +355,7 @@ val configuration = authUIConfiguration { ```kotlin val controller = authUI.createAuthFlow(configuration) +// Must be registered during Activity/Fragment initialization, not in onCreate or a listener val authLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { /* the flow finished; inspect FirebaseAuth.currentUser or the result extras */ } @@ -670,8 +674,9 @@ The high-level API provides a complete, opinionated authentication experience wi ```kotlin @Composable fun AuthenticationScreen() { + val localContext = LocalContext.current val configuration = authUIConfiguration { - context = applicationContext + context = localContext providers { provider(AuthProvider.Email()) provider(AuthProvider.Google()) @@ -1310,13 +1315,15 @@ 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 + val configuration = authUIConfiguration { context = applicationContext providers { provider(AuthProvider.Email()) provider(AuthProvider.Google()) } - theme = AuthUITheme.Adaptive // Adapts to system dark mode + theme = adaptiveTheme } ``` @@ -1331,12 +1338,13 @@ Use `.copy()` to customize specific properties of the default theme: ```kotlin @Composable fun AuthScreen() { + val localContext = LocalContext.current val customTheme = AuthUITheme.Adaptive.copy( providerButtonShape = MaterialTheme.shapes.extraLarge // Pill-shaped buttons ) val configuration = authUIConfiguration { - context = applicationContext + context = localContext providers { provider(AuthProvider.Google()) provider(AuthProvider.Email()) @@ -1362,12 +1370,14 @@ 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 + val configuration = authUIConfiguration { context = applicationContext providers { provider(AuthProvider.Email()) } - theme = AuthUITheme.Adaptive // Set theme here + theme = adaptiveTheme // Set theme here } FirebaseAuthScreen( @@ -1383,15 +1393,17 @@ FirebaseAuthScreen( You can also wrap `FirebaseAuthScreen` with `AuthUITheme`: ```kotlin +val adaptiveTheme = AuthUITheme.Adaptive + val configuration = authUIConfiguration { context = applicationContext providers { provider(AuthProvider.Email()) } - theme = AuthUITheme.Adaptive // Theme in configuration + theme = adaptiveTheme // Theme in configuration } -AuthUITheme(theme = AuthUITheme.Adaptive) { // Optional wrapper +AuthUITheme(theme = adaptiveTheme) { // Optional wrapper Surface(color = MaterialTheme.colorScheme.background) { FirebaseAuthScreen( configuration = configuration, @@ -1454,12 +1466,16 @@ Use `fromMaterialTheme()` to automatically inherit your app's Material Design th @Composable fun App() { MyAppTheme { // Your existing Material3 theme - val configuration = authUIConfiguration { - context = applicationContext - providers { - provider(AuthProvider.Email()) + val localContext = LocalContext.current + val authTheme = AuthUITheme.fromMaterialTheme() // Inherits colors, typography, shapes + val configuration = remember(localContext, authTheme) { + authUIConfiguration { + context = localContext + providers { + provider(AuthProvider.Email()) + } + theme = authTheme } - theme = AuthUITheme.fromMaterialTheme() // Inherits colors, typography, shapes } FirebaseAuthScreen( @@ -2238,7 +2254,7 @@ var errorState by remember { mutableStateOf(null) } errorState?.let { error -> ErrorRecoveryDialog( error = error, - stringProvider = DefaultAuthUIStringProvider(context), + stringProvider = DefaultAuthUIStringProvider(LocalContext.current), onRetry = { // Retry the authentication errorState = null From 59a8b668bb8decbb0357716b41a38308b288de65 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:22:25 +0100 Subject: [PATCH 6/6] docs(auth): fix the README examples raised in review --- auth/README.md | 83 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 29 deletions(-) diff --git a/auth/README.md b/auth/README.md index fb75a5cc7..98107ee50 100644 --- a/auth/README.md +++ b/auth/README.md @@ -101,7 +101,7 @@ dependencies { implementation("com.firebaseui:firebase-ui-auth:10.0.0-beta04") // Required: Firebase Auth - implementation(platform("com.google.firebase:firebase-bom:32.7.0")) + implementation(platform("com.google.firebase:firebase-bom:34.17.0")) implementation("com.google.firebase:firebase-auth") // Required: Jetpack Compose @@ -303,8 +303,8 @@ val configuration = authUIConfiguration { tosUrl = "https://example.com/terms" privacyPolicyUrl = "https://example.com/privacy" - // Optional: App logo - logo = 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 @@ -330,8 +330,8 @@ val configuration = authUIConfiguration { // Optional: Locale override locale = Locale.FRENCH - // Optional: link a new credential onto the signed-in account instead of switching - // accounts when the email already exists (default: 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 @@ -339,6 +339,7 @@ val configuration = authUIConfiguration { passwordResetActionCodeSettings = actionCodeSettings { url = "https://example.com/reset" handleCodeInApp = true + setAndroidPackageName(packageName, true, null) } // Optional: resolve an email to its providers with the legacy fetchSignInMethodsForEmail @@ -355,7 +356,8 @@ val configuration = authUIConfiguration { ```kotlin val controller = authUI.createAuthFlow(configuration) -// Must be registered during Activity/Fragment initialization, not in onCreate or a listener +// Register before the Activity reaches STARTED — as a property initializer or in onCreate. +// Registering later (in a click listener, say) throws. val authLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { /* the flow finished; inspect FirebaseAuth.currentUser or the result extras */ } @@ -685,7 +687,7 @@ fun AuthenticationScreen() { } tosUrl = "https://example.com/terms" privacyPolicyUrl = "https://example.com/privacy" - logo = Icons.Default.Lock + logo = AuthUIAsset.Vector(Icons.Default.Lock) } FirebaseAuthScreen( @@ -797,7 +799,11 @@ class AuthActivity : ComponentActivity() { } controller = authUI.createAuthFlow(configuration) - authLauncher.launch(controller.createIntent(this)) + + // Only on a fresh start; unguarded, every recreation would launch a second flow. + if (savedInstanceState == null) { + authLauncher.launch(controller.createIntent(this)) + } lifecycleScope.launch { controller.authStateFlow.collect { handleAuthState(it) } @@ -1083,7 +1089,7 @@ lifecycleScope.launch { context = context, reason = "Verify your identity to delete your account", ) { - auth.currentUser?.delete()?.await() + authUI.auth.currentUser?.delete()?.await() } } ``` @@ -1105,7 +1111,12 @@ The armed reauthentication lives on the process-cached `FirebaseAuthUI`, so it s val reauth = authUI.createReauthFlow( configuration = authUIConfiguration { context = applicationContext - // Providers are automatically filtered to those linked to the current user + // Required by the builder; createReauthFlow then filters this list down to the + // providers actually linked to the current user. + providers { + provider(AuthProvider.Email()) + provider(AuthProvider.Google()) + } }, ) val intent = reauth.createIntent(context) @@ -1168,6 +1179,8 @@ fun MfaEnrollmentFlow() { // screen also reconciles this itself, so a host that forgets cannot end up sending to an // unpermitted dial code. val flowState = rememberMfaEnrollmentFlowState(mfaConfig.allowedCountries) + // Read here, not inside onComplete: LocalContext.current is a @Composable read. + val context = LocalContext.current NavDisplay( backStack = backStack, @@ -1258,7 +1271,8 @@ FirebaseAuthScreen( // MFA challenges are handled automatically by FirebaseAuthScreen // But you can also handle them manually: if (exception is AuthException.MfaRequiredException) { - showMfaChallengeScreen(exception.resolver) + // The resolver arrives on AuthState.RequiresMfa, not on the exception. + showMfaChallengePrompt() } } ) @@ -1423,6 +1437,7 @@ Understanding which theme applies is important: ```kotlin val configuration = authUIConfiguration { context = applicationContext + providers { provider(AuthProvider.Email()) } theme = AuthUITheme.Default // LIGHT theme } @@ -1436,6 +1451,7 @@ Understanding which theme applies is important: ```kotlin val configuration = authUIConfiguration { context = applicationContext + providers { provider(AuthProvider.Email()) } // theme not specified (null) } @@ -1449,6 +1465,7 @@ Understanding which theme applies is important: ```kotlin val configuration = authUIConfiguration { context = applicationContext + providers { provider(AuthProvider.Email()) } // theme not specified (null) } @@ -1489,15 +1506,17 @@ fun App() { You can also customize while inheriting: ```kotlin +val authTheme = AuthUITheme.fromMaterialTheme( + providerButtonShape = RoundedCornerShape(16.dp) // Override button shape +) + val configuration = authUIConfiguration { context = applicationContext providers { provider(AuthProvider.Google()) provider(AuthProvider.Facebook()) } - theme = AuthUITheme.fromMaterialTheme( - providerButtonShape = RoundedCornerShape(16.dp) // Override button shape - ) + theme = authTheme } ``` @@ -1559,15 +1578,17 @@ val configuration = authUIConfiguration { **Option 2: Using `fromMaterialTheme()`:** ```kotlin +val authTheme = AuthUITheme.fromMaterialTheme( + providerButtonShape = RoundedCornerShape(16.dp) +) + val configuration = authUIConfiguration { context = applicationContext providers { provider(AuthProvider.Google()) provider(AuthProvider.Facebook()) } - theme = AuthUITheme.fromMaterialTheme( - providerButtonShape = RoundedCornerShape(16.dp) - ) + theme = authTheme } ``` @@ -1635,16 +1656,18 @@ val customProviderStyles = mapOf( ) ) +val authTheme = AuthUITheme.fromMaterialTheme( + providerButtonShape = RoundedCornerShape(12.dp), + providerStyles = customProviderStyles +) + val configuration = authUIConfiguration { context = applicationContext providers { provider(AuthProvider.Google()) provider(AuthProvider.Facebook()) } - theme = AuthUITheme.fromMaterialTheme( - providerButtonShape = RoundedCornerShape(12.dp), - providerStyles = customProviderStyles - ) + theme = authTheme } ``` @@ -2117,12 +2140,13 @@ Renaming or removing a tag, or changing the resource id it resolves to, is a bre ```kotlin @Composable fun SettingsScreen() { + val scope = rememberCoroutineScope() val context = LocalContext.current val authUI = remember { FirebaseAuthUI.getInstance() } Button( onClick = { - lifecycleScope.launch { + scope.launch { authUI.signOut(context) // User is signed out, navigate to auth screen navigateToAuth() @@ -2167,13 +2191,14 @@ Button( FirebaseUI includes default English strings. To add custom localization: ```kotlin -class SpanishStringProvider(context: Context) : AuthUIStringProvider { - override fun signInWithEmail() = "Iniciar sesión con correo" - override fun signInWithGoogle() = "Iniciar sesión con Google" - override fun signInWithFacebook() = "Iniciar sesión con Facebook" - override fun invalidEmail() = "Correo inválido" - override fun weakPassword() = "Contraseña débil" - // ... implement all other required methods +// 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 { + 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 {