Login screens: UI: display errors properly

This commit is contained in:
Benoit Marty 2019-11-20 14:04:32 +01:00
parent f74cabd145
commit abbe56acfa
14 changed files with 184 additions and 39 deletions

View file

@ -160,6 +160,11 @@ interface FragmentModule {
@FragmentKey(LoginWebFragment::class)
fun bindLoginWebFragment(fragment: LoginWebFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginGenericTextInputFormFragment::class)
fun bindLoginGenericTextInputFormFragment(fragment: LoginGenericTextInputFormFragment): Fragment
@Binds
@IntoMap
@FragmentKey(CreateDirectRoomDirectoryUsersFragment::class)

View file

@ -21,9 +21,12 @@ import android.os.Bundle
import android.view.View
import androidx.annotation.CallSuper
import com.airbnb.mvrx.activityViewModel
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.riotx.R
import im.vector.riotx.core.platform.OnBackPressed
import im.vector.riotx.core.platform.VectorBaseFragment
import javax.net.ssl.HttpsURLConnection
/**
* Parent Fragment for all the login/registration screens
@ -38,8 +41,44 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
super.onViewCreated(view, savedInstanceState)
loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java)
loginViewModel.viewEvents
.observe()
.subscribe {
handleLoginViewEvents(it)
}
.disposeOnDestroyView()
}
private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) {
when (loginViewEvents) {
is LoginViewEvents.RegistrationFlowResult ->
// This is handled by the Activity
Unit
is LoginViewEvents.RegistrationError -> displayRegistrationError(loginViewEvents.throwable)
}
}
private fun displayRegistrationError(throwable: Throwable) {
when (throwable) {
is Failure.ServerError -> {
if (throwable.error.code == MatrixError.FORBIDDEN
&& throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(getString(R.string.login_registration_disabled))
.setPositiveButton(R.string.ok, null)
.show()
} else {
onRegistrationError(throwable)
}
}
else -> onRegistrationError(throwable)
}
}
abstract fun onRegistrationError(throwable: Throwable)
override fun onBackPressed(toolbarButton: Boolean): Boolean {
if (loginViewModel.isPasswordSent) {
// Ask for confirmation before cancelling the registration

View file

@ -28,11 +28,8 @@ import com.airbnb.mvrx.viewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.auth.registration.FlowResult
import im.vector.matrix.android.api.auth.registration.Stage
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.extensions.addFragmentToBackstack
import im.vector.riotx.core.platform.ToolbarConfigurable
@ -43,7 +40,6 @@ import im.vector.riotx.features.login.terms.LoginTermsFragmentArgument
import im.vector.riotx.features.login.terms.toLocalizedLoginTerms
import kotlinx.android.synthetic.main.activity_login.*
import javax.inject.Inject
import javax.net.ssl.HttpsURLConnection
/**
* The LoginActivity manages the fragment navigation and also display the loading View
@ -54,7 +50,6 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel
@Inject lateinit var loginViewModelFactory: LoginViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
@ -122,7 +117,6 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
onRegistrationStageNotSupported()
} else {
// Go on with registration flow
// loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected)
if (loginViewModel.isPasswordSent) {
handleRegistrationNavigation(loginViewEvents.flowResult)
} else {
@ -135,31 +129,12 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
}
}
}
is LoginViewEvents.RegistrationError -> displayRegistrationError(loginViewEvents.throwable)
is LoginViewEvents.RegistrationError ->
// This is handled by the Fragments
Unit
}
}
private fun displayRegistrationError(throwable: Throwable) {
val message = when (throwable) {
is Failure.ServerError -> {
if (throwable.error.code == MatrixError.FORBIDDEN
&& throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) {
getString(R.string.login_registration_disabled)
} else {
null
}
}
else -> null
}
?: errorFormatter.toHumanReadable(throwable)
AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_error)
.setMessage(message)
.setPositiveButton(R.string.ok, null)
.show()
}
private fun onLoginFlowRetrieved() {
addFragmentToBackstack(R.id.loginFragmentContainer, LoginSignUpSignInSelectionFragment::class.java)
}

View file

@ -31,6 +31,7 @@ import androidx.core.view.isVisible
import com.airbnb.mvrx.args
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.utils.AssetReader
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_login_captcha.*
@ -47,7 +48,10 @@ data class LoginCaptchaFragmentArgument(
/**
* In this screen, the user is asked to confirm he is not a robot
*/
class LoginCaptchaFragment @Inject constructor(private val assetReader: AssetReader) : AbstractLoginFragment() {
class LoginCaptchaFragment @Inject constructor(
private val assetReader: AssetReader,
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_captcha
@ -174,6 +178,15 @@ class LoginCaptchaFragment @Inject constructor(private val assetReader: AssetRea
}
}
override fun onRegistrationError(throwable: Throwable) {
// Cannot happen here, but just in case
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetLogin)
}

View file

@ -161,6 +161,10 @@ class LoginFragment @Inject constructor(
loginViewModel.handle(LoginAction.ResetLogin)
}
override fun onRegistrationError(throwable: Throwable) {
loginFieldTil.error = errorFormatter.toHumanReadable(throwable)
}
override fun invalidate() = withState(loginViewModel) { state ->
when (state.asyncLoginAction) {
is Loading -> {
@ -184,9 +188,6 @@ class LoginFragment @Inject constructor(
passwordShown = false
renderPasswordField()
}
is Fail -> {
loginFieldTil.error = errorFormatter.toHumanReadable(state.asyncRegistration.error)
}
// Success is handled by the LoginActivity
is Success -> Unit
}

View file

@ -25,6 +25,7 @@ import butterknife.OnClick
import com.airbnb.mvrx.args
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_login_generic_text_input_form.*
import javax.inject.Inject
@ -44,7 +45,7 @@ data class LoginGenericTextInputFormFragmentArgument(
/**
* In this screen, the user is asked for a text input
*/
class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFragment() {
class LoginGenericTextInputFormFragment @Inject constructor(private val errorFormatter: ErrorFormatter) : AbstractLoginFragment() {
private val params: LoginGenericTextInputFormFragmentArgument by args()
@ -55,6 +56,15 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra
setupUi()
setupSubmitButton()
setupTil()
}
private fun setupTil() {
loginGenericTextInputFormTextInput.textChanges()
.subscribe {
loginGenericTextInputFormTil.error = null
}
.disposeOnDestroyView()
}
private fun setupUi() {
@ -128,6 +138,10 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra
}
}
override fun onRegistrationError(throwable: Throwable) {
loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetLogin)
}

View file

@ -18,6 +18,7 @@ package im.vector.riotx.features.login
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
@ -115,6 +116,15 @@ class LoginResetPasswordFragment @Inject constructor(
loginViewModel.handle(LoginAction.ResetResetPassword)
}
override fun onRegistrationError(throwable: Throwable) {
// TODO
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun invalidate() = withState(loginViewModel) { state ->
when (state.asyncResetPassword) {
is Loading -> {

View file

@ -18,15 +18,19 @@ package im.vector.riotx.features.login
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import butterknife.OnClick
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import kotlinx.android.synthetic.main.fragment_login_reset_password_success.*
import javax.inject.Inject
/**
* In this screen, the user is asked for email and new password to reset his password
*/
class LoginResetPasswordSuccessFragment @Inject constructor() : AbstractLoginFragment() {
class LoginResetPasswordSuccessFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_reset_password_success
@ -45,6 +49,15 @@ class LoginResetPasswordSuccessFragment @Inject constructor() : AbstractLoginFra
loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordSuccessDone)
}
override fun onRegistrationError(throwable: Throwable) {
// Cannot happen here, but just in case
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetResetPassword)
}

View file

@ -19,12 +19,14 @@ package im.vector.riotx.features.login
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.transition.TransitionInflater
import butterknife.OnClick
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import kotlinx.android.synthetic.main.fragment_login_server_selection.*
import me.gujun.android.span.span
@ -33,7 +35,9 @@ import javax.inject.Inject
/**
* In this screen, the user will choose between matrix.org, modular or other type of homeserver
*/
class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment() {
class LoginServerSelectionFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_server_selection
@ -118,6 +122,15 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
loginViewModel.handle(LoginAction.ResetHomeServerType)
}
override fun onRegistrationError(throwable: Throwable) {
// Cannot happen here, but just in case
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun invalidate() = withState(loginViewModel) {
when (it.asyncHomeServerLoginFlowRequest) {
is Fail -> {

View file

@ -20,6 +20,7 @@ import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import android.view.inputmethod.EditorInfo
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import butterknife.OnClick
import com.airbnb.mvrx.Fail
@ -121,6 +122,15 @@ class LoginServerUrlFormFragment @Inject constructor(
}
}
override fun onRegistrationError(throwable: Throwable) {
// Cannot happen here, but just in case
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun invalidate() = withState(loginViewModel) { state ->
when (state.asyncHomeServerLoginFlowRequest) {
is Fail -> {

View file

@ -18,17 +18,21 @@ package im.vector.riotx.features.login
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import butterknife.OnClick
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.*
import javax.inject.Inject
/**
* In this screen, the user is asked to sign up or to sign in to the homeserver
*/
class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() {
class LoginSignUpSignInSelectionFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection
@ -92,6 +96,15 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr
loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected)
}
override fun onRegistrationError(throwable: Throwable) {
// Cannot happen here, but just in case
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetSignMode)
}

View file

@ -16,14 +16,18 @@
package im.vector.riotx.features.login
import androidx.appcompat.app.AlertDialog
import butterknife.OnClick
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import javax.inject.Inject
/**
* In this screen, the user is viewing an introduction to what he can do with this application
*/
class LoginSplashFragment @Inject constructor() : AbstractLoginFragment() {
class LoginSplashFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_splash
@ -32,6 +36,15 @@ class LoginSplashFragment @Inject constructor() : AbstractLoginFragment() {
loginSharedActionViewModel.post(LoginNavigation.OpenServerSelection)
}
override fun onRegistrationError(throwable: Throwable) {
// Cannot happen here, but just in case
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun resetViewModel() {
// Nothing to do
}

View file

@ -32,6 +32,7 @@ import android.webkit.WebViewClient
import androidx.appcompat.app.AlertDialog
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.utils.AssetReader
import kotlinx.android.synthetic.main.fragment_login_web.*
import timber.log.Timber
@ -42,7 +43,10 @@ import javax.inject.Inject
* This screen is displayed for SSO login and also when the application does not support login flow or registration flow
* of the homeserver, as a fallback to login or to create an account
*/
class LoginWebFragment @Inject constructor(private val assetReader: AssetReader) : AbstractLoginFragment() {
class LoginWebFragment @Inject constructor(
private val assetReader: AssetReader,
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
private lateinit var homeServerUrl: String
private lateinit var signMode: SignMode
@ -241,6 +245,15 @@ class LoginWebFragment @Inject constructor(private val assetReader: AssetReader)
loginViewModel.handle(LoginAction.ResetLogin)
}
override fun onRegistrationError(throwable: Throwable) {
// Cannot happen here, but just in case
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun onBackPressed(toolbarButton: Boolean): Boolean {
return when {
toolbarButton -> super.onBackPressed(toolbarButton)

View file

@ -19,9 +19,11 @@ package im.vector.riotx.features.login.terms
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import androidx.appcompat.app.AlertDialog
import butterknife.OnClick
import com.airbnb.mvrx.args
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import im.vector.riotx.features.login.AbstractLoginFragment
import im.vector.riotx.features.login.LoginAction
@ -38,7 +40,10 @@ data class LoginTermsFragmentArgument(
/**
* LoginTermsFragment displays the list of policies the user has to accept
*/
class LoginTermsFragment @Inject constructor(private val policyController: PolicyController) : AbstractLoginFragment(),
class LoginTermsFragment @Inject constructor(
private val policyController: PolicyController,
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment(),
PolicyController.PolicyControllerListener {
private val params: LoginTermsFragmentArgument by args()
@ -103,6 +108,14 @@ class LoginTermsFragment @Inject constructor(private val policyController: Polic
loginViewModel.handle(LoginAction.AcceptTerms)
}
override fun onRegistrationError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetLogin)
}