mirror of
https://github.com/element-hq/element-android
synced 2024-10-23 03:06:33 +03:00
Merge pull request #5517 from vector-im/feature/adm/ftue-single-loading-state
FTUE - Single loading state
This commit is contained in:
commit
84b34f75de
10 changed files with 157 additions and 271 deletions
1
changelog.d/5517.misc
Normal file
1
changelog.d/5517.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Flattening the asynchronous onboarding state and passing all errors through the same pipeline
|
|
@ -58,12 +58,36 @@ fun Throwable.getRetryDelay(defaultValue: Long): Long {
|
|||
?: defaultValue
|
||||
}
|
||||
|
||||
fun Throwable.isUsernameInUse(): Boolean {
|
||||
return this is Failure.ServerError && error.code == MatrixError.M_USER_IN_USE
|
||||
}
|
||||
|
||||
fun Throwable.isInvalidUsername(): Boolean {
|
||||
return this is Failure.ServerError &&
|
||||
error.code == MatrixError.M_INVALID_USERNAME
|
||||
}
|
||||
|
||||
fun Throwable.isInvalidPassword(): Boolean {
|
||||
return this is Failure.ServerError &&
|
||||
error.code == MatrixError.M_FORBIDDEN &&
|
||||
error.message == "Invalid password"
|
||||
}
|
||||
|
||||
fun Throwable.isRegistrationDisabled(): Boolean {
|
||||
return this is Failure.ServerError && error.code == MatrixError.M_FORBIDDEN &&
|
||||
httpCode == HttpsURLConnection.HTTP_FORBIDDEN
|
||||
}
|
||||
|
||||
fun Throwable.isWeakPassword(): Boolean {
|
||||
return this is Failure.ServerError && error.code == MatrixError.M_WEAK_PASSWORD
|
||||
}
|
||||
|
||||
fun Throwable.isLoginEmailUnknown(): Boolean {
|
||||
return this is Failure.ServerError &&
|
||||
error.code == MatrixError.M_FORBIDDEN &&
|
||||
error.message.isEmpty()
|
||||
}
|
||||
|
||||
fun Throwable.isInvalidUIAAuth(): Boolean {
|
||||
return this is Failure.ServerError &&
|
||||
error.code == MatrixError.M_FORBIDDEN &&
|
||||
|
@ -104,8 +128,8 @@ fun Throwable.isRegistrationAvailabilityError(): Boolean {
|
|||
return this is Failure.ServerError &&
|
||||
httpCode == HttpsURLConnection.HTTP_BAD_REQUEST && /* 400 */
|
||||
(error.code == MatrixError.M_USER_IN_USE ||
|
||||
error.code == MatrixError.M_INVALID_USERNAME ||
|
||||
error.code == MatrixError.M_EXCLUSIVE)
|
||||
error.code == MatrixError.M_INVALID_USERNAME ||
|
||||
error.code == MatrixError.M_EXCLUSIVE)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,11 +18,7 @@ package im.vector.app.features.onboarding
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
@ -239,31 +235,19 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
val safeLoginWizard = loginWizard
|
||||
|
||||
if (safeLoginWizard == null) {
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Fail(Throwable("Bad configuration"))
|
||||
)
|
||||
}
|
||||
setState { copy(isLoading = false) }
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration")))
|
||||
} else {
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Loading()
|
||||
)
|
||||
}
|
||||
setState { copy(isLoading = true) }
|
||||
|
||||
currentJob = viewModelScope.launch {
|
||||
try {
|
||||
safeLoginWizard.loginWithToken(action.loginToken)
|
||||
val result = safeLoginWizard.loginWithToken(action.loginToken)
|
||||
onSessionCreated(result, isAccountCreated = false)
|
||||
} catch (failure: Throwable) {
|
||||
setState { copy(isLoading = false) }
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(failure))
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Fail(failure)
|
||||
)
|
||||
}
|
||||
null
|
||||
}
|
||||
?.let { onSessionCreated(it, isAccountCreated = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -271,7 +255,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
private fun handleRegisterAction(action: RegisterAction) {
|
||||
currentJob = viewModelScope.launch {
|
||||
if (action.hasLoadingState()) {
|
||||
setState { copy(asyncRegistration = Loading()) }
|
||||
setState { copy(isLoading = true) }
|
||||
}
|
||||
runCatching { registrationActionHandler.handleRegisterAction(registrationWizard, action) }
|
||||
.fold(
|
||||
|
@ -292,7 +276,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
)
|
||||
setState { copy(asyncRegistration = Uninitialized) }
|
||||
setState { copy(isLoading = false) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -322,7 +306,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
authenticationService.reset()
|
||||
setState {
|
||||
copy(
|
||||
asyncHomeServerLoginFlowRequest = Uninitialized,
|
||||
isLoading = false,
|
||||
homeServerUrlFromUser = null,
|
||||
homeServerUrl = null,
|
||||
loginMode = LoginMode.Unknown,
|
||||
|
@ -335,7 +319,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
OnboardingAction.ResetSignMode -> {
|
||||
setState {
|
||||
copy(
|
||||
asyncHomeServerLoginFlowRequest = Uninitialized,
|
||||
isLoading = false,
|
||||
signMode = SignMode.Unknown,
|
||||
loginMode = LoginMode.Unknown,
|
||||
loginModeSupportedTypes = emptyList()
|
||||
|
@ -345,19 +329,13 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
OnboardingAction.ResetLogin -> {
|
||||
viewModelScope.launch {
|
||||
authenticationService.cancelPendingLoginOrRegistration()
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Uninitialized,
|
||||
asyncRegistration = Uninitialized
|
||||
)
|
||||
}
|
||||
setState { copy(isLoading = false) }
|
||||
}
|
||||
}
|
||||
OnboardingAction.ResetResetPassword -> {
|
||||
setState {
|
||||
copy(
|
||||
asyncResetPassword = Uninitialized,
|
||||
asyncResetMailConfirmed = Uninitialized,
|
||||
isLoading = false,
|
||||
resetPasswordEmail = null
|
||||
)
|
||||
}
|
||||
|
@ -426,35 +404,23 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
val safeLoginWizard = loginWizard
|
||||
|
||||
if (safeLoginWizard == null) {
|
||||
setState {
|
||||
copy(
|
||||
asyncResetPassword = Fail(Throwable("Bad configuration")),
|
||||
asyncResetMailConfirmed = Uninitialized
|
||||
)
|
||||
}
|
||||
setState { copy(isLoading = false) }
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration")))
|
||||
} else {
|
||||
setState {
|
||||
copy(
|
||||
asyncResetPassword = Loading(),
|
||||
asyncResetMailConfirmed = Uninitialized
|
||||
)
|
||||
}
|
||||
setState { copy(isLoading = true) }
|
||||
|
||||
currentJob = viewModelScope.launch {
|
||||
try {
|
||||
safeLoginWizard.resetPassword(action.email, action.newPassword)
|
||||
} catch (failure: Throwable) {
|
||||
setState {
|
||||
copy(
|
||||
asyncResetPassword = Fail(failure)
|
||||
)
|
||||
}
|
||||
setState { copy(isLoading = false) }
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(failure))
|
||||
return@launch
|
||||
}
|
||||
|
||||
setState {
|
||||
copy(
|
||||
asyncResetPassword = Success(Unit),
|
||||
isLoading = false,
|
||||
resetPasswordEmail = action.email
|
||||
)
|
||||
}
|
||||
|
@ -468,34 +434,22 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
val safeLoginWizard = loginWizard
|
||||
|
||||
if (safeLoginWizard == null) {
|
||||
setState {
|
||||
copy(
|
||||
asyncResetPassword = Uninitialized,
|
||||
asyncResetMailConfirmed = Fail(Throwable("Bad configuration"))
|
||||
)
|
||||
}
|
||||
setState { copy(isLoading = false) }
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration")))
|
||||
} else {
|
||||
setState {
|
||||
copy(
|
||||
asyncResetPassword = Uninitialized,
|
||||
asyncResetMailConfirmed = Loading()
|
||||
)
|
||||
}
|
||||
setState { copy(isLoading = false) }
|
||||
|
||||
currentJob = viewModelScope.launch {
|
||||
try {
|
||||
safeLoginWizard.resetPasswordMailConfirmed()
|
||||
} catch (failure: Throwable) {
|
||||
setState {
|
||||
copy(
|
||||
asyncResetMailConfirmed = Fail(failure)
|
||||
)
|
||||
}
|
||||
setState { copy(isLoading = false) }
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(failure))
|
||||
return@launch
|
||||
}
|
||||
setState {
|
||||
copy(
|
||||
asyncResetMailConfirmed = Success(Unit),
|
||||
isLoading = false,
|
||||
resetPasswordEmail = null
|
||||
)
|
||||
}
|
||||
|
@ -515,11 +469,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun handleDirectLogin(action: OnboardingAction.LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?) {
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Loading()
|
||||
)
|
||||
}
|
||||
setState { copy(isLoading = true) }
|
||||
|
||||
currentJob = viewModelScope.launch {
|
||||
val data = try {
|
||||
|
@ -546,11 +496,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun onWellKnownError() {
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Uninitialized
|
||||
)
|
||||
}
|
||||
setState { copy(isLoading = false) }
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error))))
|
||||
}
|
||||
|
||||
|
@ -585,20 +531,12 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
when (failure) {
|
||||
is MatrixIdFailure.InvalidMatrixId,
|
||||
is Failure.UnrecognizedCertificateFailure -> {
|
||||
setState { copy(isLoading = false) }
|
||||
// Display this error in a dialog
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(failure))
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Uninitialized
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Fail(failure)
|
||||
)
|
||||
}
|
||||
setState { copy(isLoading = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -607,37 +545,23 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
val safeLoginWizard = loginWizard
|
||||
|
||||
if (safeLoginWizard == null) {
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Fail(Throwable("Bad configuration"))
|
||||
)
|
||||
}
|
||||
setState { copy(isLoading = false) }
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration")))
|
||||
} else {
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Loading()
|
||||
)
|
||||
}
|
||||
|
||||
setState { copy(isLoading = true) }
|
||||
currentJob = viewModelScope.launch {
|
||||
try {
|
||||
safeLoginWizard.login(
|
||||
val result = safeLoginWizard.login(
|
||||
action.username,
|
||||
action.password,
|
||||
action.initialDeviceName
|
||||
)
|
||||
reAuthHelper.data = action.password
|
||||
onSessionCreated(result, isAccountCreated = false)
|
||||
} catch (failure: Throwable) {
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Fail(failure)
|
||||
)
|
||||
}
|
||||
null
|
||||
setState { copy(isLoading = false) }
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(failure))
|
||||
}
|
||||
?.let {
|
||||
reAuthHelper.data = action.password
|
||||
onSessionCreated(it, isAccountCreated = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -678,12 +602,12 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
true -> {
|
||||
val personalizationState = createPersonalizationState(session, state)
|
||||
setState {
|
||||
copy(asyncLoginAction = Success(Unit), personalizationState = personalizationState)
|
||||
copy(isLoading = false, personalizationState = personalizationState)
|
||||
}
|
||||
_viewEvents.post(OnboardingViewEvents.OnAccountCreated)
|
||||
}
|
||||
false -> {
|
||||
setState { copy(asyncLoginAction = Success(Unit)) }
|
||||
setState { copy(isLoading = false) }
|
||||
_viewEvents.post(OnboardingViewEvents.OnAccountSignedIn)
|
||||
}
|
||||
}
|
||||
|
@ -712,14 +636,11 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
} else {
|
||||
currentJob = viewModelScope.launch {
|
||||
try {
|
||||
authenticationService.createSessionFromSso(homeServerConnectionConfigFinal, action.credentials)
|
||||
val result = authenticationService.createSessionFromSso(homeServerConnectionConfigFinal, action.credentials)
|
||||
onSessionCreated(result, isAccountCreated = false)
|
||||
} catch (failure: Throwable) {
|
||||
setState {
|
||||
copy(asyncLoginAction = Fail(failure))
|
||||
}
|
||||
null
|
||||
setState { copy(isLoading = false) }
|
||||
}
|
||||
?.let { onSessionCreated(it, isAccountCreated = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -743,7 +664,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
|
||||
setState {
|
||||
copy(
|
||||
asyncHomeServerLoginFlowRequest = Loading(),
|
||||
isLoading = true,
|
||||
// If user has entered https://matrix.org, ensure that server type is ServerType.MatrixOrg
|
||||
// It is also useful to set the value again in the case of a certificate error on matrix.org
|
||||
serverType = if (homeServerConnectionConfig.homeServerUri.toString() == matrixOrgUrl) {
|
||||
|
@ -757,14 +678,14 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
val data = try {
|
||||
authenticationService.getLoginFlow(homeServerConnectionConfig)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(failure))
|
||||
setState {
|
||||
copy(
|
||||
asyncHomeServerLoginFlowRequest = Uninitialized,
|
||||
isLoading = false,
|
||||
// If we were trying to retrieve matrix.org login flow, also reset the serverType
|
||||
serverType = if (serverType == ServerType.MatrixOrg) ServerType.Unknown else serverType
|
||||
)
|
||||
}
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(failure))
|
||||
null
|
||||
}
|
||||
|
||||
|
@ -785,7 +706,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
|
||||
setState {
|
||||
copy(
|
||||
asyncHomeServerLoginFlowRequest = Uninitialized,
|
||||
isLoading = false,
|
||||
homeServerUrlFromUser = homeServerConnectionConfig.homeServerUri.toString(),
|
||||
homeServerUrl = data.homeServerUrl,
|
||||
loginMode = loginMode,
|
||||
|
@ -828,20 +749,20 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun updateDisplayName(displayName: String) {
|
||||
setState { copy(asyncDisplayName = Loading()) }
|
||||
setState { copy(isLoading = true) }
|
||||
viewModelScope.launch {
|
||||
val activeSession = activeSessionHolder.getActiveSession()
|
||||
try {
|
||||
activeSession.setDisplayName(activeSession.myUserId, displayName)
|
||||
setState {
|
||||
copy(
|
||||
asyncDisplayName = Success(Unit),
|
||||
isLoading = false,
|
||||
personalizationState = personalizationState.copy(displayName = displayName)
|
||||
)
|
||||
}
|
||||
handleDisplayNameStepComplete()
|
||||
} catch (error: Throwable) {
|
||||
setState { copy(asyncDisplayName = Fail(error)) }
|
||||
setState { copy(isLoading = false) }
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(error))
|
||||
}
|
||||
}
|
||||
|
@ -883,7 +804,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
when (val pictureUri = state.personalizationState.selectedPictureUri) {
|
||||
null -> _viewEvents.post(OnboardingViewEvents.Failure(NullPointerException("picture uri is missing from state")))
|
||||
else -> {
|
||||
setState { copy(asyncProfilePicture = Loading()) }
|
||||
setState { copy(isLoading = true) }
|
||||
viewModelScope.launch {
|
||||
val activeSession = activeSessionHolder.getActiveSession()
|
||||
try {
|
||||
|
@ -894,12 +815,12 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
)
|
||||
setState {
|
||||
copy(
|
||||
asyncProfilePicture = Success(Unit),
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
onProfilePictureSaved()
|
||||
} catch (error: Throwable) {
|
||||
setState { copy(asyncProfilePicture = Fail(error)) }
|
||||
setState { copy(isLoading = false) }
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(error))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,24 +18,15 @@ package im.vector.app.features.onboarding
|
|||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MavericksState
|
||||
import com.airbnb.mvrx.PersistState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.ServerType
|
||||
import im.vector.app.features.login.SignMode
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
data class OnboardingViewState(
|
||||
val asyncLoginAction: Async<Unit> = Uninitialized,
|
||||
val asyncHomeServerLoginFlowRequest: Async<Unit> = Uninitialized,
|
||||
val asyncResetPassword: Async<Unit> = Uninitialized,
|
||||
val asyncResetMailConfirmed: Async<Unit> = Uninitialized,
|
||||
val asyncRegistration: Async<Unit> = Uninitialized,
|
||||
val asyncDisplayName: Async<Unit> = Uninitialized,
|
||||
val asyncProfilePicture: Async<Unit> = Uninitialized,
|
||||
val isLoading: Boolean = false,
|
||||
|
||||
@PersistState
|
||||
val onboardingFlow: OnboardingFlow? = null,
|
||||
|
@ -71,18 +62,7 @@ data class OnboardingViewState(
|
|||
|
||||
@PersistState
|
||||
val personalizationState: PersonalizationState = PersonalizationState()
|
||||
) : MavericksState {
|
||||
|
||||
fun isLoading(): Boolean {
|
||||
return asyncLoginAction is Loading ||
|
||||
asyncHomeServerLoginFlowRequest is Loading ||
|
||||
asyncResetPassword is Loading ||
|
||||
asyncResetMailConfirmed is Loading ||
|
||||
asyncRegistration is Loading ||
|
||||
asyncDisplayName is Loading ||
|
||||
asyncProfilePicture is Loading
|
||||
}
|
||||
}
|
||||
) : MavericksState
|
||||
|
||||
enum class OnboardingFlow {
|
||||
SignIn,
|
||||
|
|
|
@ -34,8 +34,6 @@ import im.vector.app.features.onboarding.OnboardingViewModel
|
|||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
/**
|
||||
* Parent Fragment for all the login/registration screens
|
||||
|
@ -85,21 +83,8 @@ abstract class AbstractFtueAuthFragment<VB : ViewBinding> : VectorBaseFragment<V
|
|||
is CancellationException ->
|
||||
/* Ignore this error, user has cancelled the action */
|
||||
Unit
|
||||
is Failure.ServerError ->
|
||||
if (throwable.error.code == MatrixError.M_FORBIDDEN &&
|
||||
throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) {
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(getString(R.string.login_registration_disabled))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
} else {
|
||||
onError(throwable)
|
||||
}
|
||||
is Failure.UnrecognizedCertificateFailure ->
|
||||
showUnrecognizedCertificateFailure(throwable)
|
||||
else ->
|
||||
onError(throwable)
|
||||
is Failure.UnrecognizedCertificateFailure -> showUnrecognizedCertificateFailure(throwable)
|
||||
else -> onError(throwable)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,8 +26,7 @@ import androidx.autofill.HintConstants
|
|||
import androidx.core.text.isDigitsOnly
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.hidePassword
|
||||
|
@ -45,9 +44,12 @@ import kotlinx.coroutines.flow.combine
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
||||
import org.matrix.android.sdk.api.failure.isInvalidUsername
|
||||
import org.matrix.android.sdk.api.failure.isLoginEmailUnknown
|
||||
import org.matrix.android.sdk.api.failure.isRegistrationDisabled
|
||||
import org.matrix.android.sdk.api.failure.isUsernameInUse
|
||||
import org.matrix.android.sdk.api.failure.isWeakPassword
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -256,12 +258,31 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
|
|||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
// Show M_WEAK_PASSWORD error in the password field
|
||||
if (throwable is Failure.ServerError &&
|
||||
throwable.error.code == MatrixError.M_WEAK_PASSWORD) {
|
||||
views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
} else {
|
||||
views.loginFieldTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
// Trick to display the error without text.
|
||||
views.loginFieldTil.error = " "
|
||||
when {
|
||||
throwable.isUsernameInUse() || throwable.isInvalidUsername() -> {
|
||||
views.loginFieldTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
throwable.isLoginEmailUnknown() -> {
|
||||
views.loginFieldTil.error = getString(R.string.login_login_with_email_error)
|
||||
}
|
||||
throwable.isInvalidPassword() && spaceInPassword() -> {
|
||||
views.passwordFieldTil.error = getString(R.string.auth_invalid_login_param_space_in_password)
|
||||
}
|
||||
throwable.isWeakPassword() || throwable.isInvalidPassword() -> {
|
||||
views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
throwable.isRegistrationDisabled() -> {
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(getString(R.string.login_registration_disabled))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
else -> {
|
||||
super.onError(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -274,39 +295,9 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
|
|||
setupSocialLoginButtons(state)
|
||||
setupButtons(state)
|
||||
|
||||
when (state.asyncLoginAction) {
|
||||
is Loading -> {
|
||||
// Ensure password is hidden
|
||||
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
|
||||
if (state.isLoading) {
|
||||
// Ensure password is hidden
|
||||
views.passwordField.hidePassword()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,8 +21,6 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
|
@ -53,10 +51,13 @@ class FtueAuthResetPasswordFragment @Inject constructor() : AbstractFtueAuthFrag
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupSubmitButton()
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
views.resetPasswordEmailTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
|
||||
private fun setupUi(state: OnboardingViewState) {
|
||||
views.resetPasswordTitle.text = getString(R.string.login_reset_password_on, state.homeServerUrlFromUser.toReducedUrl())
|
||||
}
|
||||
|
@ -115,16 +116,9 @@ class FtueAuthResetPasswordFragment @Inject constructor() : AbstractFtueAuthFrag
|
|||
|
||||
override fun updateWithState(state: OnboardingViewState) {
|
||||
setupUi(state)
|
||||
|
||||
when (state.asyncResetPassword) {
|
||||
is Loading -> {
|
||||
// Ensure new password is hidden
|
||||
views.passwordField.hidePassword()
|
||||
}
|
||||
is Fail -> {
|
||||
views.resetPasswordEmailTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error)
|
||||
}
|
||||
else -> Unit
|
||||
if (state.isLoading) {
|
||||
// Ensure new password is hidden
|
||||
views.passwordField.hidePassword()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.databinding.FragmentLoginResetPasswordMailConfirmationBinding
|
||||
|
@ -58,23 +57,20 @@ class FtueAuthResetPasswordMailConfirmationFragment @Inject constructor() : Abst
|
|||
|
||||
override fun updateWithState(state: OnboardingViewState) {
|
||||
setupUi(state)
|
||||
}
|
||||
|
||||
when (state.asyncResetMailConfirmed) {
|
||||
is Fail -> {
|
||||
// Link in email not yet clicked ?
|
||||
val message = if (state.asyncResetMailConfirmed.error.is401()) {
|
||||
getString(R.string.auth_reset_password_error_unauthorized)
|
||||
} else {
|
||||
errorFormatter.toHumanReadable(state.asyncResetMailConfirmed.error)
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
else -> Unit
|
||||
override fun onError(throwable: Throwable) {
|
||||
// Link in email not yet clicked ?
|
||||
val message = if (throwable.is401()) {
|
||||
getString(R.string.auth_reset_password_error_unauthorized)
|
||||
} else {
|
||||
errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,7 +121,7 @@ class FtueAuthVariant(
|
|||
|
||||
private fun updateWithState(viewState: OnboardingViewState) {
|
||||
isForceLoginFallbackEnabled = viewState.isForceLoginFallbackEnabled
|
||||
views.loginLoading.isVisible = viewState.isLoading()
|
||||
views.loginLoading.isVisible = viewState.isLoading
|
||||
}
|
||||
|
||||
override fun setIsLoading(isLoading: Boolean) = Unit
|
||||
|
|
|
@ -17,10 +17,6 @@
|
|||
package im.vector.app.features.onboarding
|
||||
|
||||
import android.net.Uri
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.airbnb.mvrx.test.MvRxTestRule
|
||||
import im.vector.app.features.login.ReAuthHelper
|
||||
import im.vector.app.features.login.SignMode
|
||||
|
@ -129,8 +125,8 @@ class OnboardingViewModelTest {
|
|||
.assertStatesChanges(
|
||||
initialState,
|
||||
{ copy(signMode = SignMode.SignUp) },
|
||||
{ copy(asyncRegistration = Loading()) },
|
||||
{ copy(asyncRegistration = Uninitialized) }
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false) }
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.RegistrationFlowResult(ANY_CONTINUING_REGISTRATION_RESULT.flowResult, isRegistrationStarted = true))
|
||||
.finish()
|
||||
|
@ -146,8 +142,8 @@ class OnboardingViewModelTest {
|
|||
test
|
||||
.assertStatesChanges(
|
||||
initialState,
|
||||
{ copy(asyncRegistration = Loading()) },
|
||||
{ copy(asyncRegistration = Uninitialized) }
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false) }
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.RegistrationFlowResult(ANY_CONTINUING_REGISTRATION_RESULT.flowResult, isRegistrationStarted = true))
|
||||
.finish()
|
||||
|
@ -176,8 +172,8 @@ class OnboardingViewModelTest {
|
|||
test
|
||||
.assertStatesChanges(
|
||||
initialState,
|
||||
{ copy(asyncRegistration = Loading()) },
|
||||
{ copy(asyncRegistration = Uninitialized) }
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false) }
|
||||
)
|
||||
.assertNoEvents()
|
||||
.finish()
|
||||
|
@ -194,9 +190,8 @@ class OnboardingViewModelTest {
|
|||
test
|
||||
.assertStatesChanges(
|
||||
initialState,
|
||||
{ copy(asyncRegistration = Loading()) },
|
||||
{ copy(asyncLoginAction = Success(Unit), personalizationState = A_HOMESERVER_CAPABILITIES.toPersonalisationState()) },
|
||||
{ copy(asyncLoginAction = Success(Unit), asyncRegistration = Uninitialized) }
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false, personalizationState = A_HOMESERVER_CAPABILITIES.toPersonalisationState()) }
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.OnAccountCreated)
|
||||
.finish()
|
||||
|
@ -212,9 +207,8 @@ class OnboardingViewModelTest {
|
|||
test
|
||||
.assertStatesChanges(
|
||||
initialState,
|
||||
{ copy(asyncRegistration = Loading()) },
|
||||
{ copy(asyncLoginAction = Success(Unit), personalizationState = A_HOMESERVER_CAPABILITIES.toPersonalisationState()) },
|
||||
{ copy(asyncRegistration = Uninitialized) }
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false, personalizationState = A_HOMESERVER_CAPABILITIES.toPersonalisationState()) }
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.OnAccountCreated)
|
||||
.finish()
|
||||
|
@ -260,8 +254,8 @@ class OnboardingViewModelTest {
|
|||
test
|
||||
.assertStatesChanges(
|
||||
initialState,
|
||||
{ copy(asyncDisplayName = Loading()) },
|
||||
{ copy(asyncDisplayName = Fail(AN_ERROR)) },
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false) },
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.Failure(AN_ERROR))
|
||||
.finish()
|
||||
|
@ -307,7 +301,7 @@ class OnboardingViewModelTest {
|
|||
viewModel.handle(OnboardingAction.SaveSelectedProfilePicture)
|
||||
|
||||
test
|
||||
.assertStates(expectedProfilePictureFailureStates(initialStateWithPicture, AN_ERROR))
|
||||
.assertStates(expectedProfilePictureFailureStates(initialStateWithPicture))
|
||||
.assertEvents(OnboardingViewEvents.Failure(AN_ERROR))
|
||||
.finish()
|
||||
}
|
||||
|
@ -362,20 +356,20 @@ class OnboardingViewModelTest {
|
|||
|
||||
private fun expectedProfilePictureSuccessStates(state: OnboardingViewState) = listOf(
|
||||
state,
|
||||
state.copy(asyncProfilePicture = Loading()),
|
||||
state.copy(asyncProfilePicture = Success(Unit))
|
||||
state.copy(isLoading = true),
|
||||
state.copy(isLoading = false)
|
||||
)
|
||||
|
||||
private fun expectedProfilePictureFailureStates(state: OnboardingViewState, cause: Exception) = listOf(
|
||||
private fun expectedProfilePictureFailureStates(state: OnboardingViewState) = listOf(
|
||||
state,
|
||||
state.copy(asyncProfilePicture = Loading()),
|
||||
state.copy(asyncProfilePicture = Fail(cause))
|
||||
state.copy(isLoading = true),
|
||||
state.copy(isLoading = false)
|
||||
)
|
||||
|
||||
private fun expectedSuccessfulDisplayNameUpdateStates(): List<OnboardingViewState.() -> OnboardingViewState> {
|
||||
return listOf(
|
||||
{ copy(asyncDisplayName = Loading()) },
|
||||
{ copy(asyncDisplayName = Success(Unit), personalizationState = personalizationState.copy(displayName = A_DISPLAY_NAME)) }
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false, personalizationState = personalizationState.copy(displayName = A_DISPLAY_NAME)) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue