clones the login view model domain to the ftue so that we can start overriding and merge login2 behaviour

This commit is contained in:
Adam Brown 2021-12-08 17:01:26 +00:00
parent eb9fc579d3
commit 1e5c057223
8 changed files with 1088 additions and 42 deletions

View file

@ -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
@ -447,6 +448,11 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(AccountCreatedViewModel::class)
fun accountCreatedViewModelFactory(factory: AccountCreatedViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(FTUEViewModel::class)
fun ftueViewModelFactory(factory: FTUEViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(LoginViewModel2::class)

View file

@ -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<ActivityLoginBinding>,
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<LoginConfig?>(FTUEActivity.EXTRA_CONFIG)
if (isFirstCreation) {
loginViewModel.handle(LoginAction.InitWith(loginConfig))
ftueViewModel.handle(FTUEAction.InitWith(loginConfig))
}
}
@ -124,17 +120,17 @@ class DefaultFTUEVariant(
activity.addFragment(views.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(views.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(views.loginFragmentContainer,
LoginSignUpSignInSelectionFragment::class.java,
option = commonOption)
is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents)
is LoginViewEvents.OnForgetPasswordClicked ->
is FTUEViewEvents.OnWebLoginError -> onWebLoginError(ftueViewEvents)
is FTUEViewEvents.OnForgetPasswordClicked ->
activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginResetPasswordFragment::class.java,
option = commonOption)
is LoginViewEvents.OnResetPasswordSendThreePidDone -> {
is FTUEViewEvents.OnResetPasswordSendThreePidDone -> {
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginResetPasswordMailConfirmationFragment::class.java,
option = commonOption)
}
is LoginViewEvents.OnResetPasswordMailConfirmationSuccess -> {
is FTUEViewEvents.OnResetPasswordMailConfirmationSuccess -> {
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
activity.addFragmentToBackstack(views.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(views.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(views.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() {

View file

@ -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<SsoIdentityProvider>?) : FTUEAction()
data class PostViewEvent(val viewEvent: FTUEViewEvents) : FTUEAction()
data class UserAcceptCertificate(val fingerprint: Fingerprint) : FTUEAction()
}

View file

@ -34,7 +34,7 @@ import javax.inject.Inject
class FTUEActivity : VectorBaseActivity<ActivityLoginBinding>(), 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

View file

@ -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<LoginViewModel>, loginViewModel2: Lazy<LoginViewModel2>) = when (vectorFeatures.loginVariant()) {
fun create(activity: FTUEActivity, ftueViewModel: Lazy<FTUEViewModel>, loginViewModel2: Lazy<LoginViewModel2>) = 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
)

View file

@ -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()
}

View file

@ -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<FTUEViewState, FTUEAction, FTUEViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<FTUEViewModel, FTUEViewState> {
override fun create(initialState: FTUEViewState): FTUEViewModel
}
companion object : MavericksViewModelFactory<FTUEViewModel, FTUEViewState> 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)
}
}

View file

@ -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<Unit> = Uninitialized,
val asyncHomeServerLoginFlowRequest: Async<Unit> = Uninitialized,
val asyncResetPassword: Async<Unit> = Uninitialized,
val asyncResetMailConfirmed: Async<Unit> = Uninitialized,
val asyncRegistration: Async<Unit> = 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<String> = emptyList(),
val knownCustomHomeServersUrls: List<String> = 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
}
}