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.api.session.contentscanner.ScanFailure
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.net.UnknownHostException
|
||||||
import javax.net.ssl.HttpsURLConnection
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
|
||||||
fun Throwable.is401() =
|
fun Throwable.is401() = this is Failure.ServerError &&
|
||||||
this is Failure.ServerError &&
|
httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED && /* 401 */
|
||||||
httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED && /* 401 */
|
error.code == MatrixError.M_UNAUTHORIZED
|
||||||
error.code == MatrixError.M_UNAUTHORIZED
|
|
||||||
|
|
||||||
fun Throwable.is404() =
|
fun Throwable.is404() = this is Failure.ServerError &&
|
||||||
this is Failure.ServerError &&
|
httpCode == HttpsURLConnection.HTTP_NOT_FOUND && /* 404 */
|
||||||
httpCode == HttpsURLConnection.HTTP_NOT_FOUND && /* 404 */
|
error.code == MatrixError.M_NOT_FOUND
|
||||||
error.code == MatrixError.M_NOT_FOUND
|
|
||||||
|
|
||||||
fun Throwable.isTokenError() =
|
fun Throwable.isTokenError() = this is Failure.ServerError &&
|
||||||
this is Failure.ServerError &&
|
(error.code == MatrixError.M_UNKNOWN_TOKEN ||
|
||||||
(error.code == MatrixError.M_UNKNOWN_TOKEN ||
|
error.code == MatrixError.M_MISSING_TOKEN ||
|
||||||
error.code == MatrixError.M_MISSING_TOKEN ||
|
error.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT)
|
||||||
error.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT)
|
|
||||||
|
|
||||||
fun Throwable.isLimitExceededError() =
|
fun Throwable.isLimitExceededError() = this is Failure.ServerError &&
|
||||||
this is Failure.ServerError &&
|
httpCode == 429 &&
|
||||||
httpCode == 429 &&
|
error.code == MatrixError.M_LIMIT_EXCEEDED
|
||||||
error.code == MatrixError.M_LIMIT_EXCEEDED
|
|
||||||
|
|
||||||
fun Throwable.shouldBeRetried(): Boolean {
|
fun Throwable.shouldBeRetried() = this is Failure.NetworkConnection ||
|
||||||
return this is Failure.NetworkConnection ||
|
this is IOException ||
|
||||||
this is IOException ||
|
isLimitExceededError()
|
||||||
this.isLimitExceededError()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the retry delay in case of rate limit exceeded error, adding 100 ms, of defaultValue otherwise
|
* 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
|
?: defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Throwable.isUsernameInUse(): Boolean {
|
fun Throwable.isUsernameInUse() = this is Failure.ServerError &&
|
||||||
return this is Failure.ServerError && error.code == MatrixError.M_USER_IN_USE
|
error.code == MatrixError.M_USER_IN_USE
|
||||||
}
|
|
||||||
|
|
||||||
fun Throwable.isInvalidUsername(): Boolean {
|
fun Throwable.isInvalidUsername() = this is Failure.ServerError &&
|
||||||
return this is Failure.ServerError &&
|
error.code == MatrixError.M_INVALID_USERNAME
|
||||||
error.code == MatrixError.M_INVALID_USERNAME
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Throwable.isInvalidPassword(): Boolean {
|
fun Throwable.isInvalidPassword() = this is Failure.ServerError &&
|
||||||
return this is Failure.ServerError &&
|
error.code == MatrixError.M_FORBIDDEN &&
|
||||||
error.code == MatrixError.M_FORBIDDEN &&
|
error.message == "Invalid password"
|
||||||
error.message == "Invalid password"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Throwable.isRegistrationDisabled(): Boolean {
|
fun Throwable.isRegistrationDisabled() = this is Failure.ServerError &&
|
||||||
return this is Failure.ServerError && error.code == MatrixError.M_FORBIDDEN &&
|
error.code == MatrixError.M_FORBIDDEN &&
|
||||||
httpCode == HttpsURLConnection.HTTP_FORBIDDEN
|
httpCode == HttpsURLConnection.HTTP_FORBIDDEN
|
||||||
}
|
|
||||||
|
|
||||||
fun Throwable.isWeakPassword(): Boolean {
|
fun Throwable.isWeakPassword() = this is Failure.ServerError &&
|
||||||
return this is Failure.ServerError && error.code == MatrixError.M_WEAK_PASSWORD
|
error.code == MatrixError.M_WEAK_PASSWORD
|
||||||
}
|
|
||||||
|
|
||||||
fun Throwable.isLoginEmailUnknown(): Boolean {
|
fun Throwable.isLoginEmailUnknown() = this is Failure.ServerError &&
|
||||||
return this is Failure.ServerError &&
|
error.code == MatrixError.M_FORBIDDEN &&
|
||||||
error.code == MatrixError.M_FORBIDDEN &&
|
error.message.isEmpty()
|
||||||
error.message.isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Throwable.isInvalidUIAAuth(): Boolean {
|
fun Throwable.isInvalidUIAAuth() = this is Failure.ServerError &&
|
||||||
return this is Failure.ServerError &&
|
error.code == MatrixError.M_FORBIDDEN &&
|
||||||
error.code == MatrixError.M_FORBIDDEN &&
|
error.flows != null
|
||||||
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
|
* 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 {
|
fun Throwable.isRegistrationAvailabilityError() = this is Failure.ServerError &&
|
||||||
return this is Failure.ServerError &&
|
httpCode == HttpsURLConnection.HTTP_BAD_REQUEST && /* 400 */
|
||||||
httpCode == HttpsURLConnection.HTTP_BAD_REQUEST && /* 400 */
|
(error.code == MatrixError.M_USER_IN_USE ||
|
||||||
(error.code == MatrixError.M_USER_IN_USE ||
|
error.code == MatrixError.M_INVALID_USERNAME ||
|
||||||
error.code == MatrixError.M_INVALID_USERNAME ||
|
error.code == MatrixError.M_EXCLUSIVE)
|
||||||
error.code == MatrixError.M_EXCLUSIVE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to convert to a ScanFailure. Return null in the cases it's not possible
|
* 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.ReAuthHelper
|
||||||
import im.vector.app.features.login.ServerType
|
import im.vector.app.features.login.ServerType
|
||||||
import im.vector.app.features.login.SignMode
|
import im.vector.app.features.login.SignMode
|
||||||
|
import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -140,14 +141,14 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
is OnboardingAction.UpdateServerType -> handleUpdateServerType(action)
|
is OnboardingAction.UpdateServerType -> handleUpdateServerType(action)
|
||||||
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
|
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
|
||||||
is OnboardingAction.InitWith -> handleInitWith(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.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action }
|
||||||
is OnboardingAction.Register -> handleRegisterWith(action).also { lastAction = action }
|
is OnboardingAction.Register -> handleRegisterWith(action).also { lastAction = action }
|
||||||
is OnboardingAction.LoginWithToken -> handleLoginWithToken(action)
|
is OnboardingAction.LoginWithToken -> handleLoginWithToken(action)
|
||||||
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
|
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
|
||||||
is OnboardingAction.ResetPassword -> handleResetPassword(action)
|
is OnboardingAction.ResetPassword -> handleResetPassword(action)
|
||||||
is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
|
is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
|
||||||
is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction)
|
is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction, ::emitFlowResultViewEvent)
|
||||||
is OnboardingAction.ResetAction -> handleResetAction(action)
|
is OnboardingAction.ResetAction -> handleResetAction(action)
|
||||||
is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action)
|
is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action)
|
||||||
OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory()
|
OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory()
|
||||||
|
@ -175,7 +176,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
return when (val config = loginConfig.toHomeserverConfig()) {
|
return when (val config = loginConfig.toHomeserverConfig()) {
|
||||||
null -> continueToPageAfterSplash(onboardingFlow)
|
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 -> {
|
is OnboardingAction.HomeServerChange.SelectHomeServer -> {
|
||||||
currentHomeServerConnectionConfig
|
currentHomeServerConnectionConfig
|
||||||
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
|
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
|
||||||
?.let { startAuthenticationFlow(it) }
|
?.let { startAuthenticationFlow(finalLastAction, it) }
|
||||||
}
|
}
|
||||||
is OnboardingAction.LoginOrRegister ->
|
is OnboardingAction.LoginOrRegister ->
|
||||||
handleDirectLogin(
|
handleDirectLogin(
|
||||||
finalLastAction,
|
finalLastAction,
|
||||||
HomeServerConnectionConfig.Builder()
|
HomeServerConnectionConfig.Builder()
|
||||||
|
@ -220,7 +221,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
.withAllowedFingerPrints(listOf(action.fingerprint))
|
.withAllowedFingerPrints(listOf(action.fingerprint))
|
||||||
.build()
|
.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 {
|
currentJob = viewModelScope.launch {
|
||||||
if (action.hasLoadingState()) {
|
if (action.hasLoadingState()) {
|
||||||
setState { copy(isLoading = true) }
|
setState { copy(isLoading = true) }
|
||||||
}
|
}
|
||||||
runCatching { registrationActionHandler.handleRegisterAction(registrationWizard, action) }
|
internalRegisterAction(action, onNextRegistrationStepAction)
|
||||||
.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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
setState { copy(isLoading = false) }
|
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) {
|
private fun handleRegisterWith(action: OnboardingAction.Register) {
|
||||||
reAuthHelper.data = action.password
|
reAuthHelper.data = action.password
|
||||||
handleRegisterAction(RegisterAction.CreateAccount(
|
handleRegisterAction(
|
||||||
action.username,
|
RegisterAction.CreateAccount(
|
||||||
action.password,
|
action.username,
|
||||||
action.initialDeviceName
|
action.password,
|
||||||
))
|
action.initialDeviceName
|
||||||
|
),
|
||||||
|
::emitFlowResultViewEvent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleResetAction(action: OnboardingAction.ResetAction) {
|
private fun handleResetAction(action: OnboardingAction.ResetAction) {
|
||||||
|
@ -337,20 +349,19 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUpdateSignMode(action: OnboardingAction.UpdateSignMode) {
|
private fun handleUpdateSignMode(action: OnboardingAction.UpdateSignMode) {
|
||||||
setState {
|
updateSignMode(action.signMode)
|
||||||
copy(
|
|
||||||
signMode = action.signMode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
when (action.signMode) {
|
when (action.signMode) {
|
||||||
SignMode.SignUp -> handleRegisterAction(RegisterAction.StartRegistration)
|
SignMode.SignUp -> handleRegisterAction(RegisterAction.StartRegistration, ::emitFlowResultViewEvent)
|
||||||
SignMode.SignIn -> startAuthenticationFlow()
|
SignMode.SignIn -> startAuthenticationFlow()
|
||||||
SignMode.SignInWithMatrixId -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId))
|
SignMode.SignInWithMatrixId -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId))
|
||||||
SignMode.Unknown -> Unit
|
SignMode.Unknown -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateSignMode(signMode: SignMode) {
|
||||||
|
setState { copy(signMode = signMode) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleUpdateUseCase(action: OnboardingAction.UpdateUseCase) {
|
private fun handleUpdateUseCase(action: OnboardingAction.UpdateUseCase) {
|
||||||
setState { copy(useCase = action.useCase) }
|
setState { copy(useCase = action.useCase) }
|
||||||
when (vectorFeatures.isOnboardingCombinedRegisterEnabled()) {
|
when (vectorFeatures.isOnboardingCombinedRegisterEnabled()) {
|
||||||
|
@ -509,18 +520,17 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
_viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignIn))
|
_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 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 }) {
|
if (isRegistrationStarted && flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) {
|
||||||
handleRegisterDummy()
|
handleRegisterDummy(onNextRegistrationStepAction)
|
||||||
} else {
|
} else {
|
||||||
// Notify the user
|
onNextRegistrationStepAction(flowResult)
|
||||||
_viewEvents.post(OnboardingViewEvents.RegistrationFlowResult(flowResult, isRegistrationStarted))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleRegisterDummy() {
|
private suspend fun handleRegisterDummy(onNextRegistrationStepAction: (FlowResult) -> Unit) {
|
||||||
handleRegisterAction(RegisterAction.RegisterDummy)
|
internalRegisterAction(RegisterAction.RegisterDummy, onNextRegistrationStepAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onSessionCreated(session: Session, isAccountCreated: Boolean) {
|
private suspend fun onSessionCreated(session: Session, isAccountCreated: Boolean) {
|
||||||
|
@ -581,42 +591,87 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleHomeserverChange(homeserverUrl: String) {
|
private fun handleHomeserverChange(action: OnboardingAction.HomeServerChange) {
|
||||||
val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(homeserverUrl)
|
val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl)
|
||||||
if (homeServerConnectionConfig == null) {
|
if (homeServerConnectionConfig == null) {
|
||||||
// This is invalid
|
// This is invalid
|
||||||
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
|
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
|
||||||
} else {
|
} 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
|
currentHomeServerConnectionConfig = homeServerConnectionConfig
|
||||||
|
|
||||||
currentJob = viewModelScope.launch {
|
currentJob = viewModelScope.launch {
|
||||||
setState { copy(isLoading = true) }
|
setState { copy(isLoading = true) }
|
||||||
|
|
||||||
runCatching { startAuthenticationFlowUseCase.execute(homeServerConnectionConfig) }.fold(
|
runCatching { startAuthenticationFlowUseCase.execute(homeServerConnectionConfig) }.fold(
|
||||||
onSuccess = {
|
onSuccess = { onAuthenticationStartedSuccess(trigger, homeServerConnectionConfig, it, serverTypeOverride) },
|
||||||
rememberHomeServer(homeServerConnectionConfig.homeServerUri.toString())
|
onFailure = { _viewEvents.post(OnboardingViewEvents.Failure(it)) }
|
||||||
if (it.isHomeserverOutdated) {
|
)
|
||||||
_viewEvents.post(OnboardingViewEvents.OutdatedHomeserver)
|
setState { copy(isLoading = false) }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setState {
|
private suspend fun onAuthenticationStartedSuccess(
|
||||||
copy(
|
trigger: OnboardingAction?,
|
||||||
serverType = alignServerTypeAfterSubmission(homeServerConnectionConfig, serverTypeOverride),
|
config: HomeServerConnectionConfig,
|
||||||
selectedHomeserver = it.selectedHomeserver,
|
authResult: StartAuthenticationResult,
|
||||||
isLoading = false,
|
serverTypeOverride: ServerType?
|
||||||
)
|
) {
|
||||||
}
|
rememberHomeServer(config.homeServerUri.toString())
|
||||||
onAuthenticationStartedSuccess()
|
if (authResult.isHomeserverOutdated) {
|
||||||
},
|
_viewEvents.post(OnboardingViewEvents.OutdatedHomeserver)
|
||||||
onFailure = {
|
}
|
||||||
setState { copy(isLoading = false) }
|
|
||||||
_viewEvents.post(OnboardingViewEvents.Failure(it))
|
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? {
|
fun getInitialHomeServerUrl(): String? {
|
||||||
return loginConfig?.homeServerUrl
|
return loginConfig?.homeServerUrl
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.content
|
import im.vector.app.core.extensions.content
|
||||||
import im.vector.app.core.extensions.editText
|
import im.vector.app.core.extensions.editText
|
||||||
|
@ -33,6 +34,10 @@ import im.vector.app.databinding.FragmentFtueServerSelectionCombinedBinding
|
||||||
import im.vector.app.features.onboarding.OnboardingAction
|
import im.vector.app.features.onboarding.OnboardingAction
|
||||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||||
import im.vector.app.features.onboarding.OnboardingViewState
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentFtueServerSelectionCombinedBinding>() {
|
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.chooseServerGetInTouch.debouncedClicks { openUrlInExternalBrowser(requireContext(), getString(R.string.ftue_ems_url)) }
|
||||||
views.chooseServerSubmit.debouncedClicks { updateServerUrl() }
|
views.chooseServerSubmit.debouncedClicks { updateServerUrl() }
|
||||||
|
views.chooseServerInput.editText().textChanges()
|
||||||
|
.onEach { views.chooseServerInput.error = null }
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateServerUrl() {
|
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://"))
|
private fun String.toReducedUrlKeepingSchemaIfInsecure() = toReducedUrl(keepSchema = this.startsWith("http://"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,7 +106,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:imeOptions="actionDone"
|
android:imeOptions="actionDone"
|
||||||
android:inputType="text"
|
android:inputType="textUri"
|
||||||
android:maxLines="1" />
|
android:maxLines="1" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
|
@ -73,7 +73,6 @@ class OnboardingViewModelTest {
|
||||||
|
|
||||||
private val fakeUri = FakeUri()
|
private val fakeUri = FakeUri()
|
||||||
private val fakeContext = FakeContext()
|
private val fakeContext = FakeContext()
|
||||||
private val initialState = OnboardingViewState()
|
|
||||||
private val fakeSession = FakeSession()
|
private val fakeSession = FakeSession()
|
||||||
private val fakeUriFilenameResolver = FakeUriFilenameResolver()
|
private val fakeUriFilenameResolver = FakeUriFilenameResolver()
|
||||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession)
|
private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession)
|
||||||
|
@ -85,11 +84,12 @@ class OnboardingViewModelTest {
|
||||||
private val fakeStartAuthenticationFlowUseCase = FakeStartAuthenticationFlowUseCase()
|
private val fakeStartAuthenticationFlowUseCase = FakeStartAuthenticationFlowUseCase()
|
||||||
private val fakeHomeServerHistoryService = FakeHomeServerHistoryService()
|
private val fakeHomeServerHistoryService = FakeHomeServerHistoryService()
|
||||||
|
|
||||||
lateinit var viewModel: OnboardingViewModel
|
private var initialState = OnboardingViewState()
|
||||||
|
private lateinit var viewModel: OnboardingViewModel
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
viewModel = createViewModel()
|
viewModelWith(initialState)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -105,8 +105,7 @@ class OnboardingViewModelTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given supports changing display name, when handling PersonalizeProfile, then emits contents choose display name`() = runTest {
|
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))
|
viewModelWith(initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = true, supportsChangingProfilePicture = false)))
|
||||||
viewModel = createViewModel(initialState)
|
|
||||||
val test = viewModel.test()
|
val test = viewModel.test()
|
||||||
|
|
||||||
viewModel.handle(OnboardingAction.PersonalizeProfile)
|
viewModel.handle(OnboardingAction.PersonalizeProfile)
|
||||||
|
@ -118,8 +117,7 @@ class OnboardingViewModelTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given only supports changing profile picture, when handling PersonalizeProfile, then emits contents choose profile picture`() = runTest {
|
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))
|
viewModelWith(initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = false, supportsChangingProfilePicture = true)))
|
||||||
viewModel = createViewModel(initialState)
|
|
||||||
val test = viewModel.test()
|
val test = viewModel.test()
|
||||||
|
|
||||||
viewModel.handle(OnboardingAction.PersonalizeProfile)
|
viewModel.handle(OnboardingAction.PersonalizeProfile)
|
||||||
|
@ -131,8 +129,7 @@ class OnboardingViewModelTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given has sign in with matrix id sign mode, when handling login or register action, then logs in directly`() = runTest {
|
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)
|
viewModelWith(initialState.copy(signMode = SignMode.SignInWithMatrixId))
|
||||||
viewModel = createViewModel(initialState)
|
|
||||||
fakeDirectLoginUseCase.givenSuccessResult(A_LOGIN_OR_REGISTER_ACTION, config = null, result = fakeSession)
|
fakeDirectLoginUseCase.givenSuccessResult(A_LOGIN_OR_REGISTER_ACTION, config = null, result = fakeSession)
|
||||||
givenInitialisesSession(fakeSession)
|
givenInitialisesSession(fakeSession)
|
||||||
val test = viewModel.test()
|
val test = viewModel.test()
|
||||||
|
@ -151,8 +148,7 @@ class OnboardingViewModelTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given has sign in with matrix id sign mode, when handling login or register action fails, then emits error`() = runTest {
|
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)
|
viewModelWith(initialState.copy(signMode = SignMode.SignInWithMatrixId))
|
||||||
viewModel = createViewModel(initialState)
|
|
||||||
fakeDirectLoginUseCase.givenFailureResult(A_LOGIN_OR_REGISTER_ACTION, config = null, cause = AN_ERROR)
|
fakeDirectLoginUseCase.givenFailureResult(A_LOGIN_OR_REGISTER_ACTION, config = null, cause = AN_ERROR)
|
||||||
givenInitialisesSession(fakeSession)
|
givenInitialisesSession(fakeSession)
|
||||||
val test = viewModel.test()
|
val test = viewModel.test()
|
||||||
|
@ -235,11 +231,13 @@ class OnboardingViewModelTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given when editing homeserver, then updates selected homeserver state and emits edited event`() = runTest {
|
fun `given in the sign up flow, when editing homeserver, then updates selected homeserver state and emits edited event`() = runTest {
|
||||||
val test = viewModel.test()
|
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp))
|
||||||
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG)
|
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())
|
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
|
||||||
|
val test = viewModel.test()
|
||||||
|
|
||||||
viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL))
|
viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL))
|
||||||
|
|
||||||
|
@ -247,12 +245,35 @@ class OnboardingViewModelTest {
|
||||||
.assertStatesChanges(
|
.assertStatesChanges(
|
||||||
initialState,
|
initialState,
|
||||||
{ copy(isLoading = true) },
|
{ copy(isLoading = true) },
|
||||||
{ copy(isLoading = false, selectedHomeserver = SELECTED_HOMESERVER_STATE) },
|
{ copy(selectedHomeserver = SELECTED_HOMESERVER_STATE) },
|
||||||
|
{ copy(isLoading = false) }
|
||||||
|
|
||||||
)
|
)
|
||||||
.assertEvents(OnboardingViewEvents.OnHomeserverEdited)
|
.assertEvents(OnboardingViewEvents.OnHomeserverEdited)
|
||||||
.finish()
|
.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
|
@Test
|
||||||
fun `given personalisation enabled, when registering account, then updates state and emits account created event`() = runTest {
|
fun `given personalisation enabled, when registering account, then updates state and emits account created event`() = runTest {
|
||||||
fakeVectorFeatures.givenPersonalisationEnabled()
|
fakeVectorFeatures.givenPersonalisationEnabled()
|
||||||
|
@ -292,14 +313,13 @@ class OnboardingViewModelTest {
|
||||||
|
|
||||||
@Test
|
@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 {
|
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))
|
viewModelWith(initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = true)))
|
||||||
viewModel = createViewModel(personalisedInitialState)
|
|
||||||
val test = viewModel.test()
|
val test = viewModel.test()
|
||||||
|
|
||||||
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
|
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
|
||||||
|
|
||||||
test
|
test
|
||||||
.assertStatesChanges(personalisedInitialState, expectedSuccessfulDisplayNameUpdateStates())
|
.assertStatesChanges(initialState, expectedSuccessfulDisplayNameUpdateStates())
|
||||||
.assertEvents(OnboardingViewEvents.OnChooseProfilePicture)
|
.assertEvents(OnboardingViewEvents.OnChooseProfilePicture)
|
||||||
.finish()
|
.finish()
|
||||||
fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME)
|
fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME)
|
||||||
|
@ -307,14 +327,13 @@ class OnboardingViewModelTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given changing profile picture is not supported, when updating display name, then updates upstream user display name and completes personalization`() = runTest {
|
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))
|
viewModelWith(initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = false)))
|
||||||
viewModel = createViewModel(personalisedInitialState)
|
|
||||||
val test = viewModel.test()
|
val test = viewModel.test()
|
||||||
|
|
||||||
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
|
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
|
||||||
|
|
||||||
test
|
test
|
||||||
.assertStatesChanges(personalisedInitialState, expectedSuccessfulDisplayNameUpdateStates())
|
.assertStatesChanges(initialState, expectedSuccessfulDisplayNameUpdateStates())
|
||||||
.assertEvents(OnboardingViewEvents.OnPersonalizationComplete)
|
.assertEvents(OnboardingViewEvents.OnPersonalizationComplete)
|
||||||
.finish()
|
.finish()
|
||||||
fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME)
|
fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME)
|
||||||
|
@ -354,14 +373,13 @@ class OnboardingViewModelTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given a selected picture, when handling save selected profile picture, then updates upstream avatar and completes personalization`() = runTest {
|
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)
|
viewModelWith(givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME))
|
||||||
viewModel = createViewModel(initialStateWithPicture)
|
|
||||||
val test = viewModel.test()
|
val test = viewModel.test()
|
||||||
|
|
||||||
viewModel.handle(OnboardingAction.SaveSelectedProfilePicture)
|
viewModel.handle(OnboardingAction.SaveSelectedProfilePicture)
|
||||||
|
|
||||||
test
|
test
|
||||||
.assertStates(expectedProfilePictureSuccessStates(initialStateWithPicture))
|
.assertStates(expectedProfilePictureSuccessStates(initialState))
|
||||||
.assertEvents(OnboardingViewEvents.OnPersonalizationComplete)
|
.assertEvents(OnboardingViewEvents.OnPersonalizationComplete)
|
||||||
.finish()
|
.finish()
|
||||||
fakeSession.fakeProfileService.verifyAvatarUpdated(fakeSession.myUserId, fakeUri.instance, A_PICTURE_FILENAME)
|
fakeSession.fakeProfileService.verifyAvatarUpdated(fakeSession.myUserId, fakeUri.instance, A_PICTURE_FILENAME)
|
||||||
|
@ -370,14 +388,13 @@ class OnboardingViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given upstream update avatar fails, when saving selected profile picture, then emits failure event`() = runTest {
|
fun `given upstream update avatar fails, when saving selected profile picture, then emits failure event`() = runTest {
|
||||||
fakeSession.fakeProfileService.givenUpdateAvatarErrors(AN_ERROR)
|
fakeSession.fakeProfileService.givenUpdateAvatarErrors(AN_ERROR)
|
||||||
val initialStateWithPicture = givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME)
|
viewModelWith(givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME))
|
||||||
viewModel = createViewModel(initialStateWithPicture)
|
|
||||||
val test = viewModel.test()
|
val test = viewModel.test()
|
||||||
|
|
||||||
viewModel.handle(OnboardingAction.SaveSelectedProfilePicture)
|
viewModel.handle(OnboardingAction.SaveSelectedProfilePicture)
|
||||||
|
|
||||||
test
|
test
|
||||||
.assertStates(expectedProfilePictureFailureStates(initialStateWithPicture))
|
.assertStates(expectedProfilePictureFailureStates(initialState))
|
||||||
.assertEvents(OnboardingViewEvents.Failure(AN_ERROR))
|
.assertEvents(OnboardingViewEvents.Failure(AN_ERROR))
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
@ -406,8 +423,8 @@ class OnboardingViewModelTest {
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createViewModel(state: OnboardingViewState = initialState): OnboardingViewModel {
|
private fun viewModelWith(state: OnboardingViewState) {
|
||||||
return OnboardingViewModel(
|
OnboardingViewModel(
|
||||||
state,
|
state,
|
||||||
fakeContext.instance,
|
fakeContext.instance,
|
||||||
fakeAuthenticationService,
|
fakeAuthenticationService,
|
||||||
|
@ -423,7 +440,10 @@ class OnboardingViewModelTest {
|
||||||
fakeDirectLoginUseCase.instance,
|
fakeDirectLoginUseCase.instance,
|
||||||
fakeStartAuthenticationFlowUseCase.instance,
|
fakeStartAuthenticationFlowUseCase.instance,
|
||||||
FakeVectorOverrides()
|
FakeVectorOverrides()
|
||||||
)
|
).also {
|
||||||
|
viewModel = it
|
||||||
|
initialState = state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun givenPictureSelected(fileUri: Uri, filename: String): OnboardingViewState {
|
private fun givenPictureSelected(fileUri: Uri, filename: String): OnboardingViewState {
|
||||||
|
@ -481,6 +501,12 @@ class OnboardingViewModelTest {
|
||||||
fakeAuthenticationService.givenRegistrationWizard(registrationWizard)
|
fakeAuthenticationService.givenRegistrationWizard(registrationWizard)
|
||||||
fakeRegisterActionHandler.givenResultsFor(registrationWizard, results)
|
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(
|
private fun HomeServerCapabilities.toPersonalisationState() = PersonalizationState(
|
||||||
|
|
|
@ -33,4 +33,8 @@ class FakeRegisterActionHandler {
|
||||||
result.first { it.first == actionArg }.second
|
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