Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ google-services.json
crashlytics-build.properties
auth/src/main/res/values/com_crashlytics_export_strings.xml
*.log
composeapp/.firebaserc
composeapp/firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,18 @@ class FirebaseAuthUI private constructor(
val firebaseAuthFlow = callbackFlow {
// Set initial state based on current auth state
val initialState = auth.currentUser?.let { user ->
AuthState.Success(result = null, user = user, isNewUser = false)
// Check if email verification is required
if (!user.isEmailVerified &&
user.email != null &&
user.providerData.any { it.providerId == "password" }
) {
AuthState.RequiresEmailVerification(
user = user,
email = user.email!!
)
} else {
AuthState.Success(result = null, user = user, isNewUser = false)
}
} ?: AuthState.Idle

trySend(initialState)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,24 @@ interface AuthUIStringProvider {

/** Helper text for recovery codes */
val mfaStepShowRecoveryCodesHelper: String

// MFA Enrollment Screen Titles
/** Title for MFA phone number enrollment screen (top app bar) */
val mfaEnrollmentEnterPhoneNumber: String

/** Title for MFA SMS verification screen (top app bar) */
val mfaEnrollmentVerifySmsCode: String

// MFA Error Messages
/** Error message when MFA enrollment requires recent authentication */
val mfaErrorRecentLoginRequired: String

/** Error message when MFA enrollment fails due to invalid verification code */
val mfaErrorInvalidVerificationCode: String

/** Error message when MFA enrollment fails due to network issues */
val mfaErrorNetwork: String

/** Generic error message for MFA enrollment failures */
val mfaErrorGeneric: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,20 @@ class DefaultAuthUIStringProvider(
get() = localizedContext.getString(R.string.fui_mfa_step_verify_factor_generic_helper)
override val mfaStepShowRecoveryCodesHelper: String
get() = localizedContext.getString(R.string.fui_mfa_step_show_recovery_codes_helper)

// MFA Enrollment Screen Titles
override val mfaEnrollmentEnterPhoneNumber: String
get() = localizedContext.getString(R.string.fui_mfa_enrollment_enter_phone_number)
override val mfaEnrollmentVerifySmsCode: String
get() = localizedContext.getString(R.string.fui_mfa_enrollment_verify_sms_code)

// MFA Error Messages
override val mfaErrorRecentLoginRequired: String
get() = localizedContext.getString(R.string.fui_mfa_error_recent_login_required)
override val mfaErrorInvalidVerificationCode: String
get() = localizedContext.getString(R.string.fui_mfa_error_invalid_verification_code)
override val mfaErrorNetwork: String
get() = localizedContext.getString(R.string.fui_mfa_error_network)
override val mfaErrorGeneric: String
get() = localizedContext.getString(R.string.fui_mfa_error_generic)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright 2025 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.firebase.ui.auth.compose.mfa

import com.firebase.ui.auth.compose.configuration.MfaFactor

/**
* State class containing all the necessary information to render a custom UI for the
* Multi-Factor Authentication (MFA) challenge flow during sign-in.
*
* This class is passed to the content slot of the MfaChallengeScreen composable, providing
* access to the current factor, user input values, callbacks for actions, and loading/error states.
*
* The challenge flow is simpler than enrollment as the user has already configured their MFA:
* 1. User enters their verification code (SMS or TOTP)
* 2. System verifies the code and completes sign-in
*
* ```kotlin
* MfaChallengeScreen(resolver, onSuccess, onCancel, onError) { state ->
* Column {
* Text("Enter your ${state.factorType} code")
* TextField(
* value = state.verificationCode,
* onValueChange = state.onVerificationCodeChange
* )
* if (state.canResend) {
* TextButton(onClick = state.onResendCodeClick) {
* Text("Resend code")
* }
* }
* Button(
* onClick = state.onVerifyClick,
* enabled = !state.isLoading && state.isValid
* ) {
* Text("Verify")
* }
* }
* }
* ```
*
* @property factorType The type of MFA factor being challenged (SMS or TOTP)
* @property maskedPhoneNumber For SMS factors, the masked phone number (e.g., "+1••••••890")
* @property isLoading `true` when verification is in progress. Use this to show loading indicators.
* @property error An optional error message to display to the user. Will be `null` if there's no error.
* @property verificationCode The current value of the verification code input field.
* @property resendTimer The number of seconds remaining before the "Resend" action is available. Will be 0 when resend is allowed.
* @property onVerificationCodeChange Callback invoked when the verification code input changes.
* @property onVerifyClick Callback to verify the entered code and complete sign-in.
* @property onResendCodeClick For SMS only: Callback to resend the verification code. `null` for TOTP.
* @property onCancelClick Callback to cancel the MFA challenge and return to sign-in.
*
* @since 10.0.0
*/
data class MfaChallengeContentState(
/** The type of MFA factor being challenged (SMS or TOTP). */
val factorType: MfaFactor,

/** For SMS: the masked phone number. For TOTP: null. */
val maskedPhoneNumber: String? = null,

/** `true` when verification is in progress. Use to show loading indicators. */
val isLoading: Boolean = false,

/** Optional error message to display. `null` if no error. */
val error: String? = null,

/** The current value of the verification code input field. */
val verificationCode: String = "",

/** The number of seconds remaining before resend is available. 0 when ready. */
val resendTimer: Int = 0,

/** Callback invoked when the verification code input changes. */
val onVerificationCodeChange: (String) -> Unit = {},

/** Callback to verify the code and complete sign-in. */
val onVerifyClick: () -> Unit = {},

/** For SMS only: Callback to resend the code. `null` for TOTP. */
val onResendCodeClick: (() -> Unit)? = null,

/** Callback to cancel the challenge and return to sign-in. */
val onCancelClick: () -> Unit = {}
) {
/**
* Returns true if the current state is valid for verification.
* The code must be 6 digits long.
*/
val isValid: Boolean
get() = verificationCode.length == 6 && verificationCode.all { it.isDigit() }

/**
* Returns true if there is an error in the current state.
*/
val hasError: Boolean
get() = !error.isNullOrBlank()

/**
* Returns true if the resend action is available (SMS only).
*/
val canResend: Boolean
get() = factorType == MfaFactor.Sms && onResendCodeClick != null
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package com.firebase.ui.auth.compose.mfa

import com.firebase.ui.auth.compose.configuration.MfaFactor
import com.firebase.ui.auth.compose.data.CountryData
import com.google.firebase.auth.MultiFactorInfo

/**
* State class containing all the necessary information to render a custom UI for the
Expand Down Expand Up @@ -66,6 +67,7 @@ import com.firebase.ui.auth.compose.data.CountryData
* @property onVerificationCodeChange (Step: [MfaEnrollmentStep.VerifyFactor]) Callback invoked when the verification code input changes. Receives the new code string.
* @property onVerifyClick (Step: [MfaEnrollmentStep.VerifyFactor]) Callback to verify the entered code and finalize MFA enrollment.
* @property selectedFactor (Step: [MfaEnrollmentStep.VerifyFactor]) The MFA factor being verified (SMS or TOTP). Use this to customize UI messages.
* @property resendTimer (Step: [MfaEnrollmentStep.VerifyFactor], SMS only) The number of seconds remaining before the "Resend" action is available. Will be 0 when resend is allowed.
* @property onResendCodeClick (Step: [MfaEnrollmentStep.VerifyFactor], SMS only) Callback to resend the SMS verification code. Will be `null` for TOTP verification.
*
* @property recoveryCodes (Step: [MfaEnrollmentStep.ShowRecoveryCodes]) A list of one-time backup codes the user should save. Only present if [com.firebase.ui.auth.compose.configuration.MfaConfiguration.enableRecoveryCodes] is `true`.
Expand All @@ -89,8 +91,12 @@ data class MfaEnrollmentContentState(
// SelectFactor step
val availableFactors: List<MfaFactor> = emptyList(),

val enrolledFactors: List<MultiFactorInfo> = emptyList(),

val onFactorSelected: (MfaFactor) -> Unit = {},

val onUnenrollFactor: (MultiFactorInfo) -> Unit = {},

val onSkipClick: (() -> Unit)? = null,

// ConfigureSms step
Expand Down Expand Up @@ -120,6 +126,8 @@ data class MfaEnrollmentContentState(

val selectedFactor: MfaFactor? = null,

val resendTimer: Int = 0,

val onResendCodeClick: (() -> Unit)? = null,

// ShowRecoveryCodes step
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2025 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.firebase.ui.auth.compose.mfa

import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider
import com.google.firebase.FirebaseNetworkException
import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException
import com.google.firebase.auth.FirebaseAuthRecentLoginRequiredException
import java.io.IOException

/**
* Maps Firebase Auth exceptions to localized error messages for MFA enrollment.
*
* @param stringProvider Provider for localized strings
* @return Localized error message appropriate for the exception type
*/
fun Exception.toMfaErrorMessage(stringProvider: AuthUIStringProvider): String {
return when (this) {
is FirebaseAuthRecentLoginRequiredException ->
stringProvider.mfaErrorRecentLoginRequired
is FirebaseAuthInvalidCredentialsException ->
stringProvider.mfaErrorInvalidVerificationCode
is IOException, is FirebaseNetworkException ->
stringProvider.mfaErrorNetwork
else -> stringProvider.mfaErrorGeneric
}
}
Loading