Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c034823
fix(auth): prevent MFA code input from triggering password manager sa…
just1and0 Jul 30, 2026
1c8f3e2
fix(auth): stale one-off AuthState no longer leaks across screen inst…
demolaf Aug 5, 2026
566715c
refactor(auth)!: hide sign-up button instead of showing disabled stat…
demolaf Aug 5, 2026
461620c
fix(auth): only exit auth flow on AuthState.Aborted, not per-provider…
demolaf Aug 7, 2026
c7ec549
fix(auth): stop showing error dialog when Google sign-in is cancelled…
demolaf Aug 10, 2026
7de1ff4
fix(auth): pre-fill email when "Continue as" button is tapped (#2423)…
just1and0 Aug 10, 2026
f896825
fix(auth): report Google/Facebook/OAuth/Anonymous sign-in failures vi…
demolaf Aug 12, 2026
637e13a
fix(auth): fix "Already resumed" crash in phone auth SMS auto-verific…
demolaf Aug 18, 2026
06a6fe9
test(auth): guard against premature field errors in SignUpUI form val…
demolaf Aug 18, 2026
3e9e3e1
refactor(auth): centralize compose test tags in FirebaseAuthTestTags
demolaf Aug 17, 2026
af6131d
test(auth): reject compose test tags declared outside the registry
demolaf Aug 18, 2026
557c992
fix(auth): apply caller modifier once at each composable root
demolaf Aug 18, 2026
f96e54b
fix(auth): apply PhoneAuthScreen's declared modifier to its content
demolaf Aug 18, 2026
446211d
fix(auth): correct modifier scope docs and harden the test tag scan
demolaf Aug 18, 2026
7e30695
feat(auth): expose auth input test tags as resource ids
demolaf Aug 18, 2026
5734b01
fix(auth): make the verification code field typeable by resource id
demolaf Aug 18, 2026
69d6d5c
fix(auth): enforce semantics owner coverage and tighten code field se…
demolaf Aug 18, 2026
aa01701
docs(auth): document test tags for Firebase Test Lab and Play pre-launch
demolaf Aug 19, 2026
525bdca
feat(auth): give verification code digits distinct localized descript…
demolaf Aug 19, 2026
c61b117
feat(auth): tag remaining secondary auth surfaces as resource ids
demolaf Aug 19, 2026
16b38ef
docs(auth): record why AuthTextField exposes internal elements via Mo…
demolaf Aug 19, 2026
7cc0d07
docs(auth): trim visibilityToggleModifier KDoc to one line
demolaf Aug 19, 2026
1bb54f7
docs(auth): trim comments and KDoc across the test-tag branch to 2 lines
demolaf Aug 19, 2026
ea79b30
test(auth): remove three redundant tests confirmed by mutation-check
demolaf Aug 19, 2026
d442657
fix(auth): default verificationCodeDigitDescription to preserve sourc…
demolaf Aug 19, 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 @@ -227,6 +227,7 @@ fun AuthFlowDemo(
is AuthState.Success -> "Success - User: ${(authState as AuthState.Success).user.email}"
is AuthState.Error -> "Error: ${(authState as AuthState.Error).exception.message}"
is AuthState.Cancelled -> "Cancelled"
is AuthState.Aborted -> "Aborted"
is AuthState.RequiresMfa -> "MFA Required"
is AuthState.RequiresEmailVerification -> "Email Verification Required"
else -> "Unknown"
Expand Down
116 changes: 109 additions & 7 deletions auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Equivalent FirebaseUI libraries are available for [iOS](https://github.com/fireb
- [Email Link Sign-In](#email-link-sign-in)
- [Password Validation Rules](#password-validation-rules)
- [Credential Manager Integration](#credential-manager-integration)
- [Automated Testing (Firebase Test Lab & Robo)](#automated-testing-firebase-test-lab--robo)
- [Sign Out & Account Deletion](#sign-out--account-deletion)
10. [Localization](#localization)
11. [Error Handling](#error-handling)
Expand Down Expand Up @@ -179,7 +180,9 @@ class MainActivity : ComponentActivity() {
Toast.makeText(this, "Error: ${exception.message}", Toast.LENGTH_SHORT).show()
},
onSignInCancelled = {
finish()
// User backed out of a single provider (e.g. dismissed the Google
// Credential Manager sheet); FirebaseAuthScreen already returns to
// the method picker on its own — no action needed here.
}
)
}
Expand Down Expand Up @@ -343,7 +346,12 @@ lifecycleScope.launch {
Log.e(TAG, "Auth failed", state.exception)
}
is AuthState.Cancelled -> {
// User cancelled
// User cancelled a single sign-in attempt (e.g. dismissed the
// Credential Manager sheet, backed out of MFA); the flow stays open
}
is AuthState.Aborted -> {
// Flow was ended via controller.cancel()
finish()
}
else -> {
// Handle other states (RequiresMfa, RequiresEmailVerification, etc.)
Expand Down Expand Up @@ -375,6 +383,7 @@ sealed class AuthState {
data class RequiresEmailVerification(val user: FirebaseUser, val email: String) : AuthState()
data class RequiresProfileCompletion(val user: FirebaseUser, val missingFields: List<String> = emptyList()) : AuthState()
object Cancelled : AuthState()
object Aborted : AuthState()
object PasswordResetLinkSent : AuthState()
object EmailSignInLinkSent : AuthState()
data class SMSAutoVerified(val credential: PhoneAuthCredential) : AuthState()
Expand Down Expand Up @@ -671,7 +680,8 @@ fun AuthenticationScreen() {
}
},
onSignInCancelled = {
navigateBack()
// User backed out of a single provider; the screen already returns
// to the method picker on its own.
}
)
}
Expand All @@ -684,7 +694,7 @@ fun AuthenticationScreen() {
| `configuration` | `AuthUIConfiguration` | *Required* | Authentication configuration (providers, theme, etc.) |
| `onSignInSuccess` | `(AuthResult) -> Unit` | *Required* | Callback when sign-in succeeds |
| `onSignInFailure` | `(AuthException) -> Unit` | *Required* | Callback when sign-in fails |
| `onSignInCancelled` | `() -> Unit` | *Required* | Callback when user cancels authentication |
| `onSignInCancelled` | `() -> Unit` | *Required* | Callback when the user backs out of a single sign-in attempt (`AuthState.Cancelled`, e.g. dismissing the Google Credential Manager sheet); `FirebaseAuthScreen` already returns to the method picker itself, so this is informational only. Not called when the whole flow ends via `AuthFlowController.cancel()` (`AuthState.Aborted`) — that state is observable directly on `authUI.authStateFlow()`/`authFlowController.authStateFlow` for callers who need it |
| `modifier` | `Modifier` | `Modifier` | Modifier for the composable |
| `authUI` | `FirebaseAuthUI` | `FirebaseAuthUI.getInstance()` | Custom FirebaseAuthUI instance (for multi-app support) |
| `emailLink` | `String?` | `null` | Email link for passwordless sign-in (see [Email Link Sign-In](#email-link-sign-in)) |
Expand All @@ -706,7 +716,8 @@ FirebaseAuthScreen(
showError(exception)
},
onSignInCancelled = {
finish()
// User backed out of a single provider; the screen already returns
// to the method picker on its own.
},
authenticatedContent = { state, uiContext ->
// Show a welcome screen or profile completion UI
Expand Down Expand Up @@ -777,7 +788,11 @@ class AuthActivity : ComponentActivity() {
showEmailVerificationScreen(state.user)
}
is AuthState.Cancelled -> {
// User cancelled authentication
// User cancelled a single sign-in attempt; the flow stays open
// and returns to the method picker
}
is AuthState.Aborted -> {
// Flow was ended via controller.cancel()
finish()
}
else -> {
Expand Down Expand Up @@ -1729,7 +1744,8 @@ override fun onCreate(savedInstanceState: Bundle?) {
// Handle error
},
onSignInCancelled = {
finish()
// User backed out of a single provider; the screen already
// returns to the method picker on its own.
}
)
}
Expand Down Expand Up @@ -1832,6 +1848,92 @@ val configuration = authUIConfiguration {
}
```

### Automated Testing (Firebase Test Lab & Robo)

Every input and button on the auth screens carries a stable, public test tag, and FirebaseUI exposes those tags as Android resource ids automatically — no setup required in your app. This is what lets [Firebase Test Lab's Robo test](https://firebase.google.com/docs/test-lab/android/robo-ux-test) and the Google Play Console's pre-launch report drive a real sign-in during automated testing, instead of typing into the wrong field or getting stuck on a screen it can't navigate.

**Why this matters:** a crawler that can't tell which field is the password will happily type a username into it, then hammer "sign in" and "forgot password" until your test account is buried in reset emails. Every field and button below resolves to one unambiguous resource id, so a crawler — or your own instrumented test — can target it directly.

**Tag reference.** Tags are grouped by screen; import `com.firebase.ui.auth.ui.FirebaseAuthTestTags`.

| Screen | Constant | Resource id |
|---|---|---|
| Sign in | `SignIn.EMAIL_FIELD` | `fui_sign_in_email_field` |
| | `SignIn.PASSWORD_FIELD` | `fui_sign_in_password_field` |
| | `SignIn.SIGN_IN_BUTTON` | `fui_sign_in_sign_in_button` |
| | `SignIn.SIGN_UP_BUTTON` | `fui_sign_in_sign_up_button` |
| | `SignIn.FORGOT_PASSWORD_BUTTON` | `fui_sign_in_forgot_password_button` |
| | `SignIn.EMAIL_LINK_BUTTON` | `fui_sign_in_email_link_button` |
| Sign up | `SignUp.NAME_FIELD` | `fui_sign_up_name_field` |
| | `SignUp.EMAIL_FIELD` | `fui_sign_up_email_field` |
| | `SignUp.PASSWORD_FIELD` | `fui_sign_up_password_field` |
| | `SignUp.CONFIRM_PASSWORD_FIELD` | `fui_sign_up_confirm_password_field` |
| | `SignUp.SIGN_UP_BUTTON` | `fui_sign_up_sign_up_button` |
| | `SignUp.SIGN_IN_BUTTON` | `fui_sign_up_sign_in_button` |
| Password recovery | `ResetPassword.EMAIL_FIELD` | `fui_reset_password_email_field` |
| | `ResetPassword.SEND_BUTTON` | `fui_reset_password_send_button` |
| | `ResetPassword.SIGN_IN_BUTTON` | `fui_reset_password_sign_in_button` |
| | `ResetPassword.DISMISS_BUTTON` | `fui_reset_password_dismiss_button` |
| Email link sign-in | `EmailLink.EMAIL_FIELD` | `fui_email_link_email_field` |
| | `EmailLink.SEND_LINK_BUTTON` | `fui_email_link_send_link_button` |
| | `EmailLink.PASSWORD_SIGN_IN_BUTTON` | `fui_email_link_password_sign_in_button` |
| | `EmailLink.DISMISS_BUTTON` | `fui_email_link_dismiss_button` |
| Phone number entry | `PhoneNumber.PHONE_NUMBER_FIELD` | `fui_phone_number_phone_number_field` |
| | `PhoneNumber.COUNTRY_SELECTOR_BUTTON` | `fui_phone_number_country_selector_button` |
| | `PhoneNumber.SEND_CODE_BUTTON` | `fui_phone_number_send_code_button` |
| SMS verification | `VerificationCode.CODE_FIELD` | `fui_verification_code_code_field` |
| | `VerificationCode.VERIFY_BUTTON` | `fui_verification_code_verify_button` |
| | `VerificationCode.RESEND_CODE_BUTTON` | `fui_verification_code_resend_code_button` |
| | `VerificationCode.CHANGE_PHONE_NUMBER_BUTTON` | `fui_verification_code_change_phone_number_button` |
| MFA sign-in challenge | `MfaChallenge.CODE_FIELD` | `fui_mfa_challenge_code_field` |
| | `MfaChallenge.VERIFY_BUTTON` | `fui_mfa_challenge_verify_button` |
| Re-authentication | `Reauth.PASSWORD_FIELD` | `fui_reauth_password_field` |
| | `Reauth.VERIFY_BUTTON` | `fui_reauth_verify_button` |
| | `Reauth.DISMISS_BUTTON` | `fui_reauth_dismiss_button` |
| Method picker | `MethodPicker.PROVIDER_LIST` | `fui_method_picker_provider_list` |
| | `MethodPicker.CONTINUE_AS_BUTTON` | `fui_method_picker_continue_as_button` |
| Country selector | `CountrySelector.COUNTRY_LIST` | `fui_country_selector_country_list` |

`VerificationCode.CODE_FIELD` and `MfaChallenge.CODE_FIELD` each name the whole six-digit input rather than an individual digit box: the field accepts a complete code in a single `ACTION_SET_TEXT`/`performTextInput` call and distributes it across the digit boxes, so one Robo directive or one `performTextInput("123456")` types the entire code.

**In your own instrumented tests**, target these the same way you'd target any other tag:

```kotlin
composeTestRule
.onNodeWithTag(FirebaseAuthTestTags.SignIn.EMAIL_FIELD)
.performTextInput("test@example.com")

composeTestRule
.onNodeWithTag(FirebaseAuthTestTags.SignIn.PASSWORD_FIELD)
.performTextInput("correcthorsebatterystaple")

composeTestRule
.onNodeWithTag(FirebaseAuthTestTags.SignIn.SIGN_IN_BUTTON)
.performClick()
```

Or with UiAutomator, by resource name:

```kotlin
device.findObject(By.res("fui_sign_in_email_field")).text = "test@example.com"
```

**With Firebase Test Lab.** Pass the resource ids as [Robo directives](https://firebase.google.com/docs/test-lab/android/command-line#robo-test-with-a-script) so the crawler fills real values instead of guessing:

```bash
gcloud firebase test android run \
--type=robo \
--app=app-debug.apk \
--robo-directives=fui_sign_in_email_field=test@example.com,fui_sign_in_password_field=correcthorsebatterystaple \
--device model=MediumPhone.arm,version=34
```

This is exactly the mechanism a **Play Console pre-launch report** uses, under **Test and release → Testing → Pre-launch report → Settings → Test account credentials**; the resource ids above are what you enter there for the username and password fields.

Verified with a real Firebase Test Lab Robo run against the sign-in screen (August 2026): the crawler resolved `fui_sign_in_email_field` and `fui_sign_in_password_field` as `android.widget.EditText` nodes, typed the directive values into both, and submitted via `fui_sign_in_sign_in_button` — along the way also navigating by resource id through sign-up, password recovery, and phone entry, confirming the tagging works generally rather than only where a directive points. Robo's crawling behavior is Google's, not ours, and can change independently of this library; treat this as a snapshot of current behavior rather than a permanent guarantee.

Renaming or removing a tag, or changing the resource id it resolves to, is a breaking change to FirebaseUI's public API — not an internal detail — so a value documented here will not change without a major version bump.

### Sign Out & Account Deletion

**Sign Out:**
Expand Down
8 changes: 8 additions & 0 deletions auth/src/main/java/com/firebase/ui/auth/AuthException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,14 @@ abstract class AuthException(
cause = firebaseException
)

// FirebaseAuthWebException code for backing out of the OAuth custom tab
"ERROR_WEB_CONTEXT_CANCELED" -> AuthCancelledException(
message = stringProvider?.errorAuthCancelled.nonEmpty()
?: firebaseException.message
?: "Authentication was cancelled",
cause = firebaseException
)

else -> UnknownException(
message = stringProvider?.errorUnknownAuth.nonEmpty()
?: firebaseException.message
Expand Down
21 changes: 16 additions & 5 deletions auth/src/main/java/com/firebase/ui/auth/AuthFlowController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.MainThread
import com.firebase.ui.auth.configuration.AuthUIConfiguration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -71,7 +72,10 @@ import java.util.concurrent.atomic.AtomicBoolean
* // Handle error
* }
* is AuthState.Cancelled -> {
* // User cancelled
* // User cancelled a single sign-in attempt; flow stays open
* }
* is AuthState.Aborted -> {
* // The whole flow was ended via authController.cancel()
* }
* else -> {}
* }
Expand All @@ -93,7 +97,7 @@ import java.util.concurrent.atomic.AtomicBoolean
* **Lifecycle Management:**
* - [createIntent] - Generate Intent to start the auth flow Activity
* - [start] - Alternative to launch the flow (for Activity context)
* - [cancel] - Cancel the ongoing auth flow, transitions to [AuthState.Cancelled]
* - [cancel] - Cancel the ongoing auth flow, transitions to [AuthState.Aborted]
* - [dispose] - Release all resources (coroutines, listeners). Call in onDestroy()
*
* @property authUI The [FirebaseAuthUI] instance managing authentication
Expand All @@ -120,7 +124,9 @@ class AuthFlowController internal constructor(
* - [AuthState.Loading] - Authentication in progress
* - [AuthState.Success] - User signed in successfully
* - [AuthState.Error] - Authentication error occurred
* - [AuthState.Cancelled] - User cancelled the flow
* - [AuthState.Cancelled] - Operation-level cancellation; the user cancelled a single
* sign-in attempt and the flow stays open
* - [AuthState.Aborted] - The whole flow was ended via [cancel]
* - [AuthState.RequiresMfa] - Multi-factor authentication required
* - [AuthState.RequiresEmailVerification] - Email verification required
*/
Expand Down Expand Up @@ -195,10 +201,14 @@ class AuthFlowController internal constructor(
/**
* Cancels the ongoing authentication flow.
*
* This method transitions the auth state to [AuthState.Cancelled] and
* This method transitions the auth state to [AuthState.Aborted] and
* signals the auth flow to terminate. The auth flow Activity will finish
* and return [Activity.RESULT_CANCELED].
*
* Unlike [AuthState.Cancelled] (an operation-level cancellation that leaves the flow
* open, e.g. dismissing the Google Credential Manager sheet), calling this method ends
* the entire flow.
*
* **Example:**
* ```kotlin
* // User clicked a "Cancel" button
Expand All @@ -209,9 +219,10 @@ class AuthFlowController internal constructor(
*
* @throws IllegalStateException if the controller has been disposed
*/
@MainThread
fun cancel() {
checkNotDisposed()
authUI.updateAuthState(AuthState.Cancelled)
authUI.updateAuthState(AuthState.Aborted)
}

/**
Expand Down
Loading