Skip to content

sample(auth): full customization demo#2414

Draft
demolaf wants to merge 1 commit into
sample/firestore-database-storagefrom
sample/full-customization-method-picker
Draft

sample(auth): full customization demo#2414
demolaf wants to merge 1 commit into
sample/firestore-database-storagefrom
sample/full-customization-method-picker

Conversation

@demolaf

@demolaf demolaf commented Jul 22, 2026

Copy link
Copy Markdown
Member

Preview

  • Full Customization Flow
full-customization-flow.webm
  • Other sign in methods via bottom sheet
other-sign-in-methods.webm
  • Error Dialog (Inherits font change)
Screenshot_1784723834

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a fully customized authentication flow demo (FullCustomizationDemoActivity and associated steps) to showcase the capabilities of customMethodPickerLayout in FirebaseAuthScreen, alongside minor inset adjustments in the default auth screens. The review feedback is highly constructive and should be addressed: it recommends implementing a BackHandler for proper step-back navigation, making the alternative sign-in methods sheet scrollable to prevent clipping on smaller screens, passing the FirebaseAuth instance dynamically to support custom configurations, performing case-insensitive and trimmed email validation, and ensuring the loading indicator in CtaButton has sufficient contrast.

Comment on lines +29 to +42
var flowStep by remember { mutableStateOf(FlowStep.EnterEmail) }
var showOtherMethods by remember { mutableStateOf(false) }
var isCheckingEmail by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()

// Password/confirmPassword are hoisted in EmailAuthContentState, not local to LoginStep/
// SignUpStep — they survive a round trip back to EnterEmail, so a stale password typed for
// one email could carry over if a different email also routes to the same step. Clear them
// whenever the user backs out via "Use a different email".
val onUseDifferentEmail: () -> Unit = {
state.onPasswordChange("")
state.onConfirmPasswordChange("")
flowStep = FlowStep.EnterEmail
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When the user is on the Login or SignUp step, pressing the system back button will exit the entire activity/screen instead of returning to the EnterEmail step. Adding a BackHandler when flowStep != FlowStep.EnterEmail intercepts the back press and routes the user back to the email entry step, matching standard Android navigation expectations.

    var flowStep by remember { mutableStateOf(FlowStep.EnterEmail) }
    var showOtherMethods by remember { mutableStateOf(false) }
    var isCheckingEmail by remember { mutableStateOf(false) }
    val coroutineScope = rememberCoroutineScope()

    // Password/confirmPassword are hoisted in EmailAuthContentState, not local to LoginStep/
    // SignUpStep — they survive a round trip back to EnterEmail, so a stale password typed for
    // one email could carry over if a different email also routes to the same step. Clear them
    // whenever the user backs out via "Use a different email".
    val onUseDifferentEmail: () -> Unit = {
        state.onPasswordChange("")
        state.onConfirmPasswordChange("")
        flowStep = FlowStep.EnterEmail
    }

    androidx.activity.compose.BackHandler(enabled = flowStep != FlowStep.EnterEmail) {
        onUseDifferentEmail()
    }

Comment on lines +35 to +39
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 64.dp),
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

With up to 9 alternative identity providers configured, the list of buttons and the Terms of Service footer will easily exceed the available screen height on smaller devices or in landscape orientation. Since the sheet's Column is not scrollable, this will cause content to be clipped and become inaccessible. Wrapping the column in a scrollable modifier (e.g., .verticalScroll(rememberScrollState())) ensures all options remain accessible.

        Column(
            modifier = Modifier
                .fillMaxWidth()
                .verticalScroll(androidx.compose.foundation.rememberScrollState())
                .padding(horizontal = 64.dp),
        ) {

Comment on lines +22 to +28
fun SingleFieldSignInUI(
state: EmailAuthContentState,
otherProviders: List<AuthProvider>,
onProviderSelected: (AuthProvider) -> Unit,
tosUrl: String?,
ppUrl: String?,
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To support multi-app configurations or custom Firebase Auth instances (such as when using the emulator), avoid calling FirebaseAuth.getInstance() directly. Instead, pass the configured FirebaseAuth instance from the parent screen.

fun SingleFieldSignInUI(
    state: EmailAuthContentState,
    auth: FirebaseAuth,
    otherProviders: List<AuthProvider>,
    onProviderSelected: (AuthProvider) -> Unit,
    tosUrl: String?,
    ppUrl: String?,
)

Comment on lines +50 to +57
onContinue = {
isCheckingEmail = true
coroutineScope.launch {
val signInMethods = fetchLegacySignInMethods(state.email)
flowStep = if (signInMethods.isEmpty()) FlowStep.SignUp else FlowStep.Login
isCheckingEmail = false
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Pass the auth instance to fetchLegacySignInMethods.

Suggested change
onContinue = {
isCheckingEmail = true
coroutineScope.launch {
val signInMethods = fetchLegacySignInMethods(state.email)
flowStep = if (signInMethods.isEmpty()) FlowStep.SignUp else FlowStep.Login
isCheckingEmail = false
}
},
onContinue = {
isCheckingEmail = true
coroutineScope.launch {
val signInMethods = fetchLegacySignInMethods(auth, state.email)
flowStep = if (signInMethods.isEmpty()) FlowStep.SignUp else FlowStep.Login
isCheckingEmail = false
}
},

Comment on lines +89 to +102
private suspend fun fetchLegacySignInMethods(email: String): List<String> {
return try {
@Suppress("DEPRECATION")
FirebaseAuth.getInstance()
.fetchSignInMethodsForEmail(email)
.await()
.signInMethods
?.filter { it.isNotBlank() }
?: emptyList()
} catch (e: Exception) {
Log.w("SingleFieldSignInUI", "fetchSignInMethodsForEmail failed for $email", e)
emptyList()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update fetchLegacySignInMethods to accept and use the passed FirebaseAuth instance instead of retrieving the default global instance.

private suspend fun fetchLegacySignInMethods(auth: FirebaseAuth, email: String): List<String> {
    return try {
        @Suppress("DEPRECATION")
        auth.fetchSignInMethodsForEmail(email)
            .await()
            .signInMethods
            ?.filter { it.isNotBlank() }
            ?: emptyList()
    } catch (e: Exception) {
        Log.w("SingleFieldSignInUI", "fetchSignInMethodsForEmail failed for $email", e)
        emptyList()
    }
}

Comment on lines +132 to +140
) { state ->
SingleFieldSignInUI(
state = state,
otherProviders = providers.filterNot { it is AuthProvider.Email },
onProviderSelected = onProviderSelected,
tosUrl = configuration.tosUrl,
ppUrl = configuration.privacyPolicyUrl,
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Pass authUI.auth to SingleFieldSignInUI to match the updated signature.

            ) { state ->
                SingleFieldSignInUI(
                    state = state,
                    auth = authUI.auth,
                    otherProviders = providers.filterNot { it is AuthProvider.Email },
                    onProviderSelected = onProviderSelected,
                    tosUrl = configuration.tosUrl,
                    ppUrl = configuration.privacyPolicyUrl,
                )
            }

var lastName by remember { mutableStateOf("") }
var confirmEmail by remember { mutableStateOf("") }

val emailsMatch = confirmEmail.isNotBlank() && confirmEmail == state.email

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Email addresses should be compared case-insensitively and ignoring leading/trailing whitespace, as mobile keyboards often auto-capitalize the first letter or append spaces. Using equals(..., ignoreCase = true) and trim() prevents validation failures due to casing or accidental spacing differences.

Suggested change
val emailsMatch = confirmEmail.isNotBlank() && confirmEmail == state.email
val emailsMatch = confirmEmail.isNotBlank() && confirmEmail.trim().equals(state.email.trim(), ignoreCase = true)

Comment on lines +46 to +48
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.size(20.dp))
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

By default, CircularProgressIndicator uses the theme's primary color. Since CtaButton also uses the primary color as its default background, the progress indicator will have extremely low contrast or be completely invisible. Setting its color to LocalContentColor.current ensures it matches the button's text color (e.g., onPrimary) and remains clearly visible.

            if (isLoading) {
                CircularProgressIndicator(
                    modifier = Modifier.size(20.dp),
                    color = androidx.compose.material3.LocalContentColor.current
                )
            } else {

@demolaf
demolaf changed the base branch from sample/firestore-database-storage to fix/mfa-edge-to-edge-insets July 22, 2026 13:02
@demolaf
demolaf changed the base branch from fix/mfa-edge-to-edge-insets to sample/firestore-database-storage July 22, 2026 13:03
@demolaf demolaf added the sample label Jul 22, 2026
@demolaf demolaf changed the title sample(auth): flesh out full customization demo with email/login/sign… sample(auth): flesh out full customization demo Jul 22, 2026
@demolaf demolaf changed the title sample(auth): flesh out full customization demo sample(auth): full customization demo Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant