Skip to content

Commit 72ad750

Browse files
authored
feat: MFA Enrollment (TOTP) (#2238)
* feat: MFA Enrollment (TOTP) * remove duplicate doc * translation
1 parent ddf6719 commit 72ad750

File tree

95 files changed

+2602
-0
lines changed

Some content is hidden

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

95 files changed

+2602
-0
lines changed

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,4 +224,42 @@ interface AuthUIStringProvider {
224224

225225
/** Unknown error recovery message */
226226
val unknownErrorRecoveryMessage: String
227+
228+
// MFA Enrollment Step Titles
229+
/** Title for MFA factor selection step */
230+
val mfaStepSelectFactorTitle: String
231+
232+
/** Title for SMS MFA configuration step */
233+
val mfaStepConfigureSmsTitle: String
234+
235+
/** Title for TOTP MFA configuration step */
236+
val mfaStepConfigureTotpTitle: String
237+
238+
/** Title for MFA verification step */
239+
val mfaStepVerifyFactorTitle: String
240+
241+
/** Title for recovery codes step */
242+
val mfaStepShowRecoveryCodesTitle: String
243+
244+
// MFA Enrollment Helper Text
245+
/** Helper text for selecting MFA factor */
246+
val mfaStepSelectFactorHelper: String
247+
248+
/** Helper text for SMS configuration */
249+
val mfaStepConfigureSmsHelper: String
250+
251+
/** Helper text for TOTP configuration */
252+
val mfaStepConfigureTotpHelper: String
253+
254+
/** Helper text for SMS verification */
255+
val mfaStepVerifyFactorSmsHelper: String
256+
257+
/** Helper text for TOTP verification */
258+
val mfaStepVerifyFactorTotpHelper: String
259+
260+
/** Generic helper text for factor verification */
261+
val mfaStepVerifyFactorGenericHelper: String
262+
263+
/** Helper text for recovery codes */
264+
val mfaStepShowRecoveryCodesHelper: String
227265
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,4 +213,36 @@ class DefaultAuthUIStringProvider(
213213
get() = localizedContext.getString(R.string.fui_error_auth_cancelled_message)
214214
override val unknownErrorRecoveryMessage: String
215215
get() = localizedContext.getString(R.string.fui_error_unknown)
216+
217+
/**
218+
* MFA Enrollment Step Titles
219+
*/
220+
override val mfaStepSelectFactorTitle: String
221+
get() = localizedContext.getString(R.string.fui_mfa_step_select_factor_title)
222+
override val mfaStepConfigureSmsTitle: String
223+
get() = localizedContext.getString(R.string.fui_mfa_step_configure_sms_title)
224+
override val mfaStepConfigureTotpTitle: String
225+
get() = localizedContext.getString(R.string.fui_mfa_step_configure_totp_title)
226+
override val mfaStepVerifyFactorTitle: String
227+
get() = localizedContext.getString(R.string.fui_mfa_step_verify_factor_title)
228+
override val mfaStepShowRecoveryCodesTitle: String
229+
get() = localizedContext.getString(R.string.fui_mfa_step_show_recovery_codes_title)
230+
231+
/**
232+
* MFA Enrollment Helper Text
233+
*/
234+
override val mfaStepSelectFactorHelper: String
235+
get() = localizedContext.getString(R.string.fui_mfa_step_select_factor_helper)
236+
override val mfaStepConfigureSmsHelper: String
237+
get() = localizedContext.getString(R.string.fui_mfa_step_configure_sms_helper)
238+
override val mfaStepConfigureTotpHelper: String
239+
get() = localizedContext.getString(R.string.fui_mfa_step_configure_totp_helper)
240+
override val mfaStepVerifyFactorSmsHelper: String
241+
get() = localizedContext.getString(R.string.fui_mfa_step_verify_factor_sms_helper)
242+
override val mfaStepVerifyFactorTotpHelper: String
243+
get() = localizedContext.getString(R.string.fui_mfa_step_verify_factor_totp_helper)
244+
override val mfaStepVerifyFactorGenericHelper: String
245+
get() = localizedContext.getString(R.string.fui_mfa_step_verify_factor_generic_helper)
246+
override val mfaStepShowRecoveryCodesHelper: String
247+
get() = localizedContext.getString(R.string.fui_mfa_step_show_recovery_codes_helper)
216248
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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+
import com.firebase.ui.auth.compose.data.CountryData
19+
20+
/**
21+
* State class containing all the necessary information to render a custom UI for the
22+
* Multi-Factor Authentication (MFA) enrollment flow.
23+
*
24+
* This class is passed to the content slot of the MfaEnrollmentScreen composable, providing
25+
* access to the current step, user input values, callbacks for actions, and loading/error states.
26+
*
27+
* Use a `when` expression on [step] to determine which UI to render:
28+
*
29+
* ```kotlin
30+
* MfaEnrollmentScreen(user, config, onComplete, onSkip) { state ->
31+
* when (state.step) {
32+
* MfaEnrollmentStep.SelectFactor -> {
33+
* // Render factor selection UI using state.availableFactors
34+
* }
35+
* MfaEnrollmentStep.ConfigureTotp -> {
36+
* // Render TOTP setup UI using state.totpSecret and state.totpQrCodeUrl
37+
* }
38+
* MfaEnrollmentStep.VerifyFactor -> {
39+
* // Render verification UI using state.verificationCode
40+
* }
41+
* // ... other steps
42+
* }
43+
* }
44+
* ```
45+
*
46+
* @property step The current step in the enrollment flow. Use this to determine which UI to display.
47+
* @property isLoading `true` when an asynchronous operation (like generating a secret or verifying a code) is in progress. Use this to show loading indicators.
48+
* @property error An optional error message to display to the user. Will be `null` if there's no error.
49+
* @property onBackClick Callback to navigate to the previous step in the flow. Invoked when the user clicks a back button.
50+
*
51+
* @property availableFactors (Step: [MfaEnrollmentStep.SelectFactor]) A list of MFA factors the user can choose from (e.g., SMS, TOTP). Determined by [com.firebase.ui.auth.compose.configuration.MfaConfiguration.allowedFactors].
52+
* @property onFactorSelected (Step: [MfaEnrollmentStep.SelectFactor]) Callback invoked when the user selects an MFA factor. Receives the selected [MfaFactor].
53+
* @property onSkipClick (Step: [MfaEnrollmentStep.SelectFactor]) Callback for the "Skip" action. Will be `null` if MFA enrollment is required via [com.firebase.ui.auth.compose.configuration.MfaConfiguration.requireEnrollment].
54+
*
55+
* @property phoneNumber (Step: [MfaEnrollmentStep.ConfigureSms]) The current value of the phone number input field. Does not include country code prefix.
56+
* @property onPhoneNumberChange (Step: [MfaEnrollmentStep.ConfigureSms]) Callback invoked when the phone number input changes. Receives the new phone number string.
57+
* @property selectedCountry (Step: [MfaEnrollmentStep.ConfigureSms]) The currently selected country for phone number formatting. Contains dial code, country code, and flag.
58+
* @property onCountrySelected (Step: [MfaEnrollmentStep.ConfigureSms]) Callback invoked when the user selects a different country. Receives the new [CountryData].
59+
* @property onSendSmsCodeClick (Step: [MfaEnrollmentStep.ConfigureSms]) Callback to send the SMS verification code to the entered phone number.
60+
*
61+
* @property totpSecret (Step: [MfaEnrollmentStep.ConfigureTotp]) The TOTP secret containing the shared key and configuration. Use this to display the secret key or access the underlying Firebase TOTP secret.
62+
* @property totpQrCodeUrl (Step: [MfaEnrollmentStep.ConfigureTotp]) A URI that can be rendered as a QR code or used as a deep link to open authenticator apps. Generated via [TotpSecret.generateQrCodeUrl].
63+
* @property onContinueToVerifyClick (Step: [MfaEnrollmentStep.ConfigureTotp]) Callback to proceed to the verification step after the user has scanned the QR code or entered the secret.
64+
*
65+
* @property verificationCode (Step: [MfaEnrollmentStep.VerifyFactor]) The current value of the verification code input field. Should be a 6-digit string.
66+
* @property onVerificationCodeChange (Step: [MfaEnrollmentStep.VerifyFactor]) Callback invoked when the verification code input changes. Receives the new code string.
67+
* @property onVerifyClick (Step: [MfaEnrollmentStep.VerifyFactor]) Callback to verify the entered code and finalize MFA enrollment.
68+
* @property selectedFactor (Step: [MfaEnrollmentStep.VerifyFactor]) The MFA factor being verified (SMS or TOTP). Use this to customize UI messages.
69+
* @property onResendCodeClick (Step: [MfaEnrollmentStep.VerifyFactor], SMS only) Callback to resend the SMS verification code. Will be `null` for TOTP verification.
70+
*
71+
* @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`.
72+
* @property onCodesSavedClick (Step: [MfaEnrollmentStep.ShowRecoveryCodes]) Callback invoked when the user confirms they have saved their recovery codes. Completes the enrollment flow.
73+
*
74+
* @since 10.0.0
75+
*/
76+
data class MfaEnrollmentContentState(
77+
/** The current step in the enrollment flow. Use this to determine which UI to display. */
78+
val step: MfaEnrollmentStep,
79+
80+
/** `true` when an async operation is in progress. Use to show loading indicators. */
81+
val isLoading: Boolean = false,
82+
83+
/** Optional error message to display. `null` if no error. */
84+
val error: String? = null,
85+
86+
/** Callback to navigate to the previous step. */
87+
val onBackClick: () -> Unit = {},
88+
89+
// SelectFactor step
90+
val availableFactors: List<MfaFactor> = emptyList(),
91+
92+
val onFactorSelected: (MfaFactor) -> Unit = {},
93+
94+
val onSkipClick: (() -> Unit)? = null,
95+
96+
// ConfigureSms step
97+
val phoneNumber: String = "",
98+
99+
val onPhoneNumberChange: (String) -> Unit = {},
100+
101+
val selectedCountry: CountryData? = null,
102+
103+
val onCountrySelected: (CountryData) -> Unit = {},
104+
105+
val onSendSmsCodeClick: () -> Unit = {},
106+
107+
// ConfigureTotp step
108+
val totpSecret: TotpSecret? = null,
109+
110+
val totpQrCodeUrl: String? = null,
111+
112+
val onContinueToVerifyClick: () -> Unit = {},
113+
114+
// VerifyFactor step
115+
val verificationCode: String = "",
116+
117+
val onVerificationCodeChange: (String) -> Unit = {},
118+
119+
val onVerifyClick: () -> Unit = {},
120+
121+
val selectedFactor: MfaFactor? = null,
122+
123+
val onResendCodeClick: (() -> Unit)? = null,
124+
125+
// ShowRecoveryCodes step
126+
val recoveryCodes: List<String>? = null,
127+
128+
val onCodesSavedClick: () -> Unit = {}
129+
) {
130+
/**
131+
* Returns true if the current state is valid for the current step.
132+
*
133+
* This can be used to enable/disable action buttons in the UI.
134+
*/
135+
val isValid: Boolean
136+
get() = when (step) {
137+
MfaEnrollmentStep.SelectFactor -> availableFactors.isNotEmpty()
138+
MfaEnrollmentStep.ConfigureSms -> phoneNumber.isNotBlank()
139+
MfaEnrollmentStep.ConfigureTotp -> totpSecret != null && totpQrCodeUrl != null
140+
MfaEnrollmentStep.VerifyFactor -> verificationCode.length == 6
141+
MfaEnrollmentStep.ShowRecoveryCodes -> !recoveryCodes.isNullOrEmpty()
142+
}
143+
144+
/**
145+
* Returns true if there is an error in the current state.
146+
*/
147+
val hasError: Boolean
148+
get() = !error.isNullOrBlank()
149+
150+
/**
151+
* Returns true if the skip action is available (only for SelectFactor step when not required).
152+
*/
153+
val canSkip: Boolean
154+
get() = step == MfaEnrollmentStep.SelectFactor && onSkipClick != null
155+
156+
/**
157+
* Returns true if the back action is available (for all steps except SelectFactor).
158+
*/
159+
val canGoBack: Boolean
160+
get() = step != MfaEnrollmentStep.SelectFactor
161+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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+
import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider
19+
20+
/**
21+
* Represents the different steps in the Multi-Factor Authentication (MFA) enrollment flow.
22+
*
23+
* This enum defines the sequence of UI states that users progress through when enrolling
24+
* in MFA, from selecting a factor to completing the setup with recovery codes.
25+
*
26+
* @since 10.0.0
27+
*/
28+
enum class MfaEnrollmentStep {
29+
/**
30+
* The user is presented with a selection of available MFA factors to enroll in.
31+
* The available factors are determined by the [com.firebase.ui.auth.compose.configuration.MfaConfiguration].
32+
*/
33+
SelectFactor,
34+
35+
/**
36+
* The user is configuring SMS-based MFA by entering their phone number.
37+
* This step prepares to send an SMS verification code to the provided number.
38+
*/
39+
ConfigureSms,
40+
41+
/**
42+
* The user is configuring TOTP (Time-based One-Time Password) MFA.
43+
* This step presents the TOTP secret (as both text and QR code) for the user
44+
* to scan into their authenticator app.
45+
*/
46+
ConfigureTotp,
47+
48+
/**
49+
* The user is verifying their chosen MFA factor by entering a verification code.
50+
* For SMS, this is the code received via text message.
51+
* For TOTP, this is the code generated by their authenticator app.
52+
*/
53+
VerifyFactor,
54+
55+
/**
56+
* The enrollment is complete and recovery codes are displayed to the user.
57+
* These backup codes can be used to sign in if the primary MFA method is unavailable.
58+
* This step only appears if recovery codes are enabled in the configuration.
59+
*/
60+
ShowRecoveryCodes
61+
}
62+
63+
/**
64+
* Returns the localized title text for this enrollment step.
65+
*
66+
* @param stringProvider The string provider for localized strings
67+
* @return The localized title for this step
68+
*/
69+
fun MfaEnrollmentStep.getTitle(stringProvider: AuthUIStringProvider): String = when (this) {
70+
MfaEnrollmentStep.SelectFactor -> stringProvider.mfaStepSelectFactorTitle
71+
MfaEnrollmentStep.ConfigureSms -> stringProvider.mfaStepConfigureSmsTitle
72+
MfaEnrollmentStep.ConfigureTotp -> stringProvider.mfaStepConfigureTotpTitle
73+
MfaEnrollmentStep.VerifyFactor -> stringProvider.mfaStepVerifyFactorTitle
74+
MfaEnrollmentStep.ShowRecoveryCodes -> stringProvider.mfaStepShowRecoveryCodesTitle
75+
}
76+
77+
/**
78+
* Returns localized helper text providing instructions for this step.
79+
*
80+
* @param stringProvider The string provider for localized strings
81+
* @param selectedFactor The MFA factor being configured or verified. Used for [MfaEnrollmentStep.VerifyFactor]
82+
* to provide factor-specific instructions. Ignored for other steps.
83+
* @return Localized instructional text appropriate for this step
84+
*/
85+
fun MfaEnrollmentStep.getHelperText(
86+
stringProvider: AuthUIStringProvider,
87+
selectedFactor: MfaFactor? = null
88+
): String = when (this) {
89+
MfaEnrollmentStep.SelectFactor -> stringProvider.mfaStepSelectFactorHelper
90+
MfaEnrollmentStep.ConfigureSms -> stringProvider.mfaStepConfigureSmsHelper
91+
MfaEnrollmentStep.ConfigureTotp -> stringProvider.mfaStepConfigureTotpHelper
92+
MfaEnrollmentStep.VerifyFactor -> when (selectedFactor) {
93+
MfaFactor.Sms -> stringProvider.mfaStepVerifyFactorSmsHelper
94+
MfaFactor.Totp -> stringProvider.mfaStepVerifyFactorTotpHelper
95+
null -> stringProvider.mfaStepVerifyFactorGenericHelper
96+
}
97+
MfaEnrollmentStep.ShowRecoveryCodes -> stringProvider.mfaStepShowRecoveryCodesHelper
98+
}

0 commit comments

Comments
 (0)