Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
545abfe
fix(auth)!: remove non-functional MFA recovery codes (#2457)
demolaf Aug 28, 2026
0fc340f
refactor(auth): scope phone verification loading state to the composi…
demolaf Aug 28, 2026
7a8f9e4
fix(e2e): fix flaky Google credential-linking test (#2455)
demolaf Sep 1, 2026
621018b
fix(auth): complete MFA challenge sign-in navigation and success call…
demolaf Sep 1, 2026
766b5e7
feat(auth): add stable resource ids for auth input fields (#2449)
demolaf Sep 1, 2026
67cf521
fix(auth): log diagnostic warning when Google sign-in NoCredentialExc…
demolaf Jul 31, 2026
b9dd90f
fix(e2e): retry and fail loudly when clearing Firebase Auth emulator …
demolaf Aug 7, 2026
6515dad
fix(e2e): don't swallow InterruptedException in emulator clear retry …
demolaf Aug 7, 2026
1b3a970
feat(auth): reshape reauthContent into a ReauthContentState content s…
demolaf Sep 1, 2026
dc813ae
test(auth): pin onComplete and factor refresh on successful MFA enrol…
demolaf Sep 2, 2026
4dcd469
feat(auth)!: make each email sign-in mode a real navigation destinati…
demolaf Sep 2, 2026
6df153a
feat(auth): make each MFA enrollment step a real navigation destinati…
demolaf Sep 2, 2026
1bac525
feat(auth)!: migrate auth navigation to Navigation 3 (#2469)
demolaf Sep 2, 2026
97429d7
feat(auth): make reauthentication a navigation destination (#2471)
demolaf Sep 2, 2026
0de3292
fix(auth): clear MFA enrollment flow state on every entry (#2473)
demolaf Sep 2, 2026
8ae7e77
feat(auth): make each phone auth step a real navigation destination (…
demolaf Sep 2, 2026
36df111
fix(auth): don't strand phone-only users on email verification after …
demolaf Sep 3, 2026
dbcb5f0
test(auth): cover the write-through recoveries and the custom reauth …
demolaf Sep 4, 2026
4e3ea8b
refactor(auth)!: replace the retained reauth closure with a resolver …
demolaf Sep 3, 2026
c7b0f84
test(auth): record the ordered state sequence of each sign-in path
demolaf Sep 3, 2026
fad7fc7
refactor(auth)!: give provider code its own flow scope instead of the…
demolaf Sep 3, 2026
322d145
refactor(auth): read the arming guards off the back stack rather than…
demolaf Sep 3, 2026
ca98011
refactor(auth): let a flow's screens read their state from the flow t…
demolaf Sep 3, 2026
7ea1d3b
feat(auth)!: report a declined reauthentication to the caller instead…
demolaf Sep 3, 2026
79eec9b
refactor(auth): name what happens to a reauthentication request inste…
demolaf Sep 3, 2026
bca42b5
refactor(auth)!: give reauthentication requests their own channel ins…
demolaf Sep 4, 2026
293346d
docs(auth): say what the reauthentication api does instead of arguing…
demolaf Sep 4, 2026
71bd912
test(auth): drive the reauthentication e2e tests through withReauth
demolaf Sep 4, 2026
24b69e9
fix(auth): clear the reauthentication handover state however the retr…
demolaf Sep 4, 2026
0859eb4
test(auth): cover a password change and an account deletion through r…
demolaf Sep 4, 2026
d61c09c
refactor(auth)!: make Reauthentication.Required's user constructor in…
demolaf Sep 4, 2026
f99fa29
test(auth): pin what makes one reauthentication request state differ …
demolaf Sep 4, 2026
e29d1a9
fix(auth): read a user's email once when deciding what their sign-in …
demolaf Sep 4, 2026
75ce57c
fix(auth): decline a reauthentication request that is never presented
demolaf Sep 7, 2026
e1db90d
fix(auth): report a failed retry as an error instead of a stale success
demolaf Sep 7, 2026
795f757
docs(auth): drop two references to reauthentication members this bran…
demolaf Sep 7, 2026
28701fc
test(auth): assert a dismissed reauthentication declines its caller, …
demolaf Sep 7, 2026
2d959d6
fix(auth): decline the outstanding request when an abort finds no pha…
demolaf Sep 7, 2026
99e70fe
feat(auth)!: require the multi-step auth screens to be given navigati…
demolaf Sep 7, 2026
9244eb8
fix(auth)!: remove the sign-in controls made redundant by per-step na…
demolaf Sep 7, 2026
ba697de
feat(auth)!: decouple SMS multi-factor configuration from the phone s…
demolaf Sep 8, 2026
1d87f10
docs(auth): correct the 10.0 upgrade guide against the current API (#…
demolaf Sep 8, 2026
d0ce78f
sample(app): drop the comments that restate the code in the auth demo…
demolaf Sep 8, 2026
ea6fecf
docs(auth): correct the auth KDoc against the current API (#2493)
demolaf Sep 8, 2026
9630281
docs(auth): correct the auth README against the current API (#2496)
demolaf Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ plugins {
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.gms.google-services")
id("kotlin-kapt")
// The slot demos host the auth screens on their own Navigation 3 back stacks, and a
// rememberNavBackStack key has to be @Serializable to survive process death.
alias(libs.plugins.kotlin.serialization)
}

android {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ fun AuthChooserScreen(
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
// Header

Text(
text = "Firebase Auth UI Compose",
style = MaterialTheme.typography.headlineLarge,
Expand All @@ -92,7 +92,6 @@ fun AuthChooserScreen(
color = MaterialTheme.colorScheme.onSurfaceVariant
)

// Emulator Mode Warning
if (isEmulatorMode) {
Card(
modifier = Modifier.fillMaxWidth(),
Expand Down Expand Up @@ -121,7 +120,6 @@ fun AuthChooserScreen(
}
}

// High-Level API Card
Card(
modifier = Modifier.fillMaxWidth(),
onClick = onHighLevelApiClick
Expand Down Expand Up @@ -157,7 +155,6 @@ fun AuthChooserScreen(
}
}

// Low-Level API Card
Card(
modifier = Modifier.fillMaxWidth(),
onClick = onLowLevelApiClick
Expand Down Expand Up @@ -193,7 +190,6 @@ fun AuthChooserScreen(
}
}

// Custom Slots & Theming Card
Card(
modifier = Modifier.fillMaxWidth(),
onClick = onCustomSlotsClick
Expand Down Expand Up @@ -229,7 +225,6 @@ fun AuthChooserScreen(
}
}

// Credential Linking Card
Card(
modifier = Modifier.fillMaxWidth(),
onClick = onCredentialLinkingClick
Expand Down Expand Up @@ -257,7 +252,6 @@ fun AuthChooserScreen(

Spacer(modifier = Modifier.height(16.dp))

// Info card
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,23 @@ import com.google.firebase.auth.actionCodeSettings
import kotlinx.coroutines.launch

/**
* Demo activity showcasing the AuthFlowController API for managing
* Firebase authentication with lifecycle-safe control.
* Drives the auth flow from an Activity with [AuthFlowController], instead of composing
* `FirebaseAuthScreen` directly.
*
* This demonstrates:
* - Creating an AuthFlowController with configuration
* - Starting the auth flow using ActivityResultLauncher
* - Observing auth state changes
* - Handling results (success, cancelled, error)
* - Proper lifecycle management with dispose()
* The flow runs in its own Activity, so its outcome arrives as an Activity result. That result
* only reports that the flow ended, which is why the demo also collects `authStateFlow` and
* registers an `AuthStateListener` beside it — those are what report progress and the signed-in
* user. Disposing the controller in `onDestroy` is the contract [AuthFlowController] documents.
*/
class AuthFlowControllerDemoActivity : ComponentActivity() {

private lateinit var authController: AuthFlowController

// Modern ActivityResultLauncher for auth flow
private val authLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
when (result.resultCode) {
Activity.RESULT_OK -> {
// Get user data from result
val userId = result.data?.getStringExtra(FirebaseAuthActivity.EXTRA_USER_ID)
val isNewUser = result.data?.getBooleanExtra(
FirebaseAuthActivity.EXTRA_IS_NEW_USER,
Expand All @@ -71,10 +67,8 @@ class AuthFlowControllerDemoActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Initialize FirebaseAuthUI
val authUI = FirebaseAuthUI.getInstance()

// Create auth configuration
val configuration = AuthUIConfiguration(
context = applicationContext,
providers = listOf(
Expand Down Expand Up @@ -103,7 +97,6 @@ class AuthFlowControllerDemoActivity : ComponentActivity() {
defaultNumber = null,
defaultCountryCode = null,
allowedCountries = emptyList(),
smsCodeLength = 6,
timeout = 120L,
isInstantVerificationEnabled = true
),
Expand All @@ -113,7 +106,6 @@ class AuthFlowControllerDemoActivity : ComponentActivity() {
privacyPolicyUrl = "https://policies.google.com/privacy?hl=en-NG&fg=1"
)

// Create AuthFlowController
authController = authUI.createAuthFlow(configuration)

setContent {
Expand All @@ -134,7 +126,6 @@ class AuthFlowControllerDemoActivity : ComponentActivity() {

override fun onDestroy() {
super.onDestroy()
// Clean up resources
authController.dispose()
}

Expand Down Expand Up @@ -164,7 +155,6 @@ fun AuthFlowDemo(
val authState by authController.authStateFlow.collectAsState(AuthState.Idle)
var currentUser by remember { mutableStateOf(FirebaseAuth.getInstance().currentUser) }

// Observe Firebase auth state changes
DisposableEffect(Unit) {
val authStateListener = FirebaseAuth.AuthStateListener { auth ->
currentUser = auth.currentUser
Expand Down Expand Up @@ -205,7 +195,6 @@ fun AuthFlowDemo(

Spacer(modifier = Modifier.height(16.dp))

// Current Auth State Card
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
Expand Down Expand Up @@ -243,7 +232,6 @@ fun AuthFlowDemo(
}
}

// Current User Card
currentUser?.let { user ->
Card(
modifier = Modifier.fillMaxWidth(),
Expand Down Expand Up @@ -274,7 +262,6 @@ fun AuthFlowDemo(

Spacer(modifier = Modifier.height(16.dp))

// Action Buttons
if (currentUser == null) {
Button(
onClick = onStartAuth,
Expand Down Expand Up @@ -302,7 +289,6 @@ fun AuthFlowDemo(
}
}

// Info Card
Card(
modifier = Modifier.fillMaxWidth()
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import com.firebase.ui.auth.AuthException
import com.firebase.ui.auth.FirebaseAuthUI
import com.firebase.ui.auth.configuration.AuthUIConfiguration
Expand All @@ -51,6 +56,33 @@ import com.firebase.ui.auth.ui.screens.email.EmailAuthContentState
import com.firebase.ui.auth.ui.screens.email.EmailAuthMode
import com.firebase.ui.auth.ui.screens.email.EmailAuthScreen
import com.google.firebase.auth.AuthResult
import kotlinx.serialization.Serializable

/**
* One email mode, as this demo's own back-stack key.
*
* The library's navigation keys are its own business — a caller hosting [EmailAuthScreen] outside
* `FirebaseAuthScreen` brings its own, built from the public [EmailAuthMode] and the address the
* screen hands back. Distinct keys are distinct entries, so every mode composes fresh and system
* back pops one mode at a time.
*/
@Serializable
private data class EmailModeKey(val mode: EmailAuthMode, val email: String = "") : NavKey

/**
* Moves to [mode], carrying the address so a switch keeps what the user typed.
*
* Adds before trimming, so no single write empties the stack: a mode already on the stack is
* replaced by the fresh key rather than revisited with a stale address; one that is not is pushed,
* leaving the mode below reachable by back.
*/
private fun NavBackStack<NavKey>.goToEmailMode(mode: EmailAuthMode, email: String) {
val existing = indexOfFirst { it is EmailModeKey && it.mode == mode }
add(EmailModeKey(mode, email))
if (existing >= 0) {
while (size > existing + 1) removeAt(existing)
}
}

class EmailAuthSlotDemoActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Expand Down Expand Up @@ -133,6 +165,12 @@ fun EmailAuthDemo(
}
}

// Every mode is a real destination on this demo's own stack, so system back steps between modes
// instead of leaving the flow. Allocated above the branch: a rememberSaveable must not sit
// behind one, or signing out and back in restores it against a composition that no longer
// matches.
val backStack = rememberNavBackStack(EmailModeKey(EmailAuthMode.SignIn))

if (currentUser != null) {
Column(
modifier = Modifier
Expand Down Expand Up @@ -160,22 +198,42 @@ fun EmailAuthDemo(
}
} else {
CompositionLocalProvider(LocalAuthUIStringProvider provides configuration.stringProvider) {
EmailAuthScreen(
context = context,
configuration = configuration,
authUI = authUI,
onSuccess = { result: AuthResult ->
Log.d("EmailAuthSlotDemo", "Auth success: ${result.user?.uid}")
},
onError = { exception: AuthException ->
Log.e("EmailAuthSlotDemo", "Auth error", exception)
NavDisplay(
backStack = backStack,
// Guarded like the library's popOrNull: NavDisplay throws on an empty
// 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 = configuration,
authUI = authUI,
prefillEmail = key.email.ifEmpty { null },
mode = key.mode,
onNavigateToMode = { mode, email ->
backStack.goToEmailMode(mode, email)
},
onSuccess = { result: AuthResult ->
Log.d("EmailAuthSlotDemo", "Auth success: ${result.user?.uid}")
},
onError = { exception: AuthException ->
Log.e("EmailAuthSlotDemo", "Auth error", exception)
},
onCancel = {
// Below the start mode there is nothing left to pop back to.
if (backStack.size > 1) {
backStack.removeLastOrNull()
} else {
Log.d("EmailAuthSlotDemo", "Auth cancelled")
}
}
) { state: EmailAuthContentState ->
CustomEmailAuthUI(state)
}
}
},
onCancel = {
Log.d("EmailAuthSlotDemo", "Auth cancelled")
}
) { state: EmailAuthContentState ->
CustomEmailAuthUI(state)
}
)
}
}
}
Expand Down
Loading
Loading