mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 05:31:21 +03:00
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
10 changed files with 106 additions and 16 deletions
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 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()
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
@ -412,8 +415,8 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
authenticationService.cancelPendingLoginOrRegistration()
|
authenticationService.cancelPendingLoginOrRegistration()
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
registrationState = RegistrationState(),
|
registrationState = RegistrationState(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,17 +413,19 @@ 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(
|
|
||||||
FtueAuthLegacyWaitForEmailFragment::class.java,
|
|
||||||
FtueAuthWaitForEmailFragmentArgument(email),
|
|
||||||
)
|
)
|
||||||
|
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
|
@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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue