Skip to content
Draft
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
10 changes: 2 additions & 8 deletions auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Built entirely with **Jetpack Compose** and **Material Design 3**, FirebaseUI Au

- **Simple API** - Choose between high-level screens or low-level controllers for maximum flexibility
- **12+ Authentication Methods** - Email/Password, Phone, Google, Facebook, Twitter, GitHub, Microsoft, Yahoo, Apple, Anonymous, and custom OAuth providers
- **Multi-Factor Authentication** - SMS and TOTP (Time-based One-Time Password) with recovery codes
- **Multi-Factor Authentication** - SMS and TOTP (Time-based One-Time Password)
- **Android Credential Manager** - Automatic credential saving and one-tap sign-in
- **Material Design 3** - Beautiful, themeable UI components that integrate seamlessly with your app
- **Localization Support** - Customizable strings for internationalization
Expand Down Expand Up @@ -1073,10 +1073,7 @@ val mfaConfig = MfaConfiguration(
allowedFactors = listOf(MfaFactor.Sms, MfaFactor.Totp),

// Optional: Require MFA enrollment (default: false)
requireEnrollment = false,

// Optional: Enable recovery codes (default: true)
enableRecoveryCodes = true
requireEnrollment = false
)

val configuration = authUIConfiguration {
Expand Down Expand Up @@ -1138,9 +1135,6 @@ MfaEnrollmentScreen(
MfaEnrollmentStep.VerifyFactor -> {
CustomVerificationUI(state)
}
MfaEnrollmentStep.ShowRecoveryCodes -> {
CustomRecoveryCodesUI(state)
}
}
}
```
Expand Down
22 changes: 0 additions & 22 deletions auth/src/main/java/com/firebase/ui/auth/FirebaseAuthUI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.tasks.await
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong

/**
* The central class that coordinates all authentication operations for Firebase Auth UI Compose.
Expand Down Expand Up @@ -80,7 +79,6 @@ class FirebaseAuthUI private constructor(
) {

private val _authStateFlow = MutableStateFlow<AuthState>(AuthState.Idle)
private val authStateRevision = AtomicLong(0)

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
var testCredentialManagerProvider: AuthProvider.Google.CredentialManagerProvider? = null
Expand Down Expand Up @@ -365,29 +363,9 @@ class FirebaseAuthUI private constructor(
*/
@MainThread
fun updateAuthState(state: AuthState) {
authStateRevision.incrementAndGet()
_authStateFlow.value = state
}

/**
* Retracts a pending [AuthState.Loading] by resetting to [AuthState.Idle], but only while
* [revision] is still the most recent write. Any state emitted since is left untouched.
*
* The revision is what makes this precise: [AuthState.Loading] compares equal whenever the
* message matches, and [MutableStateFlow] drops a write equal to the current value without
* replacing the stored reference - so neither equality nor identity can tell a concurrent
* operation's Loading apart from the caller's.
*
* @param revision The value [currentAuthStateRevision] returned right after the caller emitted
* the [AuthState.Loading] it now wants to retract
*/
internal fun clearLoadingState(revision: Long) {
if (authStateRevision.get() == revision) updateAuthState(AuthState.Idle)
}

/** Identifies the most recent [updateAuthState] write. See [clearLoadingState]. */
internal fun currentAuthStateRevision(): Long = authStateRevision.get()

internal fun updateAuthStateWithResult(result: AuthResult?, defaultIsNewUser: Boolean = false) {
val user = result?.user
if (user != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,18 @@ package com.firebase.ui.auth.configuration
/**
* Configuration class for Multi-Factor Authentication (MFA) enrollment and verification behavior.
*
* This class controls which MFA factors are available to users, whether enrollment is mandatory,
* and whether recovery codes are generated.
* This class controls which MFA factors are available to users and whether enrollment is
* mandatory.
*
* @property allowedFactors List of MFA factors that users are permitted to enroll in.
* Defaults to [MfaFactor.Sms, MfaFactor.Totp].
* @property requireEnrollment Whether MFA enrollment is mandatory for all users.
* When true, users must enroll in at least one MFA factor.
* Defaults to false.
* @property enableRecoveryCodes Whether to generate and provide recovery codes to users
* after successful MFA enrollment. These codes can be used
* as a backup authentication method. Defaults to true.
*/
class MfaConfiguration(
val allowedFactors: List<MfaFactor> = listOf(MfaFactor.Sms, MfaFactor.Totp),
val requireEnrollment: Boolean = false,
val enableRecoveryCodes: Boolean = true
val requireEnrollment: Boolean = false
) {
init {
require(allowedFactors.isNotEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,8 @@ internal suspend fun FirebaseAuthUI.verifyPhoneNumber(
forceResendingToken: PhoneAuthProvider.ForceResendingToken? = null,
verifier: AuthProvider.Phone.Verifier = AuthProvider.Phone.DefaultVerifier(),
) {
// -1 never matches a real revision, so a cancellation before the Loading lands clears nothing.
var loadingRevision = -1L
try {
updateAuthState(AuthState.Loading(config.stringProvider.loadingVerifyingPhoneNumber))
loadingRevision = currentAuthStateRevision()
provider.verifyPhoneNumberFlow(
auth = auth,
activity = activity,
Expand All @@ -148,9 +145,8 @@ internal suspend fun FirebaseAuthUI.verifyPhoneNumber(
}
}
} catch (e: CancellationException) {
// Cancellation here is the screen's own bookkeeping, not a failure: retract only the
// Loading this call emitted, then rethrow so no spurious Error reaches authStateFlow.
clearLoadingState(loadingRevision)
// Writes nothing: the caller cancelling this attempt owns whatever state replaces it, and
// a retraction from here would race the replacement's own Loading.
throw e
} catch (e: AuthException) {
updateAuthState(AuthState.Error(e))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,9 +410,6 @@ interface AuthUIStringProvider {
/** Action text for choosing a different factor during MFA challenge. */
val useDifferentMethodAction: String

/** Action text for confirming recovery codes have been saved. */
val recoveryCodesSavedAction: String

/** Label for secret key text displayed during TOTP setup. */
val secretKeyLabel: String

Expand Down Expand Up @@ -511,9 +508,6 @@ interface AuthUIStringProvider {
/** Title for MFA verification step */
val mfaStepVerifyFactorTitle: String

/** Title for recovery codes step */
val mfaStepShowRecoveryCodesTitle: String

// MFA Enrollment Helper Text
/** Helper text for selecting MFA factor */
val mfaStepSelectFactorHelper: String
Expand All @@ -533,9 +527,6 @@ interface AuthUIStringProvider {
/** Generic helper text for factor verification */
val mfaStepVerifyFactorGenericHelper: String

/** 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,9 +373,6 @@ class DefaultAuthUIStringProvider(
override val useDifferentMethodAction: String
get() = localizedContext.getString(R.string.fui_use_different_method_action)

override val recoveryCodesSavedAction: String
get() = localizedContext.getString(R.string.fui_recovery_codes_saved_action)

override val secretKeyLabel: String
get() = localizedContext.getString(R.string.fui_secret_key_label)

Expand Down Expand Up @@ -462,8 +459,6 @@ class DefaultAuthUIStringProvider(
get() = localizedContext.getString(R.string.fui_mfa_step_configure_totp_title)
override val mfaStepVerifyFactorTitle: String
get() = localizedContext.getString(R.string.fui_mfa_step_verify_factor_title)
override val mfaStepShowRecoveryCodesTitle: String
get() = localizedContext.getString(R.string.fui_mfa_step_show_recovery_codes_title)

/**
* MFA Enrollment Helper Text
Expand All @@ -480,8 +475,6 @@ class DefaultAuthUIStringProvider(
get() = localizedContext.getString(R.string.fui_mfa_step_verify_factor_totp_helper)
override val mfaStepVerifyFactorGenericHelper: String
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,6 @@ import com.google.firebase.auth.MultiFactorInfo
* @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.configuration.MfaConfiguration.enableRecoveryCodes] is `true`.
* @property onCodesSavedClick (Step: [MfaEnrollmentStep.ShowRecoveryCodes]) Callback invoked when the user confirms they have saved their recovery codes. Completes the enrollment flow.
*
* @since 10.0.0
*/
data class MfaEnrollmentContentState(
Expand Down Expand Up @@ -131,12 +128,7 @@ data class MfaEnrollmentContentState(

val resendTimer: Int = 0,

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

// ShowRecoveryCodes step
val recoveryCodes: List<String>? = null,

val onCodesSavedClick: () -> Unit = {}
val onResendCodeClick: (() -> Unit)? = null
) {
/**
* Returns true if the current state is valid for the current step.
Expand All @@ -149,7 +141,6 @@ data class MfaEnrollmentContentState(
MfaEnrollmentStep.ConfigureSms -> phoneNumber.isNotBlank()
MfaEnrollmentStep.ConfigureTotp -> totpSecret != null && totpQrCodeUrl != null
MfaEnrollmentStep.VerifyFactor -> verificationCode.length == 6
MfaEnrollmentStep.ShowRecoveryCodes -> !recoveryCodes.isNullOrEmpty()
}

/**
Expand Down
13 changes: 2 additions & 11 deletions auth/src/main/java/com/firebase/ui/auth/mfa/MfaEnrollmentStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider
* Represents the different steps in the Multi-Factor Authentication (MFA) enrollment flow.
*
* This enum defines the sequence of UI states that users progress through when enrolling
* in MFA, from selecting a factor to completing the setup with recovery codes.
* in MFA, from selecting a factor to verifying it.
*
* @since 10.0.0
*/
Expand Down Expand Up @@ -50,14 +50,7 @@ enum class MfaEnrollmentStep {
* For SMS, this is the code received via text message.
* For TOTP, this is the code generated by their authenticator app.
*/
VerifyFactor,

/**
* The enrollment is complete and recovery codes are displayed to the user.
* These backup codes can be used to sign in if the primary MFA method is unavailable.
* This step only appears if recovery codes are enabled in the configuration.
*/
ShowRecoveryCodes
VerifyFactor
}

/**
Expand All @@ -71,7 +64,6 @@ fun MfaEnrollmentStep.getTitle(stringProvider: AuthUIStringProvider): String = w
MfaEnrollmentStep.ConfigureSms -> stringProvider.mfaStepConfigureSmsTitle
MfaEnrollmentStep.ConfigureTotp -> stringProvider.mfaStepConfigureTotpTitle
MfaEnrollmentStep.VerifyFactor -> stringProvider.mfaStepVerifyFactorTitle
MfaEnrollmentStep.ShowRecoveryCodes -> stringProvider.mfaStepShowRecoveryCodesTitle
}

/**
Expand All @@ -94,5 +86,4 @@ fun MfaEnrollmentStep.getHelperText(
MfaFactor.Totp -> stringProvider.mfaStepVerifyFactorTotpHelper
null -> stringProvider.mfaStepVerifyFactorGenericHelper
}
MfaEnrollmentStep.ShowRecoveryCodes -> stringProvider.mfaStepShowRecoveryCodesHelper
}
Original file line number Diff line number Diff line change
Expand Up @@ -208,16 +208,6 @@ internal fun DefaultMfaEnrollmentContent(
null -> Unit
}
}

MfaEnrollmentStep.ShowRecoveryCodes -> {
ShowRecoveryCodesUI(
recoveryCodes = state.recoveryCodes.orEmpty(),
onDoneClick = state.onCodesSavedClick,
isLoading = state.isLoading,
error = state.error,
stringProvider = stringProvider
)
}
}

SnackbarHost(
Expand Down Expand Up @@ -571,70 +561,3 @@ private fun VerifyTotpUI(
}
}
}

@Composable
private fun ShowRecoveryCodesUI(
recoveryCodes: List<String>,
onDoneClick: () -> Unit,
isLoading: Boolean,
error: String?,
stringProvider: AuthUIStringProvider
) {
Scaffold { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = stringProvider.mfaStepShowRecoveryCodesTitle,
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center
)

Text(
text = stringProvider.mfaStepShowRecoveryCodesHelper,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.error
)

error?.let {
Text(
text = it,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center
)
}

Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
recoveryCodes.forEach { code ->
Text(
text = code,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
}
}

Button(
onClick = onDoneClick,
enabled = !isLoading,
modifier = Modifier.fillMaxWidth()
) {
Text(stringProvider.recoveryCodesSavedAction)
}
}
}
}
Loading
Loading