Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
Loading