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:
Adam Brown 2022-07-27 14:00:20 +01:00 committed by GitHub
commit 0fcf7c7079
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 106 additions and 16 deletions

1
changelog.d/6622.feature Normal file
View file

@ -0,0 +1 @@
FTUE - Allows the email address to be changed during the verification process

View file

@ -53,7 +53,7 @@ sealed class OnboardingViewEvents : VectorViewEvents {
object OnResetPasswordBreakerConfirmed : OnboardingViewEvents() object OnResetPasswordBreakerConfirmed : OnboardingViewEvents()
object OnResetPasswordComplete : 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 OnSendMsisdnSuccess(val msisdn: String) : OnboardingViewEvents()
data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : OnboardingViewEvents() data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : OnboardingViewEvents()

View file

@ -348,7 +348,10 @@ class OnboardingViewModel @AssistedInject constructor(
overrideNextStage?.invoke() ?: _viewEvents.post(OnboardingViewEvents.DisplayStartRegistration) overrideNextStage?.invoke() ?: _viewEvents.post(OnboardingViewEvents.DisplayStartRegistration)
} }
RegistrationActionHandler.Result.UnsupportedStage -> _viewEvents.post(OnboardingViewEvents.DisplayRegistrationFallback) 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.SendMsisdnSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendMsisdnSuccess(it.msisdn.msisdn))
is RegistrationActionHandler.Result.Error -> _viewEvents.post(OnboardingViewEvents.Failure(it.cause)) is RegistrationActionHandler.Result.Error -> _viewEvents.post(OnboardingViewEvents.Failure(it.cause))
RegistrationActionHandler.Result.MissingNextStage -> { RegistrationActionHandler.Result.MissingNextStage -> {
@ -486,7 +489,7 @@ class OnboardingViewModel @AssistedInject constructor(
try { try {
if (registrationWizard.isRegistrationStarted()) { if (registrationWizard.isRegistrationStarted()) {
currentThreePid?.let { currentThreePid?.let {
handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(it))) handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(it, isRestoredSession = true)))
} }
} }
} catch (e: Throwable) { } catch (e: Throwable) {

View file

@ -101,6 +101,7 @@ data class SelectedAuthenticationState(
@Parcelize @Parcelize
data class RegistrationState( data class RegistrationState(
val email: String? = null,
val isUserNameAvailable: Boolean = false, val isUserNameAvailable: Boolean = false,
val selectedMatrixId: String? = null, val selectedMatrixId: String? = null,
) : Parcelable ) : Parcelable

View file

@ -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 // Due to async, we keep a boolean to avoid displaying twice the cancellation dialog
private var displayCancelDialog = true private var displayCancelDialog = true
protected open fun backIsHardExit() = true
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -115,7 +116,7 @@ abstract class AbstractFtueAuthFragment<VB : ViewBinding> : VectorBaseFragment<V
override fun onBackPressed(toolbarButton: Boolean): Boolean { override fun onBackPressed(toolbarButton: Boolean): Boolean {
return when { return when {
displayCancelDialog && viewModel.isRegistrationStarted -> { displayCancelDialog && viewModel.isRegistrationStarted && backIsHardExit() -> {
// Ask for confirmation before cancelling the registration // Ask for confirmation before cancelling the registration
MaterialAlertDialogBuilder(requireActivity()) MaterialAlertDialogBuilder(requireActivity())
.setTitle(R.string.login_signup_cancel_confirmation_title) .setTitle(R.string.login_signup_cancel_confirmation_title)

View file

@ -25,6 +25,8 @@ import im.vector.app.core.extensions.associateContentStateWith
import im.vector.app.core.extensions.autofillEmail import im.vector.app.core.extensions.autofillEmail
import im.vector.app.core.extensions.clearErrorOnChange import im.vector.app.core.extensions.clearErrorOnChange
import im.vector.app.core.extensions.content 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.isEmail
import im.vector.app.core.extensions.setOnImeDoneListener import im.vector.app.core.extensions.setOnImeDoneListener
import im.vector.app.core.extensions.toReducedUrl import im.vector.app.core.extensions.toReducedUrl
@ -61,6 +63,10 @@ class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragmen
override fun updateWithState(state: OnboardingViewState) { override fun updateWithState(state: OnboardingViewState) {
views.emailEntryHeaderSubtitle.text = getString(R.string.ftue_auth_email_subtitle, state.selectedHomeserver.userFacingUrl.toReducedUrl()) 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) { override fun onError(throwable: Throwable) {

View file

@ -196,7 +196,7 @@ class FtueAuthVariant(
activity.popBackstack() activity.popBackstack()
} }
is OnboardingViewEvents.OnSendEmailSuccess -> { is OnboardingViewEvents.OnSendEmailSuccess -> {
openWaitForEmailVerification(viewEvents.email) openWaitForEmailVerification(viewEvents.email, viewEvents.isRestoredSession)
} }
is OnboardingViewEvents.OnSendMsisdnSuccess -> { is OnboardingViewEvents.OnSendMsisdnSuccess -> {
openMsisdnConfirmation(viewEvents.msisdn) openMsisdnConfirmation(viewEvents.msisdn)
@ -413,19 +413,21 @@ class FtueAuthVariant(
} }
} }
private fun openWaitForEmailVerification(email: String) { private fun openWaitForEmailVerification(email: String, isRestoredSession: Boolean) {
supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
when { when {
vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack( vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack(
FtueAuthWaitForEmailFragment::class.java, FtueAuthWaitForEmailFragment::class.java,
FtueAuthWaitForEmailFragmentArgument(email), FtueAuthWaitForEmailFragmentArgument(email, isRestoredSession),
) )
else -> addRegistrationStageFragmentToBackstack( else -> {
supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
addRegistrationStageFragmentToBackstack(
FtueAuthLegacyWaitForEmailFragment::class.java, FtueAuthLegacyWaitForEmailFragment::class.java,
FtueAuthWaitForEmailFragmentArgument(email), FtueAuthWaitForEmailFragmentArgument(email, isRestoredSession),
) )
} }
} }
}
private fun openMsisdnConfirmation(msisdn: String) { private fun openMsisdnConfirmation(msisdn: String) {
supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)

View file

@ -35,7 +35,8 @@ import javax.inject.Inject
@Parcelize @Parcelize
data class FtueAuthWaitForEmailFragmentArgument( data class FtueAuthWaitForEmailFragmentArgument(
val email: String val email: String,
val isRestoredSession: Boolean,
) : Parcelable ) : Parcelable
/** /**
@ -48,6 +49,8 @@ class FtueAuthWaitForEmailFragment @Inject constructor(
private val params: FtueAuthWaitForEmailFragmentArgument by args() private val params: FtueAuthWaitForEmailFragmentArgument by args()
private var inferHasLeftAndReturnedToScreen = false private var inferHasLeftAndReturnedToScreen = false
override fun backIsHardExit() = params.isRestoredSession
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueWaitForEmailVerificationBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueWaitForEmailVerificationBinding {
return FragmentFtueWaitForEmailVerificationBinding.inflate(inflater, container, false) return FragmentFtueWaitForEmailVerificationBinding.inflate(inflater, container, false)
} }
@ -97,6 +100,11 @@ class FtueAuthWaitForEmailFragment @Inject constructor(
} }
override fun resetViewModel() { override fun resetViewModel() {
viewModel.handle(OnboardingAction.ResetAuthenticationAttempt) when {
backIsHardExit() -> viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
else -> {
// delegate to the previous step
}
}
} }
} }

View file

@ -102,6 +102,48 @@ class OnboardingViewModelTest {
viewModelWith(initialState) 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 @Test
fun `when handling PostViewEvent, then emits contents as view event`() = runTest { fun `when handling PostViewEvent, then emits contents as view event`() = runTest {
val test = viewModel.test() val test = viewModel.test()
@ -254,6 +296,24 @@ class OnboardingViewModelTest {
.finish() .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 @Test
fun `given unavailable deeplink, when selecting homeserver, then emits failure with default homeserver as retry action`() = runTest { fun `given unavailable deeplink, when selecting homeserver, then emits failure with default homeserver as retry action`() = runTest {
fakeContext.givenHasConnection() fakeContext.givenHasConnection()

View file

@ -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) { fun givenUserNameIsAvailable(userName: String) {
coEvery { registrationAvailable(userName) } returns RegistrationAvailability.Available coEvery { registrationAvailable(userName) } returns RegistrationAvailability.Available
} }