Skip to content

Commit 55449ee

Browse files
committed
feat: MFA Screens
1 parent b14c136 commit 55449ee

File tree

104 files changed

+4312
-5
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+4312
-5
lines changed

auth/src/main/java/com/firebase/ui/auth/compose/FirebaseAuthUI.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,18 @@ class FirebaseAuthUI private constructor(
163163
val firebaseAuthFlow = callbackFlow {
164164
// Set initial state based on current auth state
165165
val initialState = auth.currentUser?.let { user ->
166-
AuthState.Success(result = null, user = user, isNewUser = false)
166+
// Check if email verification is required
167+
if (!user.isEmailVerified &&
168+
user.email != null &&
169+
user.providerData.any { it.providerId == "password" }
170+
) {
171+
AuthState.RequiresEmailVerification(
172+
user = user,
173+
email = user.email!!
174+
)
175+
} else {
176+
AuthState.Success(result = null, user = user, isNewUser = false)
177+
}
167178
} ?: AuthState.Idle
168179

169180
trySend(initialState)

auth/src/main/java/com/firebase/ui/auth/compose/configuration/string_provider/AuthUIStringProvider.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,24 @@ interface AuthUIStringProvider {
319319

320320
/** Helper text for recovery codes */
321321
val mfaStepShowRecoveryCodesHelper: String
322+
323+
// MFA Enrollment Screen Titles
324+
/** Title for MFA phone number enrollment screen (top app bar) */
325+
val mfaEnrollmentEnterPhoneNumber: String
326+
327+
/** Title for MFA SMS verification screen (top app bar) */
328+
val mfaEnrollmentVerifySmsCode: String
329+
330+
// MFA Error Messages
331+
/** Error message when MFA enrollment requires recent authentication */
332+
val mfaErrorRecentLoginRequired: String
333+
334+
/** Error message when MFA enrollment fails due to invalid verification code */
335+
val mfaErrorInvalidVerificationCode: String
336+
337+
/** Error message when MFA enrollment fails due to network issues */
338+
val mfaErrorNetwork: String
339+
340+
/** Generic error message for MFA enrollment failures */
341+
val mfaErrorGeneric: String
322342
}

auth/src/main/java/com/firebase/ui/auth/compose/configuration/string_provider/DefaultAuthUIStringProvider.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,4 +303,20 @@ class DefaultAuthUIStringProvider(
303303
get() = localizedContext.getString(R.string.fui_mfa_step_verify_factor_generic_helper)
304304
override val mfaStepShowRecoveryCodesHelper: String
305305
get() = localizedContext.getString(R.string.fui_mfa_step_show_recovery_codes_helper)
306+
307+
// MFA Enrollment Screen Titles
308+
override val mfaEnrollmentEnterPhoneNumber: String
309+
get() = localizedContext.getString(R.string.fui_mfa_enrollment_enter_phone_number)
310+
override val mfaEnrollmentVerifySmsCode: String
311+
get() = localizedContext.getString(R.string.fui_mfa_enrollment_verify_sms_code)
312+
313+
// MFA Error Messages
314+
override val mfaErrorRecentLoginRequired: String
315+
get() = localizedContext.getString(R.string.fui_mfa_error_recent_login_required)
316+
override val mfaErrorInvalidVerificationCode: String
317+
get() = localizedContext.getString(R.string.fui_mfa_error_invalid_verification_code)
318+
override val mfaErrorNetwork: String
319+
get() = localizedContext.getString(R.string.fui_mfa_error_network)
320+
override val mfaErrorGeneric: String
321+
get() = localizedContext.getString(R.string.fui_mfa_error_generic)
306322
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2025 Google Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the
10+
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
* express or implied. See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package com.firebase.ui.auth.compose.mfa
16+
17+
import com.firebase.ui.auth.compose.configuration.MfaFactor
18+
19+
/**
20+
* State class containing all the necessary information to render a custom UI for the
21+
* Multi-Factor Authentication (MFA) challenge flow during sign-in.
22+
*
23+
* This class is passed to the content slot of the MfaChallengeScreen composable, providing
24+
* access to the current factor, user input values, callbacks for actions, and loading/error states.
25+
*
26+
* The challenge flow is simpler than enrollment as the user has already configured their MFA:
27+
* 1. User enters their verification code (SMS or TOTP)
28+
* 2. System verifies the code and completes sign-in
29+
*
30+
* ```kotlin
31+
* MfaChallengeScreen(resolver, onSuccess, onCancel, onError) { state ->
32+
* Column {
33+
* Text("Enter your ${state.factorType} code")
34+
* TextField(
35+
* value = state.verificationCode,
36+
* onValueChange = state.onVerificationCodeChange
37+
* )
38+
* if (state.canResend) {
39+
* TextButton(onClick = state.onResendCodeClick) {
40+
* Text("Resend code")
41+
* }
42+
* }
43+
* Button(
44+
* onClick = state.onVerifyClick,
45+
* enabled = !state.isLoading && state.isValid
46+
* ) {
47+
* Text("Verify")
48+
* }
49+
* }
50+
* }
51+
* ```
52+
*
53+
* @property factorType The type of MFA factor being challenged (SMS or TOTP)
54+
* @property maskedPhoneNumber For SMS factors, the masked phone number (e.g., "+1••••••890")
55+
* @property isLoading `true` when verification is in progress. Use this to show loading indicators.
56+
* @property error An optional error message to display to the user. Will be `null` if there's no error.
57+
* @property verificationCode The current value of the verification code input field.
58+
* @property resendTimer The number of seconds remaining before the "Resend" action is available. Will be 0 when resend is allowed.
59+
* @property onVerificationCodeChange Callback invoked when the verification code input changes.
60+
* @property onVerifyClick Callback to verify the entered code and complete sign-in.
61+
* @property onResendCodeClick For SMS only: Callback to resend the verification code. `null` for TOTP.
62+
* @property onCancelClick Callback to cancel the MFA challenge and return to sign-in.
63+
*
64+
* @since 10.0.0
65+
*/
66+
data class MfaChallengeContentState(
67+
/** The type of MFA factor being challenged (SMS or TOTP). */
68+
val factorType: MfaFactor,
69+
70+
/** For SMS: the masked phone number. For TOTP: null. */
71+
val maskedPhoneNumber: String? = null,
72+
73+
/** `true` when verification is in progress. Use to show loading indicators. */
74+
val isLoading: Boolean = false,
75+
76+
/** Optional error message to display. `null` if no error. */
77+
val error: String? = null,
78+
79+
/** The current value of the verification code input field. */
80+
val verificationCode: String = "",
81+
82+
/** The number of seconds remaining before resend is available. 0 when ready. */
83+
val resendTimer: Int = 0,
84+
85+
/** Callback invoked when the verification code input changes. */
86+
val onVerificationCodeChange: (String) -> Unit = {},
87+
88+
/** Callback to verify the code and complete sign-in. */
89+
val onVerifyClick: () -> Unit = {},
90+
91+
/** For SMS only: Callback to resend the code. `null` for TOTP. */
92+
val onResendCodeClick: (() -> Unit)? = null,
93+
94+
/** Callback to cancel the challenge and return to sign-in. */
95+
val onCancelClick: () -> Unit = {}
96+
) {
97+
/**
98+
* Returns true if the current state is valid for verification.
99+
* The code must be 6 digits long.
100+
*/
101+
val isValid: Boolean
102+
get() = verificationCode.length == 6 && verificationCode.all { it.isDigit() }
103+
104+
/**
105+
* Returns true if there is an error in the current state.
106+
*/
107+
val hasError: Boolean
108+
get() = !error.isNullOrBlank()
109+
110+
/**
111+
* Returns true if the resend action is available (SMS only).
112+
*/
113+
val canResend: Boolean
114+
get() = factorType == MfaFactor.Sms && onResendCodeClick != null
115+
}

auth/src/main/java/com/firebase/ui/auth/compose/mfa/MfaEnrollmentContentState.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package com.firebase.ui.auth.compose.mfa
1616

1717
import com.firebase.ui.auth.compose.configuration.MfaFactor
1818
import com.firebase.ui.auth.compose.data.CountryData
19+
import com.google.firebase.auth.MultiFactorInfo
1920

2021
/**
2122
* State class containing all the necessary information to render a custom UI for the
@@ -66,6 +67,7 @@ import com.firebase.ui.auth.compose.data.CountryData
6667
* @property onVerificationCodeChange (Step: [MfaEnrollmentStep.VerifyFactor]) Callback invoked when the verification code input changes. Receives the new code string.
6768
* @property onVerifyClick (Step: [MfaEnrollmentStep.VerifyFactor]) Callback to verify the entered code and finalize MFA enrollment.
6869
* @property selectedFactor (Step: [MfaEnrollmentStep.VerifyFactor]) The MFA factor being verified (SMS or TOTP). Use this to customize UI messages.
70+
* @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.
6971
* @property onResendCodeClick (Step: [MfaEnrollmentStep.VerifyFactor], SMS only) Callback to resend the SMS verification code. Will be `null` for TOTP verification.
7072
*
7173
* @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`.
@@ -89,8 +91,12 @@ data class MfaEnrollmentContentState(
8991
// SelectFactor step
9092
val availableFactors: List<MfaFactor> = emptyList(),
9193

94+
val enrolledFactors: List<MultiFactorInfo> = emptyList(),
95+
9296
val onFactorSelected: (MfaFactor) -> Unit = {},
9397

98+
val onUnenrollFactor: (MultiFactorInfo) -> Unit = {},
99+
94100
val onSkipClick: (() -> Unit)? = null,
95101

96102
// ConfigureSms step
@@ -120,6 +126,8 @@ data class MfaEnrollmentContentState(
120126

121127
val selectedFactor: MfaFactor? = null,
122128

129+
val resendTimer: Int = 0,
130+
123131
val onResendCodeClick: (() -> Unit)? = null,
124132

125133
// ShowRecoveryCodes step
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2025 Google Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the
10+
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
* express or implied. See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package com.firebase.ui.auth.compose.mfa
16+
17+
import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider
18+
import com.google.firebase.FirebaseNetworkException
19+
import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException
20+
import com.google.firebase.auth.FirebaseAuthRecentLoginRequiredException
21+
import java.io.IOException
22+
23+
/**
24+
* Maps Firebase Auth exceptions to localized error messages for MFA enrollment.
25+
*
26+
* @param stringProvider Provider for localized strings
27+
* @return Localized error message appropriate for the exception type
28+
*/
29+
fun Exception.toMfaErrorMessage(stringProvider: AuthUIStringProvider): String {
30+
return when (this) {
31+
is FirebaseAuthRecentLoginRequiredException ->
32+
stringProvider.mfaErrorRecentLoginRequired
33+
is FirebaseAuthInvalidCredentialsException ->
34+
stringProvider.mfaErrorInvalidVerificationCode
35+
is IOException, is FirebaseNetworkException ->
36+
stringProvider.mfaErrorNetwork
37+
else -> stringProvider.mfaErrorGeneric
38+
}
39+
}

0 commit comments

Comments
 (0)