Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
df1ef1b
fix(auth): don't strand phone-only users on email verification after …
demolaf Sep 3, 2026
dfab195
test(auth): cover the write-through recoveries and the custom reauth …
demolaf Sep 4, 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
df1ef1b
fix(auth): don't strand phone-only users on email verification after …
demolaf Sep 3, 2026
dfab195
test(auth): cover the write-through recoveries and the custom reauth …
demolaf Sep 4, 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
df1ef1b
fix(auth): don't strand phone-only users on email verification after …
demolaf Sep 3, 2026
dfab195
test(auth): cover the write-through recoveries and the custom reauth …
demolaf Sep 4, 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
df1ef1b
fix(auth): don't strand phone-only users on email verification after …
demolaf Sep 3, 2026
dfab195
test(auth): cover the write-through recoveries and the custom reauth …
demolaf Sep 4, 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
df1ef1b
fix(auth): don't strand phone-only users on email verification after …
demolaf Sep 3, 2026
dfab195
test(auth): cover the write-through recoveries and the custom reauth …
demolaf Sep 4, 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
df1ef1b
fix(auth): don't strand phone-only users on email verification after …
demolaf Sep 3, 2026
dfab195
test(auth): cover the write-through recoveries and the custom reauth …
demolaf Sep 4, 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
df1ef1b
fix(auth): don't strand phone-only users on email verification after …
demolaf Sep 3, 2026
dfab195
test(auth): cover the write-through recoveries and the custom reauth …
demolaf Sep 4, 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
df1ef1b
fix(auth): don't strand phone-only users on email verification after …
demolaf Sep 3, 2026
dfab195
test(auth): cover the write-through recoveries and the custom reauth …
demolaf Sep 4, 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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