mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 02:15:35 +03:00
Merge pull request #5763 from vector-im/feature/adm/server-selection-errors
FTUE - Server selection errors
This commit is contained in:
commit
e58677a104
7 changed files with 241 additions and 178 deletions
1
changelog.d/5749.wip
Normal file
1
changelog.d/5749.wip
Normal file
|
@ -0,0 +1 @@
|
|||
Adds error handling within the new FTUE server selection screen
|
|
@ -22,34 +22,29 @@ import org.matrix.android.sdk.api.session.contentscanner.ContentScannerError
|
|||
import org.matrix.android.sdk.api.session.contentscanner.ScanFailure
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import java.io.IOException
|
||||
import java.net.UnknownHostException
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
fun Throwable.is401() =
|
||||
this is Failure.ServerError &&
|
||||
httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED && /* 401 */
|
||||
error.code == MatrixError.M_UNAUTHORIZED
|
||||
fun Throwable.is401() = this is Failure.ServerError &&
|
||||
httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED && /* 401 */
|
||||
error.code == MatrixError.M_UNAUTHORIZED
|
||||
|
||||
fun Throwable.is404() =
|
||||
this is Failure.ServerError &&
|
||||
httpCode == HttpsURLConnection.HTTP_NOT_FOUND && /* 404 */
|
||||
error.code == MatrixError.M_NOT_FOUND
|
||||
fun Throwable.is404() = this is Failure.ServerError &&
|
||||
httpCode == HttpsURLConnection.HTTP_NOT_FOUND && /* 404 */
|
||||
error.code == MatrixError.M_NOT_FOUND
|
||||
|
||||
fun Throwable.isTokenError() =
|
||||
this is Failure.ServerError &&
|
||||
(error.code == MatrixError.M_UNKNOWN_TOKEN ||
|
||||
error.code == MatrixError.M_MISSING_TOKEN ||
|
||||
error.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT)
|
||||
fun Throwable.isTokenError() = this is Failure.ServerError &&
|
||||
(error.code == MatrixError.M_UNKNOWN_TOKEN ||
|
||||
error.code == MatrixError.M_MISSING_TOKEN ||
|
||||
error.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT)
|
||||
|
||||
fun Throwable.isLimitExceededError() =
|
||||
this is Failure.ServerError &&
|
||||
httpCode == 429 &&
|
||||
error.code == MatrixError.M_LIMIT_EXCEEDED
|
||||
fun Throwable.isLimitExceededError() = this is Failure.ServerError &&
|
||||
httpCode == 429 &&
|
||||
error.code == MatrixError.M_LIMIT_EXCEEDED
|
||||
|
||||
fun Throwable.shouldBeRetried(): Boolean {
|
||||
return this is Failure.NetworkConnection ||
|
||||
this is IOException ||
|
||||
this.isLimitExceededError()
|
||||
}
|
||||
fun Throwable.shouldBeRetried() = this is Failure.NetworkConnection ||
|
||||
this is IOException ||
|
||||
isLimitExceededError()
|
||||
|
||||
/**
|
||||
* Get the retry delay in case of rate limit exceeded error, adding 100 ms, of defaultValue otherwise
|
||||
|
@ -63,41 +58,33 @@ 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.isUsernameInUse() = 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.isInvalidUsername() = 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.isInvalidPassword() = 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.isRegistrationDisabled() = 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.isWeakPassword() = 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.isLoginEmailUnknown() = 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 &&
|
||||
error.flows != null
|
||||
}
|
||||
fun Throwable.isInvalidUIAAuth() = this is Failure.ServerError &&
|
||||
error.code == MatrixError.M_FORBIDDEN &&
|
||||
error.flows != null
|
||||
|
||||
fun Throwable.isHomeserverUnavailable() = this is Failure.NetworkConnection &&
|
||||
this.ioException is UnknownHostException
|
||||
|
||||
/**
|
||||
* Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible
|
||||
|
@ -129,13 +116,11 @@ fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? {
|
|||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
fun Throwable.isRegistrationAvailabilityError() = 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)
|
||||
|
||||
/**
|
||||
* Try to convert to a ScanFailure. Return null in the cases it's not possible
|
||||
|
|
|
@ -41,6 +41,7 @@ import im.vector.app.features.login.LoginMode
|
|||
import im.vector.app.features.login.ReAuthHelper
|
||||
import im.vector.app.features.login.ServerType
|
||||
import im.vector.app.features.login.SignMode
|
||||
import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -140,14 +141,14 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
is OnboardingAction.UpdateServerType -> handleUpdateServerType(action)
|
||||
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
|
||||
is OnboardingAction.InitWith -> handleInitWith(action)
|
||||
is OnboardingAction.HomeServerChange -> withAction(action) { handleHomeserverChange(action.homeServerUrl) }
|
||||
is OnboardingAction.HomeServerChange -> withAction(action) { handleHomeserverChange(action) }
|
||||
is OnboardingAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action }
|
||||
is OnboardingAction.Register -> handleRegisterWith(action).also { lastAction = action }
|
||||
is OnboardingAction.LoginWithToken -> handleLoginWithToken(action)
|
||||
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
|
||||
is OnboardingAction.ResetPassword -> handleResetPassword(action)
|
||||
is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
|
||||
is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction)
|
||||
is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction, ::emitFlowResultViewEvent)
|
||||
is OnboardingAction.ResetAction -> handleResetAction(action)
|
||||
is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action)
|
||||
OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory()
|
||||
|
@ -175,7 +176,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
|
||||
return when (val config = loginConfig.toHomeserverConfig()) {
|
||||
null -> continueToPageAfterSplash(onboardingFlow)
|
||||
else -> startAuthenticationFlow(config, ServerType.Other)
|
||||
else -> startAuthenticationFlow(trigger = null, config, ServerType.Other)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,9 +210,9 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
is OnboardingAction.HomeServerChange.SelectHomeServer -> {
|
||||
currentHomeServerConnectionConfig
|
||||
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
|
||||
?.let { startAuthenticationFlow(it) }
|
||||
?.let { startAuthenticationFlow(finalLastAction, it) }
|
||||
}
|
||||
is OnboardingAction.LoginOrRegister ->
|
||||
is OnboardingAction.LoginOrRegister ->
|
||||
handleDirectLogin(
|
||||
finalLastAction,
|
||||
HomeServerConnectionConfig.Builder()
|
||||
|
@ -220,7 +221,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
.withAllowedFingerPrints(listOf(action.fingerprint))
|
||||
.build()
|
||||
)
|
||||
else -> Unit
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,41 +256,52 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleRegisterAction(action: RegisterAction) {
|
||||
private fun handleRegisterAction(action: RegisterAction, onNextRegistrationStepAction: (FlowResult) -> Unit) {
|
||||
currentJob = viewModelScope.launch {
|
||||
if (action.hasLoadingState()) {
|
||||
setState { copy(isLoading = true) }
|
||||
}
|
||||
runCatching { registrationActionHandler.handleRegisterAction(registrationWizard, action) }
|
||||
.fold(
|
||||
onSuccess = {
|
||||
when {
|
||||
action.ignoresResult() -> {
|
||||
// do nothing
|
||||
}
|
||||
else -> when (it) {
|
||||
is RegistrationResult.Success -> onSessionCreated(it.session, isAccountCreated = true)
|
||||
is RegistrationResult.FlowResponse -> onFlowResponse(it.flowResult)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
if (it !is CancellationException) {
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(it))
|
||||
}
|
||||
}
|
||||
)
|
||||
internalRegisterAction(action, onNextRegistrationStepAction)
|
||||
setState { copy(isLoading = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun internalRegisterAction(action: RegisterAction, onNextRegistrationStepAction: (FlowResult) -> Unit) {
|
||||
runCatching { registrationActionHandler.handleRegisterAction(registrationWizard, action) }
|
||||
.fold(
|
||||
onSuccess = {
|
||||
when {
|
||||
action.ignoresResult() -> {
|
||||
// do nothing
|
||||
}
|
||||
else -> when (it) {
|
||||
is RegistrationResult.Success -> onSessionCreated(it.session, isAccountCreated = true)
|
||||
is RegistrationResult.FlowResponse -> onFlowResponse(it.flowResult, onNextRegistrationStepAction)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
if (it !is CancellationException) {
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(it))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun emitFlowResultViewEvent(flowResult: FlowResult) {
|
||||
_viewEvents.post(OnboardingViewEvents.RegistrationFlowResult(flowResult, isRegistrationStarted))
|
||||
}
|
||||
|
||||
private fun handleRegisterWith(action: OnboardingAction.Register) {
|
||||
reAuthHelper.data = action.password
|
||||
handleRegisterAction(RegisterAction.CreateAccount(
|
||||
action.username,
|
||||
action.password,
|
||||
action.initialDeviceName
|
||||
))
|
||||
handleRegisterAction(
|
||||
RegisterAction.CreateAccount(
|
||||
action.username,
|
||||
action.password,
|
||||
action.initialDeviceName
|
||||
),
|
||||
::emitFlowResultViewEvent
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleResetAction(action: OnboardingAction.ResetAction) {
|
||||
|
@ -337,20 +349,19 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun handleUpdateSignMode(action: OnboardingAction.UpdateSignMode) {
|
||||
setState {
|
||||
copy(
|
||||
signMode = action.signMode
|
||||
)
|
||||
}
|
||||
|
||||
updateSignMode(action.signMode)
|
||||
when (action.signMode) {
|
||||
SignMode.SignUp -> handleRegisterAction(RegisterAction.StartRegistration)
|
||||
SignMode.SignUp -> handleRegisterAction(RegisterAction.StartRegistration, ::emitFlowResultViewEvent)
|
||||
SignMode.SignIn -> startAuthenticationFlow()
|
||||
SignMode.SignInWithMatrixId -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId))
|
||||
SignMode.Unknown -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSignMode(signMode: SignMode) {
|
||||
setState { copy(signMode = signMode) }
|
||||
}
|
||||
|
||||
private fun handleUpdateUseCase(action: OnboardingAction.UpdateUseCase) {
|
||||
setState { copy(useCase = action.useCase) }
|
||||
when (vectorFeatures.isOnboardingCombinedRegisterEnabled()) {
|
||||
|
@ -509,18 +520,17 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
_viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignIn))
|
||||
}
|
||||
|
||||
private fun onFlowResponse(flowResult: FlowResult) {
|
||||
private suspend fun onFlowResponse(flowResult: FlowResult, onNextRegistrationStepAction: (FlowResult) -> Unit) {
|
||||
// If dummy stage is mandatory, and password is already sent, do the dummy stage now
|
||||
if (isRegistrationStarted && flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) {
|
||||
handleRegisterDummy()
|
||||
handleRegisterDummy(onNextRegistrationStepAction)
|
||||
} else {
|
||||
// Notify the user
|
||||
_viewEvents.post(OnboardingViewEvents.RegistrationFlowResult(flowResult, isRegistrationStarted))
|
||||
onNextRegistrationStepAction(flowResult)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRegisterDummy() {
|
||||
handleRegisterAction(RegisterAction.RegisterDummy)
|
||||
private suspend fun handleRegisterDummy(onNextRegistrationStepAction: (FlowResult) -> Unit) {
|
||||
internalRegisterAction(RegisterAction.RegisterDummy, onNextRegistrationStepAction)
|
||||
}
|
||||
|
||||
private suspend fun onSessionCreated(session: Session, isAccountCreated: Boolean) {
|
||||
|
@ -581,42 +591,87 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleHomeserverChange(homeserverUrl: String) {
|
||||
val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(homeserverUrl)
|
||||
private fun handleHomeserverChange(action: OnboardingAction.HomeServerChange) {
|
||||
val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl)
|
||||
if (homeServerConnectionConfig == null) {
|
||||
// This is invalid
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
|
||||
} else {
|
||||
startAuthenticationFlow(homeServerConnectionConfig)
|
||||
startAuthenticationFlow(action, homeServerConnectionConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAuthenticationFlow(homeServerConnectionConfig: HomeServerConnectionConfig, serverTypeOverride: ServerType? = null) {
|
||||
private fun startAuthenticationFlow(
|
||||
trigger: OnboardingAction?,
|
||||
homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
serverTypeOverride: ServerType? = null
|
||||
) {
|
||||
currentHomeServerConnectionConfig = homeServerConnectionConfig
|
||||
|
||||
currentJob = viewModelScope.launch {
|
||||
setState { copy(isLoading = true) }
|
||||
|
||||
runCatching { startAuthenticationFlowUseCase.execute(homeServerConnectionConfig) }.fold(
|
||||
onSuccess = {
|
||||
rememberHomeServer(homeServerConnectionConfig.homeServerUri.toString())
|
||||
if (it.isHomeserverOutdated) {
|
||||
_viewEvents.post(OnboardingViewEvents.OutdatedHomeserver)
|
||||
}
|
||||
onSuccess = { onAuthenticationStartedSuccess(trigger, homeServerConnectionConfig, it, serverTypeOverride) },
|
||||
onFailure = { _viewEvents.post(OnboardingViewEvents.Failure(it)) }
|
||||
)
|
||||
setState { copy(isLoading = false) }
|
||||
}
|
||||
}
|
||||
|
||||
setState {
|
||||
copy(
|
||||
serverType = alignServerTypeAfterSubmission(homeServerConnectionConfig, serverTypeOverride),
|
||||
selectedHomeserver = it.selectedHomeserver,
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
onAuthenticationStartedSuccess()
|
||||
},
|
||||
onFailure = {
|
||||
setState { copy(isLoading = false) }
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(it))
|
||||
private suspend fun onAuthenticationStartedSuccess(
|
||||
trigger: OnboardingAction?,
|
||||
config: HomeServerConnectionConfig,
|
||||
authResult: StartAuthenticationResult,
|
||||
serverTypeOverride: ServerType?
|
||||
) {
|
||||
rememberHomeServer(config.homeServerUri.toString())
|
||||
if (authResult.isHomeserverOutdated) {
|
||||
_viewEvents.post(OnboardingViewEvents.OutdatedHomeserver)
|
||||
}
|
||||
|
||||
when (trigger) {
|
||||
is OnboardingAction.HomeServerChange.EditHomeServer -> {
|
||||
when (awaitState().onboardingFlow) {
|
||||
OnboardingFlow.SignUp -> internalRegisterAction(RegisterAction.StartRegistration) { _ ->
|
||||
updateServerSelection(config, serverTypeOverride, authResult)
|
||||
_viewEvents.post(OnboardingViewEvents.OnHomeserverEdited)
|
||||
}
|
||||
else -> throw IllegalArgumentException("developer error")
|
||||
}
|
||||
}
|
||||
is OnboardingAction.HomeServerChange.SelectHomeServer -> {
|
||||
updateServerSelection(config, serverTypeOverride, authResult)
|
||||
if (authResult.selectedHomeserver.preferredLoginMode.supportsSignModeScreen()) {
|
||||
when (awaitState().onboardingFlow) {
|
||||
OnboardingFlow.SignIn -> {
|
||||
updateSignMode(SignMode.SignIn)
|
||||
internalRegisterAction(RegisterAction.StartRegistration, ::emitFlowResultViewEvent)
|
||||
}
|
||||
OnboardingFlow.SignUp -> {
|
||||
updateSignMode(SignMode.SignUp)
|
||||
internalRegisterAction(RegisterAction.StartRegistration, ::emitFlowResultViewEvent)
|
||||
}
|
||||
OnboardingFlow.SignInSignUp,
|
||||
null -> {
|
||||
_viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
updateServerSelection(config, serverTypeOverride, authResult)
|
||||
_viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateServerSelection(config: HomeServerConnectionConfig, serverTypeOverride: ServerType?, authResult: StartAuthenticationResult) {
|
||||
setState {
|
||||
copy(
|
||||
serverType = alignServerTypeAfterSubmission(config, serverTypeOverride),
|
||||
selectedHomeserver = authResult.selectedHomeserver,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -633,29 +688,6 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onAuthenticationStartedSuccess() {
|
||||
withState {
|
||||
when (lastAction) {
|
||||
is OnboardingAction.HomeServerChange.EditHomeServer -> _viewEvents.post(OnboardingViewEvents.OnHomeserverEdited)
|
||||
is OnboardingAction.HomeServerChange.SelectHomeServer -> {
|
||||
if (it.selectedHomeserver.preferredLoginMode.supportsSignModeScreen()) {
|
||||
when (it.onboardingFlow) {
|
||||
OnboardingFlow.SignIn -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignIn))
|
||||
OnboardingFlow.SignUp -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignUp))
|
||||
OnboardingFlow.SignInSignUp,
|
||||
null -> {
|
||||
_viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved)
|
||||
}
|
||||
}
|
||||
else -> _viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getInitialHomeServerUrl(): String? {
|
||||
return loginConfig?.homeServerUrl
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.content
|
||||
import im.vector.app.core.extensions.editText
|
||||
|
@ -33,6 +34,10 @@ import im.vector.app.databinding.FragmentFtueServerSelectionCombinedBinding
|
|||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentFtueServerSelectionCombinedBinding>() {
|
||||
|
@ -61,6 +66,9 @@ class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFt
|
|||
}
|
||||
views.chooseServerGetInTouch.debouncedClicks { openUrlInExternalBrowser(requireContext(), getString(R.string.ftue_ems_url)) }
|
||||
views.chooseServerSubmit.debouncedClicks { updateServerUrl() }
|
||||
views.chooseServerInput.editText().textChanges()
|
||||
.onEach { views.chooseServerInput.error = null }
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun updateServerUrl() {
|
||||
|
@ -78,5 +86,12 @@ class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFt
|
|||
}
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
views.chooseServerInput.error = when {
|
||||
throwable.isHomeserverUnavailable() -> getString(R.string.login_error_homeserver_not_found)
|
||||
else -> errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toReducedUrlKeepingSchemaIfInsecure() = toReducedUrl(keepSchema = this.startsWith("http://"))
|
||||
}
|
||||
|
|
|
@ -106,7 +106,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="text"
|
||||
android:inputType="textUri"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
|
|
@ -73,7 +73,6 @@ class OnboardingViewModelTest {
|
|||
|
||||
private val fakeUri = FakeUri()
|
||||
private val fakeContext = FakeContext()
|
||||
private val initialState = OnboardingViewState()
|
||||
private val fakeSession = FakeSession()
|
||||
private val fakeUriFilenameResolver = FakeUriFilenameResolver()
|
||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession)
|
||||
|
@ -85,11 +84,12 @@ class OnboardingViewModelTest {
|
|||
private val fakeStartAuthenticationFlowUseCase = FakeStartAuthenticationFlowUseCase()
|
||||
private val fakeHomeServerHistoryService = FakeHomeServerHistoryService()
|
||||
|
||||
lateinit var viewModel: OnboardingViewModel
|
||||
private var initialState = OnboardingViewState()
|
||||
private lateinit var viewModel: OnboardingViewModel
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
viewModel = createViewModel()
|
||||
viewModelWith(initialState)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -105,8 +105,7 @@ class OnboardingViewModelTest {
|
|||
|
||||
@Test
|
||||
fun `given supports changing display name, when handling PersonalizeProfile, then emits contents choose display name`() = runTest {
|
||||
val initialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = true, supportsChangingProfilePicture = false))
|
||||
viewModel = createViewModel(initialState)
|
||||
viewModelWith(initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = true, supportsChangingProfilePicture = false)))
|
||||
val test = viewModel.test()
|
||||
|
||||
viewModel.handle(OnboardingAction.PersonalizeProfile)
|
||||
|
@ -118,8 +117,7 @@ class OnboardingViewModelTest {
|
|||
|
||||
@Test
|
||||
fun `given only supports changing profile picture, when handling PersonalizeProfile, then emits contents choose profile picture`() = runTest {
|
||||
val initialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = false, supportsChangingProfilePicture = true))
|
||||
viewModel = createViewModel(initialState)
|
||||
viewModelWith(initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = false, supportsChangingProfilePicture = true)))
|
||||
val test = viewModel.test()
|
||||
|
||||
viewModel.handle(OnboardingAction.PersonalizeProfile)
|
||||
|
@ -131,8 +129,7 @@ class OnboardingViewModelTest {
|
|||
|
||||
@Test
|
||||
fun `given has sign in with matrix id sign mode, when handling login or register action, then logs in directly`() = runTest {
|
||||
val initialState = initialState.copy(signMode = SignMode.SignInWithMatrixId)
|
||||
viewModel = createViewModel(initialState)
|
||||
viewModelWith(initialState.copy(signMode = SignMode.SignInWithMatrixId))
|
||||
fakeDirectLoginUseCase.givenSuccessResult(A_LOGIN_OR_REGISTER_ACTION, config = null, result = fakeSession)
|
||||
givenInitialisesSession(fakeSession)
|
||||
val test = viewModel.test()
|
||||
|
@ -151,8 +148,7 @@ class OnboardingViewModelTest {
|
|||
|
||||
@Test
|
||||
fun `given has sign in with matrix id sign mode, when handling login or register action fails, then emits error`() = runTest {
|
||||
val initialState = initialState.copy(signMode = SignMode.SignInWithMatrixId)
|
||||
viewModel = createViewModel(initialState)
|
||||
viewModelWith(initialState.copy(signMode = SignMode.SignInWithMatrixId))
|
||||
fakeDirectLoginUseCase.givenFailureResult(A_LOGIN_OR_REGISTER_ACTION, config = null, cause = AN_ERROR)
|
||||
givenInitialisesSession(fakeSession)
|
||||
val test = viewModel.test()
|
||||
|
@ -235,11 +231,13 @@ class OnboardingViewModelTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `given when editing homeserver, then updates selected homeserver state and emits edited event`() = runTest {
|
||||
val test = viewModel.test()
|
||||
fun `given in the sign up flow, when editing homeserver, then updates selected homeserver state and emits edited event`() = runTest {
|
||||
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp))
|
||||
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG)
|
||||
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(false, SELECTED_HOMESERVER_STATE))
|
||||
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, SELECTED_HOMESERVER_STATE))
|
||||
givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationResult.FlowResponse(AN_IGNORED_FLOW_RESULT))
|
||||
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
|
||||
val test = viewModel.test()
|
||||
|
||||
viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL))
|
||||
|
||||
|
@ -247,12 +245,35 @@ class OnboardingViewModelTest {
|
|||
.assertStatesChanges(
|
||||
initialState,
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false, selectedHomeserver = SELECTED_HOMESERVER_STATE) },
|
||||
{ copy(selectedHomeserver = SELECTED_HOMESERVER_STATE) },
|
||||
{ copy(isLoading = false) }
|
||||
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.OnHomeserverEdited)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given in the sign up flow, when editing homeserver errors, then does not update the selected homeserver state and emits error`() = runTest {
|
||||
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp))
|
||||
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG)
|
||||
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, SELECTED_HOMESERVER_STATE))
|
||||
givenRegistrationActionErrors(RegisterAction.StartRegistration, AN_ERROR)
|
||||
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
|
||||
val test = viewModel.test()
|
||||
|
||||
viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL))
|
||||
|
||||
test
|
||||
.assertStatesChanges(
|
||||
initialState,
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false) }
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.Failure(AN_ERROR))
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given personalisation enabled, when registering account, then updates state and emits account created event`() = runTest {
|
||||
fakeVectorFeatures.givenPersonalisationEnabled()
|
||||
|
@ -292,14 +313,13 @@ class OnboardingViewModelTest {
|
|||
|
||||
@Test
|
||||
fun `given changing profile picture is supported, when updating display name, then updates upstream user display name and moves to choose profile picture`() = runTest {
|
||||
val personalisedInitialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = true))
|
||||
viewModel = createViewModel(personalisedInitialState)
|
||||
viewModelWith(initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = true)))
|
||||
val test = viewModel.test()
|
||||
|
||||
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
|
||||
|
||||
test
|
||||
.assertStatesChanges(personalisedInitialState, expectedSuccessfulDisplayNameUpdateStates())
|
||||
.assertStatesChanges(initialState, expectedSuccessfulDisplayNameUpdateStates())
|
||||
.assertEvents(OnboardingViewEvents.OnChooseProfilePicture)
|
||||
.finish()
|
||||
fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME)
|
||||
|
@ -307,14 +327,13 @@ class OnboardingViewModelTest {
|
|||
|
||||
@Test
|
||||
fun `given changing profile picture is not supported, when updating display name, then updates upstream user display name and completes personalization`() = runTest {
|
||||
val personalisedInitialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = false))
|
||||
viewModel = createViewModel(personalisedInitialState)
|
||||
viewModelWith(initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = false)))
|
||||
val test = viewModel.test()
|
||||
|
||||
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
|
||||
|
||||
test
|
||||
.assertStatesChanges(personalisedInitialState, expectedSuccessfulDisplayNameUpdateStates())
|
||||
.assertStatesChanges(initialState, expectedSuccessfulDisplayNameUpdateStates())
|
||||
.assertEvents(OnboardingViewEvents.OnPersonalizationComplete)
|
||||
.finish()
|
||||
fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME)
|
||||
|
@ -354,14 +373,13 @@ class OnboardingViewModelTest {
|
|||
|
||||
@Test
|
||||
fun `given a selected picture, when handling save selected profile picture, then updates upstream avatar and completes personalization`() = runTest {
|
||||
val initialStateWithPicture = givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME)
|
||||
viewModel = createViewModel(initialStateWithPicture)
|
||||
viewModelWith(givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME))
|
||||
val test = viewModel.test()
|
||||
|
||||
viewModel.handle(OnboardingAction.SaveSelectedProfilePicture)
|
||||
|
||||
test
|
||||
.assertStates(expectedProfilePictureSuccessStates(initialStateWithPicture))
|
||||
.assertStates(expectedProfilePictureSuccessStates(initialState))
|
||||
.assertEvents(OnboardingViewEvents.OnPersonalizationComplete)
|
||||
.finish()
|
||||
fakeSession.fakeProfileService.verifyAvatarUpdated(fakeSession.myUserId, fakeUri.instance, A_PICTURE_FILENAME)
|
||||
|
@ -370,14 +388,13 @@ class OnboardingViewModelTest {
|
|||
@Test
|
||||
fun `given upstream update avatar fails, when saving selected profile picture, then emits failure event`() = runTest {
|
||||
fakeSession.fakeProfileService.givenUpdateAvatarErrors(AN_ERROR)
|
||||
val initialStateWithPicture = givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME)
|
||||
viewModel = createViewModel(initialStateWithPicture)
|
||||
viewModelWith(givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME))
|
||||
val test = viewModel.test()
|
||||
|
||||
viewModel.handle(OnboardingAction.SaveSelectedProfilePicture)
|
||||
|
||||
test
|
||||
.assertStates(expectedProfilePictureFailureStates(initialStateWithPicture))
|
||||
.assertStates(expectedProfilePictureFailureStates(initialState))
|
||||
.assertEvents(OnboardingViewEvents.Failure(AN_ERROR))
|
||||
.finish()
|
||||
}
|
||||
|
@ -406,8 +423,8 @@ class OnboardingViewModelTest {
|
|||
.finish()
|
||||
}
|
||||
|
||||
private fun createViewModel(state: OnboardingViewState = initialState): OnboardingViewModel {
|
||||
return OnboardingViewModel(
|
||||
private fun viewModelWith(state: OnboardingViewState) {
|
||||
OnboardingViewModel(
|
||||
state,
|
||||
fakeContext.instance,
|
||||
fakeAuthenticationService,
|
||||
|
@ -423,7 +440,10 @@ class OnboardingViewModelTest {
|
|||
fakeDirectLoginUseCase.instance,
|
||||
fakeStartAuthenticationFlowUseCase.instance,
|
||||
FakeVectorOverrides()
|
||||
)
|
||||
).also {
|
||||
viewModel = it
|
||||
initialState = state
|
||||
}
|
||||
}
|
||||
|
||||
private fun givenPictureSelected(fileUri: Uri, filename: String): OnboardingViewState {
|
||||
|
@ -481,6 +501,12 @@ class OnboardingViewModelTest {
|
|||
fakeAuthenticationService.givenRegistrationWizard(registrationWizard)
|
||||
fakeRegisterActionHandler.givenResultsFor(registrationWizard, results)
|
||||
}
|
||||
|
||||
private fun givenRegistrationActionErrors(action: RegisterAction, cause: Throwable) {
|
||||
val registrationWizard = FakeRegistrationWizard()
|
||||
fakeAuthenticationService.givenRegistrationWizard(registrationWizard)
|
||||
fakeRegisterActionHandler.givenThrowsFor(registrationWizard, action, cause)
|
||||
}
|
||||
}
|
||||
|
||||
private fun HomeServerCapabilities.toPersonalisationState() = PersonalizationState(
|
||||
|
|
|
@ -33,4 +33,8 @@ class FakeRegisterActionHandler {
|
|||
result.first { it.first == actionArg }.second
|
||||
}
|
||||
}
|
||||
|
||||
fun givenThrowsFor(wizard: RegistrationWizard, action: RegisterAction, cause: Throwable) {
|
||||
coEvery { instance.handleRegisterAction(wizard, action) } throws cause
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue