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
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ import com.firebase.ui.auth.ui.screens.email.isEmailLinkSignInOffered
import com.firebase.ui.auth.ui.screens.email.isEmailSignUpOffered
import com.firebase.ui.auth.ui.screens.email.navigateToEmailStep
import com.firebase.ui.auth.ui.screens.mfa.MfaChallengeScreen
import com.firebase.ui.auth.ui.screens.mfa.enterMfaEnrollment
import com.firebase.ui.auth.ui.screens.mfa.exitMfaEnrollment
import com.firebase.ui.auth.ui.screens.mfa.mfaEnrollmentDestinations
import com.firebase.ui.auth.ui.screens.mfa.mfaEnrollmentStartStep
import com.firebase.ui.auth.ui.screens.mfa.rememberMfaEnrollmentFlowState
import com.firebase.ui.auth.ui.screens.phone.PhoneAuthContentState
import com.firebase.ui.auth.ui.screens.phone.PhoneAuthScreen
Expand Down Expand Up @@ -410,8 +410,9 @@ fun FirebaseAuthScreen(
// Inert while armed: this content stays composed beneath the slot.
if (reauthState == null) {
if (configuration.isMfaEnabled) {
navController.navigate(
mfaEnrollmentStartStep(mfaConfiguration).route
navController.enterMfaEnrollment(
mfaConfiguration,
mfaEnrollmentFlowState,
)
} else {
val exception = AuthException.AuthCancelledException(
Expand Down Expand Up @@ -453,10 +454,16 @@ fun FirebaseAuthScreen(
onNavigate = { route ->
// Inert while armed: this content stays composed beneath the slot.
if (reauthState == null) {
// MfaEnrollment.route names SelectFactor; one factor skips it.
if (route == AuthRoute.MfaEnrollment) {
navController.navigate(
mfaEnrollmentStartStep(mfaConfiguration).route
// Naming the flow resolves a start step; naming one of its
// steps lands there. Either way the flow state is cleared,
// and MfaEnrollment.route alone cannot tell the two apart.
if (route == AuthRoute.MfaEnrollment ||
route is AuthRoute.MfaEnrollment.Step
) {
navController.enterMfaEnrollment(
mfaConfiguration,
mfaEnrollmentFlowState,
step = route as? AuthRoute.MfaEnrollment.Step,
)
} else {
navController.navigate(route.route)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,28 @@ class MfaEnrollmentFlowState internal constructor(
val totpQrCodeUrl: MutableState<String?>,
val selectedCountry: MutableState<CountryData>,
val totpSecretExpiredMessage: MutableState<String?>,
)
) {
/**
* Returns every field to the value [rememberMfaEnrollmentFlowState] starts it at.
*
* Called on flow *entry* — [enterMfaEnrollment] — not on completion: a flow that has been
* left is still composed for the length of the exit transition, and clearing underneath it
* leaves a rendered step whose controls act on state that is already gone.
*
* @since 10.0.0
*/
fun reset() {
selectedFactor.value = null
phoneNumber.value = ""
verificationCode.value = ""
resendTimerSeconds.intValue = 0
smsSession.value = null
totpSecret.value = null
totpQrCodeUrl.value = null
selectedCountry.value = CountryUtils.getDefaultCountry()
totpSecretExpiredMessage.value = null
}
}

/**
* Creates and remembers the [MfaEnrollmentFlowState] a host installs [mfaEnrollmentDestinations]
Expand Down Expand Up @@ -167,6 +188,36 @@ internal fun NavHostController.navigateToMfaStep(step: AuthRoute.MfaEnrollment.S
navigate(step.route)
}

/**
* Enters the enrolment flow on a cleared [flowState], landing on [step] or, without one, on
* [mfaEnrollmentStartStep].
*
* The clear happens here rather than when the flow completes because [flowState] outlives the
* flow — six of its fields are `rememberSaveable`, so an enrolment's phone number and verification
* code otherwise survive both the exit and process death, and the next entry lands on a filled-in
* form whose Verify button re-submits a code Firebase has already consumed. Clearing on the way
* out instead would mutate state a leaving step still reads while it is composed for the exit
* transition; on the way in there is no such step.
*
* Both of the host's flow entry points go through here: `AuthSuccessUiContext.onManageMfa`, and
* `onNavigate` given [AuthRoute.MfaEnrollment] or any one of its steps.
*
* @param step Where a caller that named a step rather than the whole flow asked to land; null
* means it named the flow, which resolves through [mfaEnrollmentStartStep]. Entry takes the step
* as a parameter rather than leaving that caller to navigate for itself, because
* [AuthRoute.MfaEnrollment] and [AuthRoute.MfaEnrollment.SelectFactor] report the same
* [AuthRoute.route]: a caller doing its own `navigate` cannot honour a named step without also
* skipping the clear.
*/
internal fun NavHostController.enterMfaEnrollment(
configuration: MfaConfiguration,
flowState: MfaEnrollmentFlowState,
step: AuthRoute.MfaEnrollment.Step? = null,
) {
flowState.reset()
navigate((step ?: mfaEnrollmentStartStep(configuration)).route)
}

/**
* Leaves the MFA enrolment flow, popping a step at a time until the top of the back stack is not
* one.
Expand Down
Loading