From 3e1801a5c4fe19f164ebd4d1bd6cf20d9e8b721d Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 8 Dec 2021 17:01:26 +0000 Subject: [PATCH] clones the login view model domain to the ftue so that we can start overriding and merge login2 behaviour --- .../app/core/di/MavericksViewModelModule.kt | 1 + .../app/features/ftue/DefaultFTUEVariant.kt | 72 +- .../im/vector/app/features/ftue/FTUEAction.kt | 79 ++ .../vector/app/features/ftue/FTUEActivity.kt | 2 +- .../app/features/ftue/FTUEVariantFactory.kt | 5 +- .../app/features/ftue/FTUEViewEvents.kt | 50 ++ .../vector/app/features/ftue/FTUEViewModel.kt | 840 ++++++++++++++++++ .../vector/app/features/ftue/FTUEViewState.kt | 76 ++ 8 files changed, 1083 insertions(+), 42 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/ftue/FTUEAction.kt create mode 100644 vector/src/main/java/im/vector/app/features/ftue/FTUEViewEvents.kt create mode 100644 vector/src/main/java/im/vector/app/features/ftue/FTUEViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/ftue/FTUEViewState.kt diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index cc31a7dca6..9e55e29c74 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -37,6 +37,7 @@ import im.vector.app.features.crypto.verification.emoji.VerificationEmojiCodeVie import im.vector.app.features.devtools.RoomDevToolViewModel import im.vector.app.features.discovery.DiscoverySettingsViewModel import im.vector.app.features.discovery.change.SetIdentityServerViewModel +import im.vector.app.features.ftue.FTUEViewModel import im.vector.app.features.home.HomeActivityViewModel import im.vector.app.features.home.HomeDetailViewModel import im.vector.app.features.home.PromoteRestrictedViewModel diff --git a/vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt b/vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt index 0c5462eba3..62f592ea2c 100644 --- a/vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt +++ b/vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt @@ -35,7 +35,6 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityLoginBinding import im.vector.app.features.home.HomeActivity -import im.vector.app.features.login.LoginAction import im.vector.app.features.login.LoginCaptchaFragment import im.vector.app.features.login.LoginCaptchaFragmentArgument import im.vector.app.features.login.LoginConfig @@ -50,9 +49,6 @@ import im.vector.app.features.login.LoginServerSelectionFragment import im.vector.app.features.login.LoginServerUrlFormFragment import im.vector.app.features.login.LoginSignUpSignInSelectionFragment import im.vector.app.features.login.LoginSplashFragment -import im.vector.app.features.login.LoginViewEvents -import im.vector.app.features.login.LoginViewModel -import im.vector.app.features.login.LoginViewState import im.vector.app.features.login.LoginWaitForEmailFragment import im.vector.app.features.login.LoginWaitForEmailFragmentArgument import im.vector.app.features.login.LoginWebFragment @@ -72,7 +68,7 @@ private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG" class DefaultFTUEVariant( private val views: ActivityLoginBinding, - private val loginViewModel: LoginViewModel, + private val ftueViewModel: FTUEViewModel, private val activity: VectorBaseActivity, private val supportFragmentManager: FragmentManager ) : FTUEVariant { @@ -103,16 +99,16 @@ class DefaultFTUEVariant( } with(activity) { - loginViewModel.onEach { + ftueViewModel.onEach { updateWithState(it) } - loginViewModel.observeViewEvents { handleLoginViewEvents(it) } + ftueViewModel.observeViewEvents { handleLoginViewEvents(it) } } // Get config extra val loginConfig = activity.intent.getParcelableExtra(FTUEActivity.EXTRA_CONFIG) if (isFirstCreation) { - loginViewModel.handle(LoginAction.InitWith(loginConfig)) + ftueViewModel.handle(FTUEAction.InitWith(loginConfig)) } } @@ -124,17 +120,17 @@ class DefaultFTUEVariant( activity.addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java) } - private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) { - when (loginViewEvents) { - is LoginViewEvents.RegistrationFlowResult -> { + private fun handleLoginViewEvents(ftueViewEvents: FTUEViewEvents) { + when (ftueViewEvents) { + is FTUEViewEvents.RegistrationFlowResult -> { // Check that all flows are supported by the application - if (loginViewEvents.flowResult.missingStages.any { !it.isSupported() }) { + if (ftueViewEvents.flowResult.missingStages.any { !it.isSupported() }) { // Display a popup to propose use web fallback onRegistrationStageNotSupported() } else { - if (loginViewEvents.isRegistrationStarted) { + if (ftueViewEvents.isRegistrationStarted) { // Go on with registration flow - handleRegistrationNavigation(loginViewEvents.flowResult) + handleRegistrationNavigation(ftueViewEvents.flowResult) } else { // First ask for login and password // I add a tag to indicate that this fragment is a registration stage. @@ -147,7 +143,7 @@ class DefaultFTUEVariant( } } } - is LoginViewEvents.OutdatedHomeserver -> { + is FTUEViewEvents.OutdatedHomeserver -> { MaterialAlertDialogBuilder(activity) .setTitle(R.string.login_error_outdated_homeserver_title) .setMessage(R.string.login_error_outdated_homeserver_warning_content) @@ -155,7 +151,7 @@ class DefaultFTUEVariant( .show() Unit } - is LoginViewEvents.OpenServerSelection -> + is FTUEViewEvents.OpenServerSelection -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java, option = { ft -> @@ -167,63 +163,63 @@ class DefaultFTUEVariant( // TODO Disabled because it provokes a flickering // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) }) - is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone(loginViewEvents) - is LoginViewEvents.OnSignModeSelected -> onSignModeSelected(loginViewEvents) - is LoginViewEvents.OnLoginFlowRetrieved -> + is FTUEViewEvents.OnServerSelectionDone -> onServerSelectionDone(ftueViewEvents) + is FTUEViewEvents.OnSignModeSelected -> onSignModeSelected(ftueViewEvents) + is FTUEViewEvents.OnLoginFlowRetrieved -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, LoginSignUpSignInSelectionFragment::class.java, option = commonOption) - is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents) - is LoginViewEvents.OnForgetPasswordClicked -> + is FTUEViewEvents.OnWebLoginError -> onWebLoginError(ftueViewEvents) + is FTUEViewEvents.OnForgetPasswordClicked -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, LoginResetPasswordFragment::class.java, option = commonOption) - is LoginViewEvents.OnResetPasswordSendThreePidDone -> { + is FTUEViewEvents.OnResetPasswordSendThreePidDone -> { supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) activity.addFragmentToBackstack(R.id.loginFragmentContainer, LoginResetPasswordMailConfirmationFragment::class.java, option = commonOption) } - is LoginViewEvents.OnResetPasswordMailConfirmationSuccess -> { + is FTUEViewEvents.OnResetPasswordMailConfirmationSuccess -> { supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) activity.addFragmentToBackstack(R.id.loginFragmentContainer, LoginResetPasswordSuccessFragment::class.java, option = commonOption) } - is LoginViewEvents.OnResetPasswordMailConfirmationSuccessDone -> { + is FTUEViewEvents.OnResetPasswordMailConfirmationSuccessDone -> { // Go back to the login fragment supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) } - is LoginViewEvents.OnSendEmailSuccess -> { + is FTUEViewEvents.OnSendEmailSuccess -> { // Pop the enter email Fragment supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) activity.addFragmentToBackstack(R.id.loginFragmentContainer, LoginWaitForEmailFragment::class.java, - LoginWaitForEmailFragmentArgument(loginViewEvents.email), + LoginWaitForEmailFragmentArgument(ftueViewEvents.email), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) } - is LoginViewEvents.OnSendMsisdnSuccess -> { + is FTUEViewEvents.OnSendMsisdnSuccess -> { // Pop the enter Msisdn Fragment supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) activity.addFragmentToBackstack(R.id.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, - LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginViewEvents.msisdn), + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, ftueViewEvents.msisdn), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) } - is LoginViewEvents.Failure, - is LoginViewEvents.Loading -> + is FTUEViewEvents.Failure, + is FTUEViewEvents.Loading -> // This is handled by the Fragments Unit }.exhaustive } - private fun updateWithState(loginViewState: LoginViewState) { - if (loginViewState.isUserLogged()) { + private fun updateWithState(ftueViewState: FTUEViewState) { + if (ftueViewState.isUserLogged()) { val intent = HomeActivity.newIntent( activity, - accountCreation = loginViewState.signMode == SignMode.SignUp + accountCreation = ftueViewState.signMode == SignMode.SignUp ) activity.startActivity(intent) activity.finish() @@ -231,10 +227,10 @@ class DefaultFTUEVariant( } // Loading - views.loginLoading.isVisible = loginViewState.isLoading() + views.loginLoading.isVisible = ftueViewState.isLoading() } - private fun onWebLoginError(onWebLoginError: LoginViewEvents.OnWebLoginError) { + private fun onWebLoginError(onWebLoginError: FTUEViewEvents.OnWebLoginError) { // Pop the backstack supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) @@ -246,7 +242,7 @@ class DefaultFTUEVariant( .show() } - private fun onServerSelectionDone(loginViewEvents: LoginViewEvents.OnServerSelectionDone) { + private fun onServerSelectionDone(loginViewEvents: FTUEViewEvents.OnServerSelectionDone) { when (loginViewEvents.serverType) { ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow ServerType.EMS, @@ -257,7 +253,7 @@ class DefaultFTUEVariant( } } - private fun onSignModeSelected(loginViewEvents: LoginViewEvents.OnSignModeSelected) = withState(loginViewModel) { state -> + private fun onSignModeSelected(loginViewEvents: FTUEViewEvents.OnSignModeSelected) = withState(ftueViewModel) { state -> // state.signMode could not be ready yet. So use value from the ViewEvent when (loginViewEvents.signMode) { SignMode.Unknown -> error("Sign mode has to be set before calling this method") @@ -290,7 +286,7 @@ class DefaultFTUEVariant( override fun onNewIntent(intent: Intent?) { intent?.data ?.let { tryOrNull { it.getQueryParameter("loginToken") } } - ?.let { loginViewModel.handle(LoginAction.LoginWithToken(it)) } + ?.let { ftueViewModel.handle(FTUEAction.LoginWithToken(it)) } } private fun onRegistrationStageNotSupported() { diff --git a/vector/src/main/java/im/vector/app/features/ftue/FTUEAction.kt b/vector/src/main/java/im/vector/app/features/ftue/FTUEAction.kt new file mode 100644 index 0000000000..b43ffd3331 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/ftue/FTUEAction.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.ftue + +import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.login.LoginConfig +import im.vector.app.features.login.ServerType +import im.vector.app.features.login.SignMode +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.internal.network.ssl.Fingerprint + +sealed class FTUEAction : VectorViewModelAction { + data class OnGetStarted(val resetLoginConfig: Boolean) : FTUEAction() + + data class UpdateServerType(val serverType: ServerType) : FTUEAction() + data class UpdateHomeServer(val homeServerUrl: String) : FTUEAction() + data class UpdateSignMode(val signMode: SignMode) : FTUEAction() + data class LoginWithToken(val loginToken: String) : FTUEAction() + data class WebLoginSuccess(val credentials: Credentials) : FTUEAction() + data class InitWith(val loginConfig: LoginConfig?) : FTUEAction() + data class ResetPassword(val email: String, val newPassword: String) : FTUEAction() + object ResetPasswordMailConfirmed : FTUEAction() + + // Login or Register, depending on the signMode + data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : FTUEAction() + + // Register actions + open class RegisterAction : FTUEAction() + + data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction() + object SendAgainThreePid : RegisterAction() + + // TODO Confirm Email (from link in the email, open in the phone, intercepted by the app) + data class ValidateThreePid(val code: String) : RegisterAction() + + data class CheckIfEmailHasBeenValidated(val delayMillis: Long) : RegisterAction() + object StopEmailValidationCheck : RegisterAction() + + data class CaptchaDone(val captchaResponse: String) : RegisterAction() + object AcceptTerms : RegisterAction() + object RegisterDummy : RegisterAction() + + // Reset actions + open class ResetAction : FTUEAction() + + object ResetHomeServerType : ResetAction() + object ResetHomeServerUrl : ResetAction() + object ResetSignMode : ResetAction() + object ResetLogin : ResetAction() + object ResetResetPassword : ResetAction() + + // Homeserver history + object ClearHomeServerHistory : FTUEAction() + + // For the soft logout case + data class SetupSsoForSessionRecovery(val homeServerUrl: String, + val deviceId: String, + val ssoIdentityProviders: List?) : FTUEAction() + + data class PostViewEvent(val viewEvent: FTUEViewEvents) : FTUEAction() + + data class UserAcceptCertificate(val fingerprint: Fingerprint) : FTUEAction() +} diff --git a/vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt b/vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt index 805e39c48d..c54c0547a4 100644 --- a/vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt +++ b/vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt @@ -34,7 +34,7 @@ import javax.inject.Inject class FTUEActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { private val ftueVariant by lifecycleAwareLazy { - ftueVariantFactory.create(this, loginViewModel = lazyViewModel(), loginViewModel2 = lazyViewModel()) + ftueVariantFactory.create(this, ftueViewModel = lazyViewModel(), loginViewModel2 = lazyViewModel()) } @Inject lateinit var ftueVariantFactory: FTUEVariantFactory diff --git a/vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt b/vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt index 7efd6023fe..aabd0338a2 100644 --- a/vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt +++ b/vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt @@ -17,7 +17,6 @@ package im.vector.app.features.ftue import im.vector.app.features.VectorFeatures -import im.vector.app.features.login.LoginViewModel import im.vector.app.features.login2.LoginViewModel2 import javax.inject.Inject @@ -25,11 +24,11 @@ class FTUEVariantFactory @Inject constructor( private val vectorFeatures: VectorFeatures, ) { - fun create(activity: FTUEActivity, loginViewModel: Lazy, loginViewModel2: Lazy) = when (vectorFeatures.loginVariant()) { + fun create(activity: FTUEActivity, ftueViewModel: Lazy, loginViewModel2: Lazy) = when (vectorFeatures.loginVariant()) { VectorFeatures.LoginVariant.LEGACY -> error("Legacy is not supported by the FTUE") VectorFeatures.LoginVariant.FTUE -> DefaultFTUEVariant( views = activity.getBinding(), - loginViewModel = loginViewModel.value, + ftueViewModel = ftueViewModel.value, activity = activity, supportFragmentManager = activity.supportFragmentManager ) diff --git a/vector/src/main/java/im/vector/app/features/ftue/FTUEViewEvents.kt b/vector/src/main/java/im/vector/app/features/ftue/FTUEViewEvents.kt new file mode 100644 index 0000000000..d10063c797 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/ftue/FTUEViewEvents.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.app.features.ftue + +import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.features.login.ServerType +import im.vector.app.features.login.SignMode +import org.matrix.android.sdk.api.auth.registration.FlowResult + +/** + * Transient events for Login + */ +sealed class FTUEViewEvents : VectorViewEvents { + data class Loading(val message: CharSequence? = null) : FTUEViewEvents() + data class Failure(val throwable: Throwable) : FTUEViewEvents() + + data class RegistrationFlowResult(val flowResult: FlowResult, val isRegistrationStarted: Boolean) : FTUEViewEvents() + object OutdatedHomeserver : FTUEViewEvents() + + // Navigation event + + object OpenServerSelection : FTUEViewEvents() + data class OnServerSelectionDone(val serverType: ServerType) : FTUEViewEvents() + object OnLoginFlowRetrieved : FTUEViewEvents() + data class OnSignModeSelected(val signMode: SignMode) : FTUEViewEvents() + object OnForgetPasswordClicked : FTUEViewEvents() + object OnResetPasswordSendThreePidDone : FTUEViewEvents() + object OnResetPasswordMailConfirmationSuccess : FTUEViewEvents() + object OnResetPasswordMailConfirmationSuccessDone : FTUEViewEvents() + + data class OnSendEmailSuccess(val email: String) : FTUEViewEvents() + data class OnSendMsisdnSuccess(val msisdn: String) : FTUEViewEvents() + + data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : FTUEViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/ftue/FTUEViewModel.kt b/vector/src/main/java/im/vector/app/features/ftue/FTUEViewModel.kt new file mode 100644 index 0000000000..4c78d6fd30 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/ftue/FTUEViewModel.kt @@ -0,0 +1,840 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.ftue + +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 +import im.vector.app.R +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.extensions.configureAndStart +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.ensureTrailingSlash +import im.vector.app.features.login.HomeServerConnectionConfigFactory +import im.vector.app.features.login.LoginConfig +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 kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixPatterns.getDomain +import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.HomeServerHistoryService +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.auth.login.LoginWizard +import org.matrix.android.sdk.api.auth.registration.FlowResult +import org.matrix.android.sdk.api.auth.registration.RegistrationResult +import org.matrix.android.sdk.api.auth.registration.RegistrationWizard +import org.matrix.android.sdk.api.auth.registration.Stage +import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixIdFailure +import org.matrix.android.sdk.api.session.Session +import timber.log.Timber +import java.util.concurrent.CancellationException + +/** + * + */ +class FTUEViewModel @AssistedInject constructor( + @Assisted initialState: FTUEViewState, + private val applicationContext: Context, + private val authenticationService: AuthenticationService, + private val activeSessionHolder: ActiveSessionHolder, + private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, + private val reAuthHelper: ReAuthHelper, + private val stringProvider: StringProvider, + private val homeServerHistoryService: HomeServerHistoryService +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: FTUEViewState): FTUEViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + init { + getKnownCustomHomeServersUrls() + } + + private fun getKnownCustomHomeServersUrls() { + setState { + copy(knownCustomHomeServersUrls = homeServerHistoryService.getKnownServersUrls()) + } + } + + // Store the last action, to redo it after user has trusted the untrusted certificate + private var lastAction: FTUEAction? = null + private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null + + private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash() + + val currentThreePid: String? + get() = registrationWizard?.currentThreePid + + // True when login and password has been sent with success to the homeserver + val isRegistrationStarted: Boolean + get() = authenticationService.isRegistrationStarted + + private val registrationWizard: RegistrationWizard? + get() = authenticationService.getRegistrationWizard() + + private val loginWizard: LoginWizard? + get() = authenticationService.getLoginWizard() + + private var loginConfig: LoginConfig? = null + + private var currentJob: Job? = null + set(value) { + // Cancel any previous Job + field?.cancel() + field = value + } + + override fun handle(action: FTUEAction) { + when (action) { + is FTUEAction.OnGetStarted -> handleOnGetStarted(action) + is FTUEAction.UpdateServerType -> handleUpdateServerType(action) + is FTUEAction.UpdateSignMode -> handleUpdateSignMode(action) + is FTUEAction.InitWith -> handleInitWith(action) + is FTUEAction.UpdateHomeServer -> handleUpdateHomeserver(action).also { lastAction = action } + is FTUEAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action } + is FTUEAction.LoginWithToken -> handleLoginWithToken(action) + is FTUEAction.WebLoginSuccess -> handleWebLoginSuccess(action) + is FTUEAction.ResetPassword -> handleResetPassword(action) + is FTUEAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() + is FTUEAction.RegisterAction -> handleRegisterAction(action) + is FTUEAction.ResetAction -> handleResetAction(action) + is FTUEAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action) + is FTUEAction.UserAcceptCertificate -> handleUserAcceptCertificate(action) + FTUEAction.ClearHomeServerHistory -> handleClearHomeServerHistory() + is FTUEAction.PostViewEvent -> _viewEvents.post(action.viewEvent) + }.exhaustive + } + + private fun handleOnGetStarted(action: FTUEAction.OnGetStarted) { + if (action.resetLoginConfig) { + loginConfig = null + } + + val configUrl = loginConfig?.homeServerUrl?.takeIf { it.isNotEmpty() } + if (configUrl != null) { + // Use config from uri + val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(configUrl) + if (homeServerConnectionConfig == null) { + // Url is invalid, in this case, just use the regular flow + Timber.w("Url from config url was invalid: $configUrl") + _viewEvents.post(FTUEViewEvents.OpenServerSelection) + } else { + getLoginFlow(homeServerConnectionConfig, ServerType.Other) + } + } else { + _viewEvents.post(FTUEViewEvents.OpenServerSelection) + } + } + + private fun handleUserAcceptCertificate(action: FTUEAction.UserAcceptCertificate) { + // It happens when we get the login flow, or during direct authentication. + // So alter the homeserver config and retrieve again the login flow + when (val finalLastAction = lastAction) { + is FTUEAction.UpdateHomeServer -> { + currentHomeServerConnectionConfig + ?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) } + ?.let { getLoginFlow(it) } + } + is FTUEAction.LoginOrRegister -> + handleDirectLogin( + finalLastAction, + HomeServerConnectionConfig.Builder() + // Will be replaced by the task + .withHomeServerUri("https://dummy.org") + .withAllowedFingerPrints(listOf(action.fingerprint)) + .build() + ) + } + } + + private fun rememberHomeServer(homeServerUrl: String) { + homeServerHistoryService.addHomeServerToHistory(homeServerUrl) + getKnownCustomHomeServersUrls() + } + + private fun handleClearHomeServerHistory() { + homeServerHistoryService.clearHistory() + getKnownCustomHomeServersUrls() + } + + private fun handleLoginWithToken(action: FTUEAction.LoginWithToken) { + val safeLoginWizard = loginWizard + + if (safeLoginWizard == null) { + setState { + copy( + asyncLoginAction = Fail(Throwable("Bad configuration")) + ) + } + } else { + setState { + copy( + asyncLoginAction = Loading() + ) + } + + currentJob = viewModelScope.launch { + try { + safeLoginWizard.loginWithToken(action.loginToken) + } catch (failure: Throwable) { + _viewEvents.post(FTUEViewEvents.Failure(failure)) + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + null + } + ?.let { onSessionCreated(it) } + } + } + } + + private fun handleSetupSsoForSessionRecovery(action: FTUEAction.SetupSsoForSessionRecovery) { + setState { + copy( + signMode = SignMode.SignIn, + loginMode = LoginMode.Sso(action.ssoIdentityProviders), + homeServerUrlFromUser = action.homeServerUrl, + homeServerUrl = action.homeServerUrl, + deviceId = action.deviceId + ) + } + } + + private fun handleRegisterAction(action: FTUEAction.RegisterAction) { + when (action) { + is FTUEAction.CaptchaDone -> handleCaptchaDone(action) + is FTUEAction.AcceptTerms -> handleAcceptTerms() + is FTUEAction.RegisterDummy -> handleRegisterDummy() + is FTUEAction.AddThreePid -> handleAddThreePid(action) + is FTUEAction.SendAgainThreePid -> handleSendAgainThreePid() + is FTUEAction.ValidateThreePid -> handleValidateThreePid(action) + is FTUEAction.CheckIfEmailHasBeenValidated -> handleCheckIfEmailHasBeenValidated(action) + is FTUEAction.StopEmailValidationCheck -> handleStopEmailValidationCheck() + } + } + + private fun handleCheckIfEmailHasBeenValidated(action: FTUEAction.CheckIfEmailHasBeenValidated) { + // We do not want the common progress bar to be displayed, so we do not change asyncRegistration value in the state + currentJob = executeRegistrationStep(withLoading = false) { + it.checkIfEmailHasBeenValidated(action.delayMillis) + } + } + + private fun handleStopEmailValidationCheck() { + currentJob = null + } + + private fun handleValidateThreePid(action: FTUEAction.ValidateThreePid) { + currentJob = executeRegistrationStep { + it.handleValidateThreePid(action.code) + } + } + + private fun executeRegistrationStep(withLoading: Boolean = true, + block: suspend (RegistrationWizard) -> RegistrationResult): Job { + if (withLoading) { + setState { copy(asyncRegistration = Loading()) } + } + return viewModelScope.launch { + try { + registrationWizard?.let { block(it) } + /* + // Simulate registration disabled + throw Failure.ServerError(MatrixError( + code = MatrixError.FORBIDDEN, + message = "Registration is disabled" + ), 403)) + */ + } catch (failure: Throwable) { + if (failure !is CancellationException) { + _viewEvents.post(FTUEViewEvents.Failure(failure)) + } + null + } + ?.let { data -> + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + setState { + copy( + asyncRegistration = Uninitialized + ) + } + } + } + + private fun handleAddThreePid(action: FTUEAction.AddThreePid) { + setState { copy(asyncRegistration = Loading()) } + currentJob = viewModelScope.launch { + try { + registrationWizard?.addThreePid(action.threePid) + } catch (failure: Throwable) { + _viewEvents.post(FTUEViewEvents.Failure(failure)) + } + setState { + copy( + asyncRegistration = Uninitialized + ) + } + } + } + + private fun handleSendAgainThreePid() { + setState { copy(asyncRegistration = Loading()) } + currentJob = viewModelScope.launch { + try { + registrationWizard?.sendAgainThreePid() + } catch (failure: Throwable) { + _viewEvents.post(FTUEViewEvents.Failure(failure)) + } + setState { + copy( + asyncRegistration = Uninitialized + ) + } + } + } + + private fun handleAcceptTerms() { + currentJob = executeRegistrationStep { + it.acceptTerms() + } + } + + private fun handleRegisterDummy() { + currentJob = executeRegistrationStep { + it.dummy() + } + } + + private fun handleRegisterWith(action: FTUEAction.LoginOrRegister) { + reAuthHelper.data = action.password + currentJob = executeRegistrationStep { + it.createAccount( + action.username, + action.password, + action.initialDeviceName + ) + } + } + + private fun handleCaptchaDone(action: FTUEAction.CaptchaDone) { + currentJob = executeRegistrationStep { + it.performReCaptcha(action.captchaResponse) + } + } + + private fun handleResetAction(action: FTUEAction.ResetAction) { + // Cancel any request + currentJob = null + + when (action) { + FTUEAction.ResetHomeServerType -> { + setState { + copy( + serverType = ServerType.Unknown + ) + } + } + FTUEAction.ResetHomeServerUrl -> { + viewModelScope.launch { + authenticationService.reset() + setState { + copy( + asyncHomeServerLoginFlowRequest = Uninitialized, + homeServerUrlFromUser = null, + homeServerUrl = null, + loginMode = LoginMode.Unknown, + serverType = ServerType.Unknown, + loginModeSupportedTypes = emptyList() + ) + } + } + } + FTUEAction.ResetSignMode -> { + setState { + copy( + asyncHomeServerLoginFlowRequest = Uninitialized, + signMode = SignMode.Unknown, + loginMode = LoginMode.Unknown, + loginModeSupportedTypes = emptyList() + ) + } + } + FTUEAction.ResetLogin -> { + viewModelScope.launch { + authenticationService.cancelPendingLoginOrRegistration() + setState { + copy( + asyncLoginAction = Uninitialized, + asyncRegistration = Uninitialized + ) + } + } + } + FTUEAction.ResetResetPassword -> { + setState { + copy( + asyncResetPassword = Uninitialized, + asyncResetMailConfirmed = Uninitialized, + resetPasswordEmail = null + ) + } + } + } + } + + private fun handleUpdateSignMode(action: FTUEAction.UpdateSignMode) { + setState { + copy( + signMode = action.signMode + ) + } + + when (action.signMode) { + SignMode.SignUp -> startRegistrationFlow() + SignMode.SignIn -> startAuthenticationFlow() + SignMode.SignInWithMatrixId -> _viewEvents.post(FTUEViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId)) + SignMode.Unknown -> Unit + } + } + + private fun handleUpdateServerType(action: FTUEAction.UpdateServerType) { + setState { + copy( + serverType = action.serverType + ) + } + + when (action.serverType) { + ServerType.Unknown -> Unit /* Should not happen */ + ServerType.MatrixOrg -> + // Request login flow here + handle(FTUEAction.UpdateHomeServer(matrixOrgUrl)) + ServerType.EMS, + ServerType.Other -> _viewEvents.post(FTUEViewEvents.OnServerSelectionDone(action.serverType)) + }.exhaustive + } + + private fun handleInitWith(action: FTUEAction.InitWith) { + loginConfig = action.loginConfig + + // If there is a pending email validation continue on this step + try { + if (registrationWizard?.isRegistrationStarted == true) { + currentThreePid?.let { + handle(FTUEAction.PostViewEvent(FTUEViewEvents.OnSendEmailSuccess(it))) + } + } + } catch (e: Throwable) { + // NOOP. API is designed to use wizards in a login/registration flow, + // but we need to check the state anyway. + } + } + + private fun handleResetPassword(action: FTUEAction.ResetPassword) { + val safeLoginWizard = loginWizard + + if (safeLoginWizard == null) { + setState { + copy( + asyncResetPassword = Fail(Throwable("Bad configuration")), + asyncResetMailConfirmed = Uninitialized + ) + } + } else { + setState { + copy( + asyncResetPassword = Loading(), + asyncResetMailConfirmed = Uninitialized + ) + } + + currentJob = viewModelScope.launch { + try { + safeLoginWizard.resetPassword(action.email, action.newPassword) + } catch (failure: Throwable) { + setState { + copy( + asyncResetPassword = Fail(failure) + ) + } + return@launch + } + + setState { + copy( + asyncResetPassword = Success(Unit), + resetPasswordEmail = action.email + ) + } + + _viewEvents.post(FTUEViewEvents.OnResetPasswordSendThreePidDone) + } + } + } + + private fun handleResetPasswordMailConfirmed() { + val safeLoginWizard = loginWizard + + if (safeLoginWizard == null) { + setState { + copy( + asyncResetPassword = Uninitialized, + asyncResetMailConfirmed = Fail(Throwable("Bad configuration")) + ) + } + } else { + setState { + copy( + asyncResetPassword = Uninitialized, + asyncResetMailConfirmed = Loading() + ) + } + + currentJob = viewModelScope.launch { + try { + safeLoginWizard.resetPasswordMailConfirmed() + } catch (failure: Throwable) { + setState { + copy( + asyncResetMailConfirmed = Fail(failure) + ) + } + return@launch + } + setState { + copy( + asyncResetMailConfirmed = Success(Unit), + resetPasswordEmail = null + ) + } + + _viewEvents.post(FTUEViewEvents.OnResetPasswordMailConfirmationSuccess) + } + } + } + + private fun handleLoginOrRegister(action: FTUEAction.LoginOrRegister) = withState { state -> + when (state.signMode) { + SignMode.Unknown -> error("Developer error, invalid sign mode") + SignMode.SignIn -> handleLogin(action) + SignMode.SignUp -> handleRegisterWith(action) + SignMode.SignInWithMatrixId -> handleDirectLogin(action, null) + }.exhaustive + } + + private fun handleDirectLogin(action: FTUEAction.LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?) { + setState { + copy( + asyncLoginAction = Loading() + ) + } + + currentJob = viewModelScope.launch { + val data = try { + authenticationService.getWellKnownData(action.username, homeServerConnectionConfig) + } catch (failure: Throwable) { + onDirectLoginError(failure) + return@launch + } + when (data) { + is WellknownResult.Prompt -> + onWellknownSuccess(action, data, homeServerConnectionConfig) + is WellknownResult.FailPrompt -> + // Relax on IS discovery if homeserver is valid + if (data.homeServerUrl != null && data.wellKnown != null) { + onWellknownSuccess(action, WellknownResult.Prompt(data.homeServerUrl!!, null, data.wellKnown!!), homeServerConnectionConfig) + } else { + onWellKnownError() + } + else -> { + onWellKnownError() + } + }.exhaustive + } + } + + private fun onWellKnownError() { + setState { + copy( + asyncLoginAction = Uninitialized + ) + } + _viewEvents.post(FTUEViewEvents.Failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error)))) + } + + private suspend fun onWellknownSuccess(action: FTUEAction.LoginOrRegister, + wellKnownPrompt: WellknownResult.Prompt, + homeServerConnectionConfig: HomeServerConnectionConfig?) { + val alteredHomeServerConnectionConfig = homeServerConnectionConfig + ?.copy( + homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl), + identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) } + ) + ?: HomeServerConnectionConfig( + homeServerUri = Uri.parse("https://${action.username.getDomain()}"), + homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl), + identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) } + ) + + val data = try { + authenticationService.directAuthentication( + alteredHomeServerConnectionConfig, + action.username, + action.password, + action.initialDeviceName) + } catch (failure: Throwable) { + onDirectLoginError(failure) + return + } + onSessionCreated(data) + } + + private fun onDirectLoginError(failure: Throwable) { + when (failure) { + is MatrixIdFailure.InvalidMatrixId, + is Failure.UnrecognizedCertificateFailure -> { + // Display this error in a dialog + _viewEvents.post(FTUEViewEvents.Failure(failure)) + setState { + copy( + asyncLoginAction = Uninitialized + ) + } + } + else -> { + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + } + } + + private fun handleLogin(action: FTUEAction.LoginOrRegister) { + val safeLoginWizard = loginWizard + + if (safeLoginWizard == null) { + setState { + copy( + asyncLoginAction = Fail(Throwable("Bad configuration")) + ) + } + } else { + setState { + copy( + asyncLoginAction = Loading() + ) + } + + currentJob = viewModelScope.launch { + try { + safeLoginWizard.login( + action.username, + action.password, + action.initialDeviceName + ) + } catch (failure: Throwable) { + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + null + } + ?.let { + reAuthHelper.data = action.password + onSessionCreated(it) + } + } + } + } + + private fun startRegistrationFlow() { + currentJob = executeRegistrationStep { + it.getRegistrationFlow() + } + } + + private fun startAuthenticationFlow() { + // Ensure Wizard is ready + loginWizard + + _viewEvents.post(FTUEViewEvents.OnSignModeSelected(SignMode.SignIn)) + } + + private fun onFlowResponse(flowResult: FlowResult) { + // 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() + } else { + // Notify the user + _viewEvents.post(FTUEViewEvents.RegistrationFlowResult(flowResult, isRegistrationStarted)) + } + } + + private suspend fun onSessionCreated(session: Session) { + activeSessionHolder.setActiveSession(session) + + authenticationService.reset() + session.configureAndStart(applicationContext) + setState { + copy( + asyncLoginAction = Success(Unit) + ) + } + } + + private fun handleWebLoginSuccess(action: FTUEAction.WebLoginSuccess) = withState { state -> + val homeServerConnectionConfigFinal = homeServerConnectionConfigFactory.create(state.homeServerUrl) + + if (homeServerConnectionConfigFinal == null) { + // Should not happen + Timber.w("homeServerConnectionConfig is null") + } else { + currentJob = viewModelScope.launch { + try { + authenticationService.createSessionFromSso(homeServerConnectionConfigFinal, action.credentials) + } catch (failure: Throwable) { + setState { + copy(asyncLoginAction = Fail(failure)) + } + null + } + ?.let { onSessionCreated(it) } + } + } + } + + private fun handleUpdateHomeserver(action: FTUEAction.UpdateHomeServer) { + val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl) + if (homeServerConnectionConfig == null) { + // This is invalid + _viewEvents.post(FTUEViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig"))) + } else { + getLoginFlow(homeServerConnectionConfig) + } + } + + private fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, + serverTypeOverride: ServerType? = null) { + currentHomeServerConnectionConfig = homeServerConnectionConfig + + currentJob = viewModelScope.launch { + authenticationService.cancelPendingLoginOrRegistration() + + setState { + copy( + asyncHomeServerLoginFlowRequest = Loading(), + // 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) { + ServerType.MatrixOrg + } else { + serverTypeOverride ?: serverType + } + ) + } + + val data = try { + authenticationService.getLoginFlow(homeServerConnectionConfig) + } catch (failure: Throwable) { + _viewEvents.post(FTUEViewEvents.Failure(failure)) + setState { + copy( + asyncHomeServerLoginFlowRequest = Uninitialized, + // If we were trying to retrieve matrix.org login flow, also reset the serverType + serverType = if (serverType == ServerType.MatrixOrg) ServerType.Unknown else serverType + ) + } + null + } + + data ?: return@launch + + // Valid Homeserver, add it to the history. + // Note: we add what the user has input, data.homeServerUrlBase can be different + rememberHomeServer(homeServerConnectionConfig.homeServerUri.toString()) + + val loginMode = when { + // SSO login is taken first + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) && + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported + } + + setState { + copy( + asyncHomeServerLoginFlowRequest = Uninitialized, + homeServerUrlFromUser = homeServerConnectionConfig.homeServerUri.toString(), + homeServerUrl = data.homeServerUrl, + loginMode = loginMode, + loginModeSupportedTypes = data.supportedLoginTypes.toList() + ) + } + if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) || + data.isOutdatedHomeserver) { + // Notify the UI + _viewEvents.post(FTUEViewEvents.OutdatedHomeserver) + } + _viewEvents.post(FTUEViewEvents.OnLoginFlowRetrieved) + } + } + + fun getInitialHomeServerUrl(): String? { + return loginConfig?.homeServerUrl + } + + fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? { + return authenticationService.getSsoUrl(redirectUrl, deviceId, providerId) + } + + fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? { + return authenticationService.getFallbackUrl(forSignIn, deviceId) + } +} diff --git a/vector/src/main/java/im/vector/app/features/ftue/FTUEViewState.kt b/vector/src/main/java/im/vector/app/features/ftue/FTUEViewState.kt new file mode 100644 index 0000000000..9f34ed3782 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/ftue/FTUEViewState.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.ftue + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.PersistState +import com.airbnb.mvrx.Success +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 + +data class FTUEViewState( + val asyncLoginAction: Async = Uninitialized, + val asyncHomeServerLoginFlowRequest: Async = Uninitialized, + val asyncResetPassword: Async = Uninitialized, + val asyncResetMailConfirmed: Async = Uninitialized, + val asyncRegistration: Async = Uninitialized, + + // User choices + @PersistState + val serverType: ServerType = ServerType.Unknown, + @PersistState + val signMode: SignMode = SignMode.Unknown, + @PersistState + val resetPasswordEmail: String? = null, + @PersistState + val homeServerUrlFromUser: String? = null, + + // Can be modified after a Wellknown request + @PersistState + val homeServerUrl: String? = null, + + // For SSO session recovery + @PersistState + val deviceId: String? = null, + + // Network result + @PersistState + val loginMode: LoginMode = LoginMode.Unknown, + // Supported types for the login. We cannot use a sealed class for LoginType because it is not serializable + @PersistState + val loginModeSupportedTypes: List = emptyList(), + val knownCustomHomeServersUrls: List = emptyList() +) : MavericksState { + + fun isLoading(): Boolean { + return asyncLoginAction is Loading || + asyncHomeServerLoginFlowRequest is Loading || + asyncResetPassword is Loading || + asyncResetMailConfirmed is Loading || + asyncRegistration is Loading || + // Keep loading when it is success because of the delay to switch to the next Activity + asyncLoginAction is Success + } + + fun isUserLogged(): Boolean { + return asyncLoginAction is Success + } +}