Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e859949
fix(auth)!: remove non-functional MFA recovery codes (#2457)
demolaf Aug 28, 2026
f8ea8ea
refactor(auth): scope phone verification loading state to the composi…
demolaf Aug 28, 2026
2dd87cf
fix(e2e): fix flaky Google credential-linking test (#2455)
demolaf Sep 1, 2026
4eb910f
fix(auth): complete MFA challenge sign-in navigation and success call…
demolaf Sep 1, 2026
4b68e42
feat(auth): add stable resource ids for auth input fields (#2449)
demolaf Sep 1, 2026
57018e5
fix(auth): log diagnostic warning when Google sign-in NoCredentialExc…
demolaf Jul 31, 2026
bd39e3b
fix(e2e): retry and fail loudly when clearing Firebase Auth emulator …
demolaf Aug 7, 2026
a39e7d6
fix(e2e): don't swallow InterruptedException in emulator clear retry …
demolaf Aug 7, 2026
059bba8
feat(auth): reshape reauthContent into a ReauthContentState content s…
demolaf Sep 1, 2026
1306c0e
test(auth): pin onComplete and factor refresh on successful MFA enrol…
demolaf Sep 2, 2026
217e039
feat(auth)!: make each email sign-in mode a real navigation destinati…
demolaf Sep 2, 2026
bfc90ec
feat(auth): make each MFA enrollment step a real navigation destinati…
demolaf Sep 2, 2026
156d7f5
feat(auth)!: migrate auth navigation to Navigation 3 (#2469)
demolaf Sep 2, 2026
a340cb3
feat(auth): make reauthentication a navigation destination (#2471)
demolaf Sep 2, 2026
9f7aad8
fix(auth): clear MFA enrollment flow state on every entry (#2473)
demolaf Sep 2, 2026
b20855b
feat(auth): make each phone auth step a real navigation destination (…
demolaf Sep 2, 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
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
Expand All @@ -32,7 +36,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.lifecycleScope
Expand All @@ -58,6 +61,7 @@ import com.firebase.ui.auth.configuration.theme.AuthUIAsset
import com.firebase.ui.auth.configuration.theme.AuthUITheme
import com.firebase.ui.auth.ui.screens.AuthSuccessUiContext
import com.firebase.ui.auth.ui.screens.FirebaseAuthScreen
import com.firebase.ui.auth.ui.screens.reauth.ReauthContentState
import com.firebase.ui.auth.util.EmailLinkConstants
import com.firebase.ui.auth.util.displayIdentifier
import com.firebase.ui.auth.util.getDisplayEmail
Expand Down Expand Up @@ -102,10 +106,15 @@ class HighLevelApiDemoActivity : ComponentActivity() {
isMfaEnabled = false
stringProvider = customStringProvider
transitions = AuthUITransitions(
enterTransition = { slideInHorizontally { it } },
exitTransition = { slideOutHorizontally { -it } },
popEnterTransition = { slideInHorizontally { -it } },
popExitTransition = { slideOutHorizontally { it } }
transitionSpec = {
slideInHorizontally { it } togetherWith slideOutHorizontally { -it }
},
popTransitionSpec = {
slideInHorizontally { -it } togetherWith slideOutHorizontally { it }
},
predictivePopTransitionSpec = { _ ->
slideInHorizontally { -it } togetherWith slideOutHorizontally { it }
},
)
providers {
provider(AuthProvider.Anonymous)
Expand Down Expand Up @@ -229,13 +238,7 @@ class HighLevelApiDemoActivity : ComponentActivity() {
onSignInCancelled = {
Log.d("HighLevelApiDemoActivity", "Authentication cancelled")
},
reauthContent = { state, onDismiss ->
ReauthDialog(
authUI = authUI,
state = state,
onDismiss = onDismiss,
)
},
reauthContent = { state -> ReauthDialog(state = state) },
authenticatedContent = { state, uiContext ->
AppAuthenticatedContent(state, uiContext)
}
Expand Down Expand Up @@ -333,8 +336,6 @@ private fun AppAuthenticatedContent(
try {
uiContext.authUI.delete(context)
} catch (e: AuthException.InvalidCredentialsException) {
// ReauthenticationRequired state was emitted —
// FirebaseAuthScreen navigates to the reauth flow automatically.
Log.d("HighLevelApiDemoActivity", "Reauth required before delete")
} catch (e: AuthException) {
Log.e("HighLevelApiDemoActivity", "Delete failed", e)
Expand Down Expand Up @@ -414,20 +415,15 @@ private fun AppAuthenticatedContent(
}
}

/**
* Custom reauth UI. The slot only chooses a provider — the library owns every credential path, and
* for email/phone it presents its own sub-flow, which replaces this dialog while it is up. Keep the
* slot stateless for that reason.
*/
@Composable
private fun ReauthDialog(
authUI: FirebaseAuthUI,
state: AuthState.ReauthenticationRequired,
onDismiss: () -> Unit,
) {
var password by remember { mutableStateOf("") }
var isVerifying by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
val coroutineScope = rememberCoroutineScope()
val email = state.user.email.orEmpty()

private fun ReauthDialog(state: ReauthContentState) {
AlertDialog(
onDismissRequest = onDismiss,
onDismissRequest = state.onDismiss,
containerColor = MaterialTheme.colorScheme.surfaceVariant,
title = {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Expand All @@ -442,60 +438,43 @@ private fun ReauthDialog(
}
},
text = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
"Signing in as $email",
"Signed in as ${state.user.displayIdentifier()}",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
)
com.firebase.ui.auth.ui.components.AuthTextField(
value = password,
onValueChange = {
password = it
errorMessage = null
},
label = { Text("Password") },
isSecureTextField = true,
isError = errorMessage != null,
errorMessage = errorMessage,
)
}
},
dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") }
},
confirmButton = {
Button(
onClick = {
coroutineScope.launch {
isVerifying = true
errorMessage = null
try {
val result = authUI.auth
.signInWithEmailAndPassword(email, password)
.await()
result.user?.let { user ->
authUI.updateAuthState(AuthState.Success(result, user))
}
} catch (e: Exception) {
errorMessage = "Incorrect password. Please try again."
} finally {
isVerifying = false
}
}
},
enabled = password.isNotBlank() && !isVerifying,
) {
if (isVerifying) {
state.error?.let { error ->
Text(
error,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
)
}
if (state.isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
)
} else {
Text("Verify")
}
state.providers.forEach { provider ->
Button(
onClick = { state.onProviderSelected(provider) },
enabled = !state.isLoading,
modifier = Modifier.fillMaxWidth(),
) {
Text("Continue with ${provider.providerName}")
}
}
}
},
confirmButton = {},
dismissButton = {
TextButton(onClick = state.onDismiss) { Text("Cancel") }
},
)
}

Expand Down
Loading
Loading