replacing async login/register state with separate failure view event and shared isLoading

This commit is contained in:
Adam Brown 2022-03-08 11:41:32 +00:00
parent e3df9c4cef
commit 2227df479c
4 changed files with 67 additions and 129 deletions

View File

@ -239,31 +239,19 @@ class OnboardingViewModel @AssistedInject constructor(
val safeLoginWizard = loginWizard val safeLoginWizard = loginWizard
if (safeLoginWizard == null) { if (safeLoginWizard == null) {
setState { setState { copy(isLoading = false) }
copy( _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration")))
asyncLoginAction = Fail(Throwable("Bad configuration"))
)
}
} else { } else {
setState { setState { copy(isLoading = true) }
copy(
asyncLoginAction = Loading()
)
}
currentJob = viewModelScope.launch { currentJob = viewModelScope.launch {
try { try {
safeLoginWizard.loginWithToken(action.loginToken) val result = safeLoginWizard.loginWithToken(action.loginToken)
onSessionCreated(result, isAccountCreated = false)
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(OnboardingViewEvents.Failure(failure)) _viewEvents.post(OnboardingViewEvents.Failure(failure))
setState { setState { copy(isLoading = false) }
copy(
asyncLoginAction = Fail(failure)
)
}
null
} }
?.let { onSessionCreated(it, isAccountCreated = false) }
} }
} }
} }
@ -271,7 +259,7 @@ class OnboardingViewModel @AssistedInject constructor(
private fun handleRegisterAction(action: RegisterAction) { private fun handleRegisterAction(action: RegisterAction) {
currentJob = viewModelScope.launch { currentJob = viewModelScope.launch {
if (action.hasLoadingState()) { if (action.hasLoadingState()) {
setState { copy(asyncRegistration = Loading()) } setState { copy(isLoading = true) }
} }
runCatching { registrationActionHandler.handleRegisterAction(registrationWizard, action) } runCatching { registrationActionHandler.handleRegisterAction(registrationWizard, action) }
.fold( .fold(
@ -292,7 +280,7 @@ class OnboardingViewModel @AssistedInject constructor(
} }
} }
) )
setState { copy(asyncRegistration = Uninitialized) } setState { copy(isLoading = false) }
} }
} }
@ -345,12 +333,7 @@ class OnboardingViewModel @AssistedInject constructor(
OnboardingAction.ResetLogin -> { OnboardingAction.ResetLogin -> {
viewModelScope.launch { viewModelScope.launch {
authenticationService.cancelPendingLoginOrRegistration() authenticationService.cancelPendingLoginOrRegistration()
setState { setState { copy(isLoading = false) }
copy(
asyncLoginAction = Uninitialized,
asyncRegistration = Uninitialized
)
}
} }
} }
OnboardingAction.ResetResetPassword -> { OnboardingAction.ResetResetPassword -> {
@ -515,11 +498,7 @@ class OnboardingViewModel @AssistedInject constructor(
} }
private fun handleDirectLogin(action: OnboardingAction.LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?) { private fun handleDirectLogin(action: OnboardingAction.LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?) {
setState { setState { copy(isLoading = true) }
copy(
asyncLoginAction = Loading()
)
}
currentJob = viewModelScope.launch { currentJob = viewModelScope.launch {
val data = try { val data = try {
@ -546,11 +525,7 @@ class OnboardingViewModel @AssistedInject constructor(
} }
private fun onWellKnownError() { private fun onWellKnownError() {
setState { setState { copy(isLoading = false) }
copy(
asyncLoginAction = Uninitialized
)
}
_viewEvents.post(OnboardingViewEvents.Failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error)))) _viewEvents.post(OnboardingViewEvents.Failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error))))
} }
@ -587,18 +562,10 @@ class OnboardingViewModel @AssistedInject constructor(
is Failure.UnrecognizedCertificateFailure -> { is Failure.UnrecognizedCertificateFailure -> {
// Display this error in a dialog // Display this error in a dialog
_viewEvents.post(OnboardingViewEvents.Failure(failure)) _viewEvents.post(OnboardingViewEvents.Failure(failure))
setState { setState { copy(isLoading = false) }
copy(
asyncLoginAction = Uninitialized
)
}
} }
else -> { else -> {
setState { setState { copy(isLoading = false) }
copy(
asyncLoginAction = Fail(failure)
)
}
} }
} }
} }
@ -607,37 +574,22 @@ class OnboardingViewModel @AssistedInject constructor(
val safeLoginWizard = loginWizard val safeLoginWizard = loginWizard
if (safeLoginWizard == null) { if (safeLoginWizard == null) {
setState { setState { copy(isLoading = false) }
copy( _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration")))
asyncLoginAction = Fail(Throwable("Bad configuration"))
)
}
} else { } else {
setState { setState { copy(isLoading = true) }
copy(
asyncLoginAction = Loading()
)
}
currentJob = viewModelScope.launch { currentJob = viewModelScope.launch {
try { try {
safeLoginWizard.login( val result = safeLoginWizard.login(
action.username, action.username,
action.password, action.password,
action.initialDeviceName action.initialDeviceName
) )
reAuthHelper.data = action.password
onSessionCreated(result, isAccountCreated = false)
} catch (failure: Throwable) { } catch (failure: Throwable) {
setState { setState { copy(isLoading = false) }
copy(
asyncLoginAction = Fail(failure)
)
}
null
} }
?.let {
reAuthHelper.data = action.password
onSessionCreated(it, isAccountCreated = false)
}
} }
} }
} }
@ -678,12 +630,12 @@ class OnboardingViewModel @AssistedInject constructor(
true -> { true -> {
val personalizationState = createPersonalizationState(session, state) val personalizationState = createPersonalizationState(session, state)
setState { setState {
copy(asyncLoginAction = Success(Unit), personalizationState = personalizationState) copy(isLoading = false, personalizationState = personalizationState)
} }
_viewEvents.post(OnboardingViewEvents.OnAccountCreated) _viewEvents.post(OnboardingViewEvents.OnAccountCreated)
} }
false -> { false -> {
setState { copy(asyncLoginAction = Success(Unit)) } setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.OnAccountSignedIn) _viewEvents.post(OnboardingViewEvents.OnAccountSignedIn)
} }
} }
@ -712,14 +664,11 @@ class OnboardingViewModel @AssistedInject constructor(
} else { } else {
currentJob = viewModelScope.launch { currentJob = viewModelScope.launch {
try { try {
authenticationService.createSessionFromSso(homeServerConnectionConfigFinal, action.credentials) val result = authenticationService.createSessionFromSso(homeServerConnectionConfigFinal, action.credentials)
onSessionCreated(result, isAccountCreated = false)
} catch (failure: Throwable) { } catch (failure: Throwable) {
setState { setState { copy(isLoading = false) }
copy(asyncLoginAction = Fail(failure))
}
null
} }
?.let { onSessionCreated(it, isAccountCreated = false) }
} }
} }
} }

View File

@ -29,11 +29,9 @@ import im.vector.app.features.login.SignMode
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
data class OnboardingViewState( data class OnboardingViewState(
val asyncLoginAction: Async<Unit> = Uninitialized,
val asyncHomeServerLoginFlowRequest: Async<Unit> = Uninitialized, val asyncHomeServerLoginFlowRequest: Async<Unit> = Uninitialized,
val asyncResetPassword: Async<Unit> = Uninitialized, val asyncResetPassword: Async<Unit> = Uninitialized,
val asyncResetMailConfirmed: Async<Unit> = Uninitialized, val asyncResetMailConfirmed: Async<Unit> = Uninitialized,
val asyncRegistration: Async<Unit> = Uninitialized,
val isLoading: Boolean = false, val isLoading: Boolean = false,
@PersistState @PersistState
@ -73,11 +71,9 @@ data class OnboardingViewState(
) : MavericksState { ) : MavericksState {
fun legacyIsLoading(): Boolean { fun legacyIsLoading(): Boolean {
return asyncLoginAction is Loading || return asyncHomeServerLoginFlowRequest is Loading ||
asyncHomeServerLoginFlowRequest is Loading ||
asyncResetPassword is Loading || asyncResetPassword is Loading ||
asyncResetMailConfirmed is Loading || asyncResetMailConfirmed is Loading
asyncRegistration is Loading
} }
} }

View File

@ -26,8 +26,6 @@ import androidx.autofill.HintConstants
import androidx.core.text.isDigitsOnly import androidx.core.text.isDigitsOnly
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.hidePassword import im.vector.app.core.extensions.hidePassword
@ -74,6 +72,33 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel.viewEvents
.stream()
.onEach {
when (it) {
is OnboardingViewEvents.Failure -> {
if (it.throwable is Failure.ServerError &&
it.throwable.error.code == MatrixError.M_FORBIDDEN &&
it.throwable.error.message.isEmpty()) {
// Login with email, but email unknown
views.loginFieldTil.error = getString(R.string.login_login_with_email_error)
} else {
// Trick to display the error without text.
views.loginFieldTil.error = " "
if (it.throwable.isInvalidPassword() && spaceInPassword()) {
views.passwordFieldTil.error = getString(R.string.auth_invalid_login_param_space_in_password)
} else {
views.passwordFieldTil.error = errorFormatter.toHumanReadable(it.throwable)
}
}
}
else -> {
// do nothing
}
}
}
.launchIn(lifecycleScope)
setupSubmitButton() setupSubmitButton()
setupForgottenPasswordButton() setupForgottenPasswordButton()
@ -274,39 +299,9 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
setupSocialLoginButtons(state) setupSocialLoginButtons(state)
setupButtons(state) setupButtons(state)
when (state.asyncLoginAction) { if (state.isLoading) {
is Loading -> { // Ensure password is hidden
// Ensure password is hidden views.passwordField.hidePassword()
views.passwordField.hidePassword()
}
is Fail -> {
val error = state.asyncLoginAction.error
if (error is Failure.ServerError &&
error.error.code == MatrixError.M_FORBIDDEN &&
error.error.message.isEmpty()) {
// Login with email, but email unknown
views.loginFieldTil.error = getString(R.string.login_login_with_email_error)
} else {
// Trick to display the error without text.
views.loginFieldTil.error = " "
if (error.isInvalidPassword() && spaceInPassword()) {
views.passwordFieldTil.error = getString(R.string.auth_invalid_login_param_space_in_password)
} else {
views.passwordFieldTil.error = errorFormatter.toHumanReadable(error)
}
}
}
// Success is handled by the LoginActivity
else -> Unit
}
when (state.asyncRegistration) {
is Loading -> {
// Ensure password is hidden
views.passwordField.hidePassword()
}
// Success is handled by the LoginActivity
else -> Unit
} }
} }

View File

@ -128,8 +128,8 @@ class OnboardingViewModelTest {
.assertStatesChanges( .assertStatesChanges(
initialState, initialState,
{ copy(signMode = SignMode.SignUp) }, { copy(signMode = SignMode.SignUp) },
{ copy(asyncRegistration = Loading()) }, { copy(isLoading = true) },
{ copy(asyncRegistration = Uninitialized) } { copy(isLoading = false) }
) )
.assertEvents(OnboardingViewEvents.RegistrationFlowResult(ANY_CONTINUING_REGISTRATION_RESULT.flowResult, isRegistrationStarted = true)) .assertEvents(OnboardingViewEvents.RegistrationFlowResult(ANY_CONTINUING_REGISTRATION_RESULT.flowResult, isRegistrationStarted = true))
.finish() .finish()
@ -145,8 +145,8 @@ class OnboardingViewModelTest {
test test
.assertStatesChanges( .assertStatesChanges(
initialState, initialState,
{ copy(asyncRegistration = Loading()) }, { copy(isLoading = true) },
{ copy(asyncRegistration = Uninitialized) } { copy(isLoading = false) }
) )
.assertEvents(OnboardingViewEvents.RegistrationFlowResult(ANY_CONTINUING_REGISTRATION_RESULT.flowResult, isRegistrationStarted = true)) .assertEvents(OnboardingViewEvents.RegistrationFlowResult(ANY_CONTINUING_REGISTRATION_RESULT.flowResult, isRegistrationStarted = true))
.finish() .finish()
@ -175,8 +175,8 @@ class OnboardingViewModelTest {
test test
.assertStatesChanges( .assertStatesChanges(
initialState, initialState,
{ copy(asyncRegistration = Loading()) }, { copy(isLoading = true) },
{ copy(asyncRegistration = Uninitialized) } { copy(isLoading = false) }
) )
.assertNoEvents() .assertNoEvents()
.finish() .finish()
@ -193,9 +193,8 @@ class OnboardingViewModelTest {
test test
.assertStatesChanges( .assertStatesChanges(
initialState, initialState,
{ copy(asyncRegistration = Loading()) }, { copy(isLoading = true) },
{ copy(asyncLoginAction = Success(Unit), personalizationState = A_HOMESERVER_CAPABILITIES.toPersonalisationState()) }, { copy(isLoading = false, personalizationState = A_HOMESERVER_CAPABILITIES.toPersonalisationState()) }
{ copy(asyncLoginAction = Success(Unit), asyncRegistration = Uninitialized) }
) )
.assertEvents(OnboardingViewEvents.OnAccountCreated) .assertEvents(OnboardingViewEvents.OnAccountCreated)
.finish() .finish()
@ -211,9 +210,8 @@ class OnboardingViewModelTest {
test test
.assertStatesChanges( .assertStatesChanges(
initialState, initialState,
{ copy(asyncRegistration = Loading()) }, { copy(isLoading = true) },
{ copy(asyncLoginAction = Success(Unit), personalizationState = A_HOMESERVER_CAPABILITIES.toPersonalisationState()) }, { copy(isLoading = false, personalizationState = A_HOMESERVER_CAPABILITIES.toPersonalisationState()) }
{ copy(asyncRegistration = Uninitialized) }
) )
.assertEvents(OnboardingViewEvents.OnAccountCreated) .assertEvents(OnboardingViewEvents.OnAccountCreated)
.finish() .finish()