Merge pull request #6641 from vector-im/feature/adm/ftue-soft-exit-email-verification
FTUE - Allow editing email during email verification
This commit is contained in:
		
						commit
						0fcf7c7079
					
				
							
								
								
									
										1
									
								
								changelog.d/6622.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								changelog.d/6622.feature
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
FTUE - Allows the email address to be changed during the verification process
 | 
			
		||||
@ -53,7 +53,7 @@ sealed class OnboardingViewEvents : VectorViewEvents {
 | 
			
		||||
    object OnResetPasswordBreakerConfirmed : OnboardingViewEvents()
 | 
			
		||||
    object OnResetPasswordComplete : OnboardingViewEvents()
 | 
			
		||||
 | 
			
		||||
    data class OnSendEmailSuccess(val email: String) : OnboardingViewEvents()
 | 
			
		||||
    data class OnSendEmailSuccess(val email: String, val isRestoredSession: Boolean) : OnboardingViewEvents()
 | 
			
		||||
    data class OnSendMsisdnSuccess(val msisdn: String) : OnboardingViewEvents()
 | 
			
		||||
 | 
			
		||||
    data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : OnboardingViewEvents()
 | 
			
		||||
 | 
			
		||||
@ -348,7 +348,10 @@ class OnboardingViewModel @AssistedInject constructor(
 | 
			
		||||
                            overrideNextStage?.invoke() ?: _viewEvents.post(OnboardingViewEvents.DisplayStartRegistration)
 | 
			
		||||
                        }
 | 
			
		||||
                        RegistrationActionHandler.Result.UnsupportedStage -> _viewEvents.post(OnboardingViewEvents.DisplayRegistrationFallback)
 | 
			
		||||
                        is RegistrationActionHandler.Result.SendEmailSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email))
 | 
			
		||||
                        is RegistrationActionHandler.Result.SendEmailSuccess -> {
 | 
			
		||||
                            _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email, isRestoredSession = false))
 | 
			
		||||
                            setState { copy(registrationState = registrationState.copy(email = it.email)) }
 | 
			
		||||
                        }
 | 
			
		||||
                        is RegistrationActionHandler.Result.SendMsisdnSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendMsisdnSuccess(it.msisdn.msisdn))
 | 
			
		||||
                        is RegistrationActionHandler.Result.Error -> _viewEvents.post(OnboardingViewEvents.Failure(it.cause))
 | 
			
		||||
                        RegistrationActionHandler.Result.MissingNextStage -> {
 | 
			
		||||
@ -412,8 +415,8 @@ class OnboardingViewModel @AssistedInject constructor(
 | 
			
		||||
                    authenticationService.cancelPendingLoginOrRegistration()
 | 
			
		||||
                    setState {
 | 
			
		||||
                        copy(
 | 
			
		||||
                            isLoading = false,
 | 
			
		||||
                            registrationState = RegistrationState(),
 | 
			
		||||
                                isLoading = false,
 | 
			
		||||
                                registrationState = RegistrationState(),
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
@ -486,7 +489,7 @@ class OnboardingViewModel @AssistedInject constructor(
 | 
			
		||||
        try {
 | 
			
		||||
            if (registrationWizard.isRegistrationStarted()) {
 | 
			
		||||
                currentThreePid?.let {
 | 
			
		||||
                    handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(it)))
 | 
			
		||||
                    handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(it, isRestoredSession = true)))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e: Throwable) {
 | 
			
		||||
 | 
			
		||||
@ -101,6 +101,7 @@ data class SelectedAuthenticationState(
 | 
			
		||||
 | 
			
		||||
@Parcelize
 | 
			
		||||
data class RegistrationState(
 | 
			
		||||
        val email: String? = null,
 | 
			
		||||
        val isUserNameAvailable: Boolean = false,
 | 
			
		||||
        val selectedMatrixId: String? = null,
 | 
			
		||||
) : Parcelable
 | 
			
		||||
 | 
			
		||||
@ -46,6 +46,7 @@ abstract class AbstractFtueAuthFragment<VB : ViewBinding> : VectorBaseFragment<V
 | 
			
		||||
 | 
			
		||||
    // Due to async, we keep a boolean to avoid displaying twice the cancellation dialog
 | 
			
		||||
    private var displayCancelDialog = true
 | 
			
		||||
    protected open fun backIsHardExit() = true
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
@ -115,7 +116,7 @@ abstract class AbstractFtueAuthFragment<VB : ViewBinding> : VectorBaseFragment<V
 | 
			
		||||
 | 
			
		||||
    override fun onBackPressed(toolbarButton: Boolean): Boolean {
 | 
			
		||||
        return when {
 | 
			
		||||
            displayCancelDialog && viewModel.isRegistrationStarted -> {
 | 
			
		||||
            displayCancelDialog && viewModel.isRegistrationStarted && backIsHardExit() -> {
 | 
			
		||||
                // Ask for confirmation before cancelling the registration
 | 
			
		||||
                MaterialAlertDialogBuilder(requireActivity())
 | 
			
		||||
                        .setTitle(R.string.login_signup_cancel_confirmation_title)
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,8 @@ import im.vector.app.core.extensions.associateContentStateWith
 | 
			
		||||
import im.vector.app.core.extensions.autofillEmail
 | 
			
		||||
import im.vector.app.core.extensions.clearErrorOnChange
 | 
			
		||||
import im.vector.app.core.extensions.content
 | 
			
		||||
import im.vector.app.core.extensions.editText
 | 
			
		||||
import im.vector.app.core.extensions.hasContent
 | 
			
		||||
import im.vector.app.core.extensions.isEmail
 | 
			
		||||
import im.vector.app.core.extensions.setOnImeDoneListener
 | 
			
		||||
import im.vector.app.core.extensions.toReducedUrl
 | 
			
		||||
@ -61,6 +63,10 @@ class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragmen
 | 
			
		||||
 | 
			
		||||
    override fun updateWithState(state: OnboardingViewState) {
 | 
			
		||||
        views.emailEntryHeaderSubtitle.text = getString(R.string.ftue_auth_email_subtitle, state.selectedHomeserver.userFacingUrl.toReducedUrl())
 | 
			
		||||
 | 
			
		||||
        if (!views.emailEntryInput.hasContent()) {
 | 
			
		||||
            views.emailEntryInput.editText().setText(state.registrationState.email)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onError(throwable: Throwable) {
 | 
			
		||||
 | 
			
		||||
@ -196,7 +196,7 @@ class FtueAuthVariant(
 | 
			
		||||
                activity.popBackstack()
 | 
			
		||||
            }
 | 
			
		||||
            is OnboardingViewEvents.OnSendEmailSuccess -> {
 | 
			
		||||
                openWaitForEmailVerification(viewEvents.email)
 | 
			
		||||
                openWaitForEmailVerification(viewEvents.email, viewEvents.isRestoredSession)
 | 
			
		||||
            }
 | 
			
		||||
            is OnboardingViewEvents.OnSendMsisdnSuccess -> {
 | 
			
		||||
                openMsisdnConfirmation(viewEvents.msisdn)
 | 
			
		||||
@ -413,17 +413,19 @@ class FtueAuthVariant(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun openWaitForEmailVerification(email: String) {
 | 
			
		||||
        supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
 | 
			
		||||
    private fun openWaitForEmailVerification(email: String, isRestoredSession: Boolean) {
 | 
			
		||||
        when {
 | 
			
		||||
            vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack(
 | 
			
		||||
                    FtueAuthWaitForEmailFragment::class.java,
 | 
			
		||||
                    FtueAuthWaitForEmailFragmentArgument(email),
 | 
			
		||||
            )
 | 
			
		||||
            else -> addRegistrationStageFragmentToBackstack(
 | 
			
		||||
                    FtueAuthLegacyWaitForEmailFragment::class.java,
 | 
			
		||||
                    FtueAuthWaitForEmailFragmentArgument(email),
 | 
			
		||||
                    FtueAuthWaitForEmailFragmentArgument(email, isRestoredSession),
 | 
			
		||||
            )
 | 
			
		||||
            else -> {
 | 
			
		||||
                supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
 | 
			
		||||
                addRegistrationStageFragmentToBackstack(
 | 
			
		||||
                        FtueAuthLegacyWaitForEmailFragment::class.java,
 | 
			
		||||
                        FtueAuthWaitForEmailFragmentArgument(email, isRestoredSession),
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -35,7 +35,8 @@ import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
@Parcelize
 | 
			
		||||
data class FtueAuthWaitForEmailFragmentArgument(
 | 
			
		||||
        val email: String
 | 
			
		||||
        val email: String,
 | 
			
		||||
        val isRestoredSession: Boolean,
 | 
			
		||||
) : Parcelable
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -48,6 +49,8 @@ class FtueAuthWaitForEmailFragment @Inject constructor(
 | 
			
		||||
    private val params: FtueAuthWaitForEmailFragmentArgument by args()
 | 
			
		||||
    private var inferHasLeftAndReturnedToScreen = false
 | 
			
		||||
 | 
			
		||||
    override fun backIsHardExit() = params.isRestoredSession
 | 
			
		||||
 | 
			
		||||
    override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueWaitForEmailVerificationBinding {
 | 
			
		||||
        return FragmentFtueWaitForEmailVerificationBinding.inflate(inflater, container, false)
 | 
			
		||||
    }
 | 
			
		||||
@ -97,6 +100,11 @@ class FtueAuthWaitForEmailFragment @Inject constructor(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun resetViewModel() {
 | 
			
		||||
        viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
 | 
			
		||||
        when {
 | 
			
		||||
            backIsHardExit() -> viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
 | 
			
		||||
            else -> {
 | 
			
		||||
                // delegate to the previous step
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -102,6 +102,48 @@ class OnboardingViewModelTest {
 | 
			
		||||
        viewModelWith(initialState)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun `given registration started with currentThreePid, when handling InitWith, then emits restored session OnSendEmailSuccess`() = runTest {
 | 
			
		||||
        val test = viewModel.test()
 | 
			
		||||
        fakeAuthenticationService.givenRegistrationWizard(FakeRegistrationWizard().also {
 | 
			
		||||
            it.givenRegistrationStarted(hasStarted = true)
 | 
			
		||||
            it.givenCurrentThreePid(AN_EMAIL)
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        viewModel.handle(OnboardingAction.InitWith(LoginConfig(A_HOMESERVER_URL, identityServerUrl = null)))
 | 
			
		||||
 | 
			
		||||
        test
 | 
			
		||||
                .assertEvents(OnboardingViewEvents.OnSendEmailSuccess(AN_EMAIL, isRestoredSession = true))
 | 
			
		||||
                .finish()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun `given registration not started, when handling InitWith, then does nothing`() = runTest {
 | 
			
		||||
        val test = viewModel.test()
 | 
			
		||||
        fakeAuthenticationService.givenRegistrationWizard(FakeRegistrationWizard().also { it.givenRegistrationStarted(hasStarted = false) })
 | 
			
		||||
 | 
			
		||||
        viewModel.handle(OnboardingAction.InitWith(LoginConfig(A_HOMESERVER_URL, identityServerUrl = null)))
 | 
			
		||||
 | 
			
		||||
        test
 | 
			
		||||
                .assertNoEvents()
 | 
			
		||||
                .finish()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun `given registration started without currentThreePid, when handling InitWith, then does nothing`() = runTest {
 | 
			
		||||
        val test = viewModel.test()
 | 
			
		||||
        fakeAuthenticationService.givenRegistrationWizard(FakeRegistrationWizard().also {
 | 
			
		||||
            it.givenRegistrationStarted(hasStarted = true)
 | 
			
		||||
            it.givenCurrentThreePid(threePid = null)
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        viewModel.handle(OnboardingAction.InitWith(LoginConfig(A_HOMESERVER_URL, identityServerUrl = null)))
 | 
			
		||||
 | 
			
		||||
        test
 | 
			
		||||
                .assertNoEvents()
 | 
			
		||||
                .finish()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun `when handling PostViewEvent, then emits contents as view event`() = runTest {
 | 
			
		||||
        val test = viewModel.test()
 | 
			
		||||
@ -254,6 +296,24 @@ class OnboardingViewModelTest {
 | 
			
		||||
                .finish()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun `given register action returns email success, when handling action, then updates registration state and emits email success`() = runTest {
 | 
			
		||||
        val test = viewModel.test()
 | 
			
		||||
        givenRegistrationResultFor(A_LOADABLE_REGISTER_ACTION, RegistrationActionHandler.Result.SendEmailSuccess(AN_EMAIL))
 | 
			
		||||
 | 
			
		||||
        viewModel.handle(OnboardingAction.PostRegisterAction(A_LOADABLE_REGISTER_ACTION))
 | 
			
		||||
 | 
			
		||||
        test
 | 
			
		||||
                .assertStatesChanges(
 | 
			
		||||
                        initialState,
 | 
			
		||||
                        { copy(isLoading = true) },
 | 
			
		||||
                        { copy(registrationState = RegistrationState(email = AN_EMAIL)) },
 | 
			
		||||
                        { copy(isLoading = false) }
 | 
			
		||||
                )
 | 
			
		||||
                .assertEvents(OnboardingViewEvents.OnSendEmailSuccess(AN_EMAIL, isRestoredSession = false))
 | 
			
		||||
                .finish()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun `given unavailable deeplink, when selecting homeserver, then emits failure with default homeserver as retry action`() = runTest {
 | 
			
		||||
        fakeContext.givenHasConnection()
 | 
			
		||||
 | 
			
		||||
@ -45,6 +45,14 @@ class FakeRegistrationWizard : RegistrationWizard by mockk(relaxed = false) {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun givenRegistrationStarted(hasStarted: Boolean) {
 | 
			
		||||
        coEvery { isRegistrationStarted() } returns hasStarted
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun givenCurrentThreePid(threePid: String?) {
 | 
			
		||||
        coEvery { getCurrentThreePid() } returns threePid
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun givenUserNameIsAvailable(userName: String) {
 | 
			
		||||
        coEvery { registrationAvailable(userName) } returns RegistrationAvailability.Available
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user