Skip to content

Latest commit

 

History

History
538 lines (452 loc) · 15.6 KB

File metadata and controls

538 lines (452 loc) · 15.6 KB

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

FirebaseUI Auth 10.x is a complete rewrite built with Jetpack Compose and Material Design 3. The architecture has changed significantly from the View-based system to a modern, declarative UI framework.

Key Architectural Changes

1. UI Framework

  • 9.x: Android Views, Activities, Fragments
  • 10.x: Jetpack Compose, Composables

2. Configuration

  • 9.x: Builder pattern (createSignInIntentBuilder())
  • 10.x: Kotlin DSL (authUIConfiguration {})

3. Providers

  • 9.x: IdpConfig.EmailBuilder().build()
  • 10.x: AuthProvider.Email()

4. Flow Control

  • 9.x: Activity-based with startActivityForResult() and ActivityResultLauncher
  • 10.x: Composable screens with direct callbacks OR AuthFlowController for Activity-based apps

5. Theming

  • 9.x: XML theme resources (R.style.AppTheme)
  • 10.x: AuthUITheme with Material 3 color schemes

6. State Management

  • 9.x: AuthStateListener callbacks
  • 10.x: Reactive Flow<AuthState>

Migration Steps

Step 1: Update Dependencies

Old (9.x):

dependencies {
    implementation("com.firebaseui:firebase-ui-auth:9.1.1")
}

New (10.x):

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")

    // Required: Jetpack Compose
    implementation(platform("androidx.compose:compose-bom:2026.06.01"))
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.material3:material3")
}

Step 2: Migrate to Compose

Old (9.x) - Activity-based:

public class SignInActivity extends AppCompatActivity {
    private final ActivityResultLauncher<Intent> signInLauncher =
        registerForActivityResult(
            new ActivityResultContracts.StartActivityForResult(),
            result -> {
                IdpResponse response = IdpResponse.fromResultIntent(result.getData());

                if (result.getResultCode() == RESULT_OK) {
                    FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
                    // User signed in
                } else {
                    // Sign in failed
                }
            }
        );

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_signin);

        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:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MyAppTheme {
                val authTheme = AuthUITheme.fromMaterialTheme()
                val configuration = remember(authTheme) {
                    authUIConfiguration {
                        context = applicationContext
                        providers {
                            provider(AuthProvider.Email())
                            provider(AuthProvider.Google())
                        }
                        theme = authTheme
                    }
                }

                FirebaseAuthScreen(
                    configuration = configuration,
                    onSignInSuccess = { result ->
                        val user = result.user
                        // User signed in
                    },
                    onSignInFailure = { exception ->
                        // Sign in failed
                    },
                    onSignInCancelled = {
                        finish()
                    }
                )
            }
        }
    }
}

Step 3: Update Provider Configuration

Old (9.x):

List<AuthUI.IdpConfig> providers = Arrays.asList(
    new AuthUI.IdpConfig.EmailBuilder()
        .setRequireName(true)
        .build(),
    new AuthUI.IdpConfig.GoogleBuilder()
        .build(),
    new AuthUI.IdpConfig.PhoneBuilder()
        .build()
);

New (10.x):

context is required — the builder throws without it. applicationContext is an Activity property; from a composable use LocalContext.current.applicationContext instead.

val configuration = authUIConfiguration {
    context = applicationContext
    providers {
        provider(AuthProvider.Email(isDisplayNameRequired = true))
        provider(
            AuthProvider.Google(
                scopes = listOf("email"),
                serverClientId = "YOUR_CLIENT_ID"
            )
        )
        provider(AuthProvider.Phone(defaultCountryCode = "US"))
    }
}

Step 4: Update Theming

Old (9.x) - XML styles:

<style name="AppTheme" parent="FirebaseUI">
    <item name="colorPrimary">@color/primary</item>
    <item name="colorPrimaryDark">@color/primary_dark</item>
    <item name="colorAccent">@color/accent</item>
</style>
.setTheme(R.style.AppTheme)

New (10.x) - Material 3:

val configuration = authUIConfiguration {
    context = applicationContext
    providers { provider(AuthProvider.Email()) }
    theme = AuthUITheme(
        colorScheme = lightColorScheme(
            primary = Color(0xFF6200EE),
            onPrimary = Color.White,
            secondary = Color(0xFF03DAC6)
        )
    )
}

Or inherit from your app theme:

MyAppTheme {
    val localContext = LocalContext.current
    val authTheme = AuthUITheme.fromMaterialTheme()
    val configuration = remember(localContext, authTheme) {
        authUIConfiguration {
            context = localContext
            providers { provider(AuthProvider.Email()) }
            theme = authTheme
        }
    }

    FirebaseAuthScreen(configuration = configuration, ...)
}

Step 5: Update Sign Out

Old (9.x):

AuthUI.getInstance()
    .signOut(this)
    .addOnCompleteListener(task -> {
        // User signed out
    });

New (10.x):

lifecycleScope.launch {
    FirebaseAuthUI.getInstance().signOut(context)
    // User signed out
}

Step 6: Update Account Deletion

Old (9.x):

AuthUI.getInstance()
    .delete(this)
    .addOnCompleteListener(task -> {
        if (task.isSuccessful()) {
            // Account deleted
        } else {
            // Deletion failed
        }
    });

New (10.x):

lifecycleScope.launch {
    try {
        FirebaseAuthUI.getInstance().delete(context)
        // Account deleted
    } catch (e: Exception) {
        // Deletion failed
    }
}

Step 7: Auth State Observation

Old (9.x):

FirebaseAuth.getInstance().addAuthStateListener(firebaseAuth -> {
    FirebaseUser user = firebaseAuth.getCurrentUser();
    if (user != null) {
        // User is signed in
    } else {
        // User is signed out
    }
});

New (10.x):

@Composable
fun AuthGate() {
    val authUI = remember { FirebaseAuthUI.getInstance() }
    // remember the flow: authStateFlow() builds a new one per call, and without this every
    // recomposition would restart collection and re-register the underlying Firebase listener.
    val authStateFlow = remember(authUI) { authUI.authStateFlow() }
    val authState by authStateFlow.collectAsState(initial = AuthState.Idle)

    when (authState) {
        is AuthState.Success -> {
            // User is signed in
            MainAppScreen()
        }
        else -> {
            // Show authentication
            FirebaseAuthScreen(...)
        }
    }
}

Provider-Specific Migration

Email Provider

Old (9.x):

new AuthUI.IdpConfig.EmailBuilder()
    .setRequireName(true)
    .setAllowNewAccounts(true)
    .enableEmailLinkSignIn()
    .setActionCodeSettings(actionCodeSettings)
    .build()

New (10.x):

AuthProvider.Email(
    isDisplayNameRequired = true,
    isNewAccountsAllowed = true,
    isEmailLinkSignInEnabled = true,
    emailLinkActionCodeSettings = actionCodeSettings {
        url = "https://example.com/auth"
        handleCodeInApp = true
        setAndroidPackageName(packageName, true, null)
    }
)

Google Provider

Old (9.x):

new AuthUI.IdpConfig.GoogleBuilder()
    .setScopes(Arrays.asList("email", "profile"))
    .build()

New (10.x):

AuthProvider.Google(
    scopes = listOf("email", "profile"),
    serverClientId = "YOUR_CLIENT_ID"
)

Phone Provider

Old (9.x):

new AuthUI.IdpConfig.PhoneBuilder()
    .setDefaultNumber("US", "+1 123-456-7890")
    .build()

New (10.x):

AuthProvider.Phone(
    defaultCountryCode = "US",
    defaultNumber = "+11234567890"
)

Advanced Migration Scenarios

Custom UI (Activity-based apps that can't use Compose everywhere)

If you have an existing Activity-based app and want to keep using Activities:

New (10.x) - Low-Level API:

class AuthActivity : ComponentActivity() {
    private lateinit var controller: AuthFlowController

    private val authLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        if (result.resultCode == Activity.RESULT_OK) {
            startActivity(Intent(this, MainActivity::class.java))
            finish()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val authUI = FirebaseAuthUI.getInstance()
        val configuration = authUIConfiguration {
            context = applicationContext
            providers {
                provider(AuthProvider.Email())
                provider(AuthProvider.Google())
            }
        }

        controller = authUI.createAuthFlow(configuration)

        // Only on a fresh start. Launching unguarded would start a second flow every time the
        // Activity is recreated — a rotation, or a restore after process death.
        if (savedInstanceState == null) {
            authLauncher.launch(controller.createIntent(this))
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        controller.dispose()
    }
}

The flow runs in its own Activity, so the outcome arrives as an Activity result rather than a return value. That result only says the flow ended, so to follow it in detail — loading, errors, MFA — collect controller.authStateFlow alongside the launcher. Dispose the controller in onDestroy: a disposed controller cannot be reused, and disposal is the contract its other members are documented against.

Common Issues and Solutions

Issue: "Unresolved reference: authUIConfiguration"

Solution: Ensure you have the correct import:

import com.firebase.ui.auth.configuration.authUIConfiguration

Issue: "ActivityResultLauncher is deprecated"

Solution: Compose FirebaseAuthScreen and use its callbacks — no ActivityResultLauncher needed. The exception is the Activity-based route above: AuthFlowController hands you an Intent, so that one still launches through a result contract.

Issue: "How do I customize the UI?"

Solution: Use content slots for custom UI. EmailAuthScreen renders the mode it is given and never changes it on its own, so the host owns the mode and navigates between them — give each mode its own destination, or the fields of the one you leave carry over into the one you arrive at:

@Serializable
data class EmailModeKey(val mode: EmailAuthMode, val email: String = "") : NavKey

val backStack = rememberNavBackStack(EmailModeKey(EmailAuthMode.SignIn))

NavDisplay(
    backStack = backStack,
    // NavDisplay throws on an empty back stack, and throws from recomposition, so the first
    // mode must not pop.
    onBack = { if (backStack.size > 1) backStack.removeLastOrNull() },
    entryProvider = entryProvider {
        entry<EmailModeKey> { key ->
            EmailAuthScreen(
                context = context,
                configuration = emailConfig,
                authUI = authUI,
                prefillEmail = key.email.ifEmpty { null },
                mode = key.mode,
                // The typed address travels with the switch, so the mode you arrive at can
                // prefill it. Replace a mode already on the stack rather than revisiting it
                // with a stale address.
                onNavigateToMode = { mode, email ->
                    val existing = backStack.indexOfFirst { it is EmailModeKey && it.mode == mode }
                    backStack.add(EmailModeKey(mode, email))
                    if (existing >= 0) {
                        while (backStack.size > existing + 1) backStack.removeAt(existing)
                    }
                },
                onSuccess = { /* ... */ },
                onError = { /* ... */ },
                onCancel = { /* ... */ },
            ) { state ->
                // Your custom UI here
                CustomSignInUI(state)
            }
        }
    },
)

PhoneAuthScreen and MfaEnrollmentScreen work the same way, with step / onNavigateToStep / onNavigateBack plus a flowState from rememberPhoneAuthFlowState or rememberMfaEnrollmentFlowState remembered above the NavDisplay. If you would rather not own any of this, use FirebaseAuthScreen, which owns it for you.

A back-stack key must be @Serializable to survive process death, so add the org.jetbrains.kotlin.plugin.serialization plugin to the module hosting these screens.

Issue: "My XML themes aren't working"

Solution: Convert XML themes to Kotlin code using AuthUITheme:

val configuration = authUIConfiguration {
    context = applicationContext
    providers { provider(AuthProvider.Email()) }
    theme = AuthUITheme(
        colorScheme = lightColorScheme(
            primary = Color(0xFF6200EE),
            // ... other colors
        )
    )
}

Testing Your Migration

  1. Build the app - Ensure it compiles without errors
  2. Test all auth flows - Sign in, sign up, password reset
  3. Test all providers - Email, Google, Phone, etc.
  4. Test sign out - Verify users can sign out
  5. Test account deletion - Verify accounts can be deleted
  6. Test error handling - Verify errors are handled gracefully
  7. Test theming - Verify UI matches your design

Checklist

  • Updated dependency to firebase-ui-auth:10.0.0-beta05
  • Migrated to Jetpack Compose
  • Converted Activities to ComponentActivities with setContent {}
  • Replaced createSignInIntentBuilder() with authUIConfiguration {}
  • Updated all provider configurations
  • Converted XML themes to AuthUITheme
  • Updated error handling from result codes to exceptions
  • Replaced ActivityResultLauncher with FirebaseAuthScreen callbacks (Activity-based apps keep one, for AuthFlowController.createIntent())
  • Updated sign-out to use suspend functions
  • Updated account deletion to use suspend functions
  • Tested all authentication flows
  • Tested on multiple Android versions

Need Help?