From e091ea82dac08ebb8915b432607b85b1a782ce7a Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 8 Dec 2021 12:06:29 +0000 Subject: [PATCH 01/18] porting the LoginActivty2 to a dynamic FTUE activity - supports switching between a copied legacy flow (DefaultFTUE) and the WIP variant - this will allow us to make iterative changes to the default ftue flow without affecting the legacy flow/forks --- .../app/features/ftue/DefaultFTUEVariant.kt | 367 ++++++++++++++++++ .../vector/app/features/ftue/FTUEActivity.kt | 85 ++++ .../app/features/ftue/FTUEVariantFactory.kt | 43 ++ .../login2/created/AccountCreatedFragment.kt | 1 + .../features/navigation/DefaultNavigator.kt | 1 + 5 files changed, 497 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt create mode 100644 vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt create mode 100644 vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt 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 new file mode 100644 index 0000000000..0c5462eba3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt @@ -0,0 +1,367 @@ +/* + * Copyright (c) 2021 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.Intent +import android.view.View +import android.view.ViewGroup +import androidx.core.view.ViewCompat +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import com.airbnb.mvrx.withState +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R +import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.extensions.addFragmentToBackstack +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 +import im.vector.app.features.login.LoginFragment +import im.vector.app.features.login.LoginGenericTextInputFormFragment +import im.vector.app.features.login.LoginGenericTextInputFormFragmentArgument +import im.vector.app.features.login.LoginMode +import im.vector.app.features.login.LoginResetPasswordFragment +import im.vector.app.features.login.LoginResetPasswordMailConfirmationFragment +import im.vector.app.features.login.LoginResetPasswordSuccessFragment +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 +import im.vector.app.features.login.ServerType +import im.vector.app.features.login.SignMode +import im.vector.app.features.login.TextInputFormFragmentMode +import im.vector.app.features.login.isSupported +import im.vector.app.features.login.terms.LoginTermsFragment +import im.vector.app.features.login.terms.LoginTermsFragmentArgument +import im.vector.app.features.login.terms.toLocalizedLoginTerms +import org.matrix.android.sdk.api.auth.registration.FlowResult +import org.matrix.android.sdk.api.auth.registration.Stage +import org.matrix.android.sdk.api.extensions.tryOrNull + +private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" +private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG" + +class DefaultFTUEVariant( + private val views: ActivityLoginBinding, + private val loginViewModel: LoginViewModel, + private val activity: VectorBaseActivity, + private val supportFragmentManager: FragmentManager +) : FTUEVariant { + + private val enterAnim = R.anim.enter_fade_in + private val exitAnim = R.anim.exit_fade_out + + private val popEnterAnim = R.anim.no_anim + private val popExitAnim = R.anim.exit_fade_out + + private val topFragment: Fragment? + get() = supportFragmentManager.findFragmentById(R.id.loginFragmentContainer) + + private val commonOption: (FragmentTransaction) -> Unit = { ft -> + // Find the loginLogo on the current Fragment, this should not return null + (topFragment?.view as? ViewGroup) + // Find findViewById does not work, I do not know why + // findViewById(R.id.loginLogo) + ?.children + ?.firstOrNull { it.id == R.id.loginLogo } + ?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) + } + + override fun initUiAndData(isFirstCreation: Boolean) { + if (isFirstCreation) { + addFirstFragment() + } + + with(activity) { + loginViewModel.onEach { + updateWithState(it) + } + loginViewModel.observeViewEvents { handleLoginViewEvents(it) } + } + + // Get config extra + val loginConfig = activity.intent.getParcelableExtra(FTUEActivity.EXTRA_CONFIG) + if (isFirstCreation) { + loginViewModel.handle(LoginAction.InitWith(loginConfig)) + } + } + + override fun setIsLoading(isLoading: Boolean) { + // do nothing + } + + private fun addFirstFragment() { + activity.addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java) + } + + private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) { + when (loginViewEvents) { + is LoginViewEvents.RegistrationFlowResult -> { + // Check that all flows are supported by the application + if (loginViewEvents.flowResult.missingStages.any { !it.isSupported() }) { + // Display a popup to propose use web fallback + onRegistrationStageNotSupported() + } else { + if (loginViewEvents.isRegistrationStarted) { + // Go on with registration flow + handleRegistrationNavigation(loginViewEvents.flowResult) + } else { + // First ask for login and password + // I add a tag to indicate that this fragment is a registration stage. + // This way it will be automatically popped in when starting the next registration stage + activity.addFragmentToBackstack(R.id.loginFragmentContainer, + LoginFragment::class.java, + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption + ) + } + } + } + is LoginViewEvents.OutdatedHomeserver -> { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.login_error_outdated_homeserver_title) + .setMessage(R.string.login_error_outdated_homeserver_warning_content) + .setPositiveButton(R.string.ok, null) + .show() + Unit + } + is LoginViewEvents.OpenServerSelection -> + activity.addFragmentToBackstack(R.id.loginFragmentContainer, + LoginServerSelectionFragment::class.java, + option = { ft -> + activity.findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // Disable transition of text + // findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // No transition here now actually + // findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // 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 -> + activity.addFragmentToBackstack(R.id.loginFragmentContainer, + LoginSignUpSignInSelectionFragment::class.java, + option = commonOption) + is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents) + is LoginViewEvents.OnForgetPasswordClicked -> + activity.addFragmentToBackstack(R.id.loginFragmentContainer, + LoginResetPasswordFragment::class.java, + option = commonOption) + is LoginViewEvents.OnResetPasswordSendThreePidDone -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + activity.addFragmentToBackstack(R.id.loginFragmentContainer, + LoginResetPasswordMailConfirmationFragment::class.java, + option = commonOption) + } + is LoginViewEvents.OnResetPasswordMailConfirmationSuccess -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + activity.addFragmentToBackstack(R.id.loginFragmentContainer, + LoginResetPasswordSuccessFragment::class.java, + option = commonOption) + } + is LoginViewEvents.OnResetPasswordMailConfirmationSuccessDone -> { + // Go back to the login fragment + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + } + is LoginViewEvents.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), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + } + is LoginViewEvents.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), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + } + is LoginViewEvents.Failure, + is LoginViewEvents.Loading -> + // This is handled by the Fragments + Unit + }.exhaustive + } + + private fun updateWithState(loginViewState: LoginViewState) { + if (loginViewState.isUserLogged()) { + val intent = HomeActivity.newIntent( + activity, + accountCreation = loginViewState.signMode == SignMode.SignUp + ) + activity.startActivity(intent) + activity.finish() + return + } + + // Loading + views.loginLoading.isVisible = loginViewState.isLoading() + } + + private fun onWebLoginError(onWebLoginError: LoginViewEvents.OnWebLoginError) { + // Pop the backstack + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + + // And inform the user + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.dialog_title_error) + .setMessage(activity.getString(R.string.login_sso_error_message, onWebLoginError.description, onWebLoginError.errorCode)) + .setPositiveButton(R.string.ok, null) + .show() + } + + private fun onServerSelectionDone(loginViewEvents: LoginViewEvents.OnServerSelectionDone) { + when (loginViewEvents.serverType) { + ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow + ServerType.EMS, + ServerType.Other -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, + LoginServerUrlFormFragment::class.java, + option = commonOption) + ServerType.Unknown -> Unit /* Should not happen */ + } + } + + private fun onSignModeSelected(loginViewEvents: LoginViewEvents.OnSignModeSelected) = withState(loginViewModel) { 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") + SignMode.SignUp -> { + // This is managed by the LoginViewEvents + } + SignMode.SignIn -> { + // It depends on the LoginMode + when (state.loginMode) { + LoginMode.Unknown, + is LoginMode.Sso -> error("Developer error") + is LoginMode.SsoAndPassword, + LoginMode.Password -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, + LoginFragment::class.java, + tag = FRAGMENT_LOGIN_TAG, + option = commonOption) + LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) + }.exhaustive + } + SignMode.SignInWithMatrixId -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, + LoginFragment::class.java, + tag = FRAGMENT_LOGIN_TAG, + option = commonOption) + }.exhaustive + } + + /** + * Handle the SSO redirection here + */ + override fun onNewIntent(intent: Intent?) { + intent?.data + ?.let { tryOrNull { it.getQueryParameter("loginToken") } } + ?.let { loginViewModel.handle(LoginAction.LoginWithToken(it)) } + } + + private fun onRegistrationStageNotSupported() { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.app_name) + .setMessage(activity.getString(R.string.login_registration_not_supported)) + .setPositiveButton(R.string.yes) { _, _ -> + activity.addFragmentToBackstack(R.id.loginFragmentContainer, + LoginWebFragment::class.java, + option = commonOption) + } + .setNegativeButton(R.string.no, null) + .show() + } + + private fun onLoginModeNotSupported(supportedTypes: List) { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.app_name) + .setMessage(activity.getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" })) + .setPositiveButton(R.string.yes) { _, _ -> + activity.addFragmentToBackstack(R.id.loginFragmentContainer, + LoginWebFragment::class.java, + option = commonOption) + } + .setNegativeButton(R.string.no, null) + .show() + } + + private fun handleRegistrationNavigation(flowResult: FlowResult) { + // Complete all mandatory stages first + val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory } + + if (mandatoryStage != null) { + doStage(mandatoryStage) + } else { + // Consider optional stages + val optionalStage = flowResult.missingStages.firstOrNull { !it.mandatory && it !is Stage.Dummy } + if (optionalStage == null) { + // Should not happen... + } else { + doStage(optionalStage) + } + } + } + + private fun doStage(stage: Stage) { + // Ensure there is no fragment for registration stage in the backstack + supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) + + when (stage) { + is Stage.ReCaptcha -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, + LoginCaptchaFragment::class.java, + LoginCaptchaFragmentArgument(stage.publicKey), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is Stage.Email -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, + LoginGenericTextInputFormFragment::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is Stage.Msisdn -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, + LoginGenericTextInputFormFragment::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is Stage.Terms -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, + LoginTermsFragment::class.java, + LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + else -> Unit // Should not happen + } + } +} 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 new file mode 100644 index 0000000000..805e39c48d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2021 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.content.Intent +import android.net.Uri +import com.google.android.material.appbar.MaterialToolbar +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.lazyViewModel +import im.vector.app.core.platform.ToolbarConfigurable +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.platform.lifecycleAwareLazy +import im.vector.app.databinding.ActivityLoginBinding +import im.vector.app.features.login.LoginConfig +import im.vector.app.features.pin.UnlockedActivity +import javax.inject.Inject + +@AndroidEntryPoint +class FTUEActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { + + private val ftueVariant by lifecycleAwareLazy { + ftueVariantFactory.create(this, loginViewModel = lazyViewModel(), loginViewModel2 = lazyViewModel()) + } + + @Inject lateinit var ftueVariantFactory: FTUEVariantFactory + + override fun getBinding() = ActivityLoginBinding.inflate(layoutInflater) + + override fun getCoordinatorLayout() = views.coordinatorLayout + + override fun configure(toolbar: MaterialToolbar) { + configureToolbar(toolbar) + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + ftueVariant.onNewIntent(intent) + } + + override fun initUiAndData() { + ftueVariant.initUiAndData(isFirstCreation()) + } + + // Hack for AccountCreatedFragment + fun setIsLoading(isLoading: Boolean) { + ftueVariant.setIsLoading(isLoading) + } + + companion object { + const val EXTRA_CONFIG = "EXTRA_CONFIG" + + fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { + return Intent(context, FTUEActivity::class.java).apply { + putExtra(EXTRA_CONFIG, loginConfig) + } + } + + fun redirectIntent(context: Context, data: Uri?): Intent { + return Intent(context, FTUEActivity::class.java).apply { + setData(data) + } + } + } +} + +interface FTUEVariant { + fun onNewIntent(intent: Intent?) + fun initUiAndData(isFirstCreation: Boolean) + fun setIsLoading(isLoading: Boolean) +} 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 new file mode 100644 index 0000000000..7efd6023fe --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 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.features.VectorFeatures +import im.vector.app.features.login.LoginViewModel +import im.vector.app.features.login2.LoginViewModel2 +import javax.inject.Inject + +class FTUEVariantFactory @Inject constructor( + private val vectorFeatures: VectorFeatures, +) { + + fun create(activity: FTUEActivity, loginViewModel: 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, + activity = activity, + supportFragmentManager = activity.supportFragmentManager + ) + VectorFeatures.LoginVariant.FTUE_WIP -> FTUEWipVariant( + views = activity.getBinding(), + loginViewModel = loginViewModel2.value, + activity = activity, + supportFragmentManager = activity.supportFragmentManager + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt index 8223053ad8..9127e3400e 100644 --- a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt @@ -34,6 +34,7 @@ import im.vector.app.core.resources.ColorProvider import im.vector.app.databinding.DialogBaseEditTextBinding import im.vector.app.databinding.FragmentLoginAccountCreatedBinding import im.vector.app.features.displayname.getBestName +import im.vector.app.features.ftue.FTUEActivity import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.login2.AbstractLoginFragment2 diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 30ead8a6bf..dbf0024ab6 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -52,6 +52,7 @@ import im.vector.app.features.crypto.verification.SupportedVerificationMethodsPr import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.debug.DebugMenuActivity import im.vector.app.features.devtools.RoomDevToolActivity +import im.vector.app.features.ftue.FTUEActivity import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs import im.vector.app.features.home.room.detail.search.SearchActivity From 3e1801a5c4fe19f164ebd4d1bd6cf20d9e8b721d Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 8 Dec 2021 17:01:26 +0000 Subject: [PATCH 02/18] 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 + } +} From 8de892bc60fff6aa2dbc41ec3543f76afe1cde93 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 9 Dec 2021 11:03:34 +0000 Subject: [PATCH 03/18] renaming the ftue domain to onboarding and ftue to ftue auth --- .../app/core/di/MavericksViewModelModule.kt | 1 - .../app/features/ftue/DefaultFTUEVariant.kt | 363 -------- .../im/vector/app/features/ftue/FTUEAction.kt | 79 -- .../vector/app/features/ftue/FTUEActivity.kt | 85 -- .../app/features/ftue/FTUEVariantFactory.kt | 42 - .../app/features/ftue/FTUEViewEvents.kt | 50 -- .../vector/app/features/ftue/FTUEViewModel.kt | 840 ------------------ .../vector/app/features/ftue/FTUEViewState.kt | 76 -- .../login2/created/AccountCreatedFragment.kt | 1 - .../features/navigation/DefaultNavigator.kt | 1 - 10 files changed, 1538 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt delete mode 100644 vector/src/main/java/im/vector/app/features/ftue/FTUEAction.kt delete mode 100644 vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt delete mode 100644 vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt delete mode 100644 vector/src/main/java/im/vector/app/features/ftue/FTUEViewEvents.kt delete mode 100644 vector/src/main/java/im/vector/app/features/ftue/FTUEViewModel.kt delete 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 9e55e29c74..cc31a7dca6 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,7 +37,6 @@ 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 deleted file mode 100644 index 62f592ea2c..0000000000 --- a/vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt +++ /dev/null @@ -1,363 +0,0 @@ -/* - * Copyright (c) 2021 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.Intent -import android.view.View -import android.view.ViewGroup -import androidx.core.view.ViewCompat -import androidx.core.view.children -import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentTransaction -import com.airbnb.mvrx.withState -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import im.vector.app.R -import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE -import im.vector.app.core.extensions.addFragment -import im.vector.app.core.extensions.addFragmentToBackstack -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.LoginCaptchaFragment -import im.vector.app.features.login.LoginCaptchaFragmentArgument -import im.vector.app.features.login.LoginConfig -import im.vector.app.features.login.LoginFragment -import im.vector.app.features.login.LoginGenericTextInputFormFragment -import im.vector.app.features.login.LoginGenericTextInputFormFragmentArgument -import im.vector.app.features.login.LoginMode -import im.vector.app.features.login.LoginResetPasswordFragment -import im.vector.app.features.login.LoginResetPasswordMailConfirmationFragment -import im.vector.app.features.login.LoginResetPasswordSuccessFragment -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.LoginWaitForEmailFragment -import im.vector.app.features.login.LoginWaitForEmailFragmentArgument -import im.vector.app.features.login.LoginWebFragment -import im.vector.app.features.login.ServerType -import im.vector.app.features.login.SignMode -import im.vector.app.features.login.TextInputFormFragmentMode -import im.vector.app.features.login.isSupported -import im.vector.app.features.login.terms.LoginTermsFragment -import im.vector.app.features.login.terms.LoginTermsFragmentArgument -import im.vector.app.features.login.terms.toLocalizedLoginTerms -import org.matrix.android.sdk.api.auth.registration.FlowResult -import org.matrix.android.sdk.api.auth.registration.Stage -import org.matrix.android.sdk.api.extensions.tryOrNull - -private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" -private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG" - -class DefaultFTUEVariant( - private val views: ActivityLoginBinding, - private val ftueViewModel: FTUEViewModel, - private val activity: VectorBaseActivity, - private val supportFragmentManager: FragmentManager -) : FTUEVariant { - - private val enterAnim = R.anim.enter_fade_in - private val exitAnim = R.anim.exit_fade_out - - private val popEnterAnim = R.anim.no_anim - private val popExitAnim = R.anim.exit_fade_out - - private val topFragment: Fragment? - get() = supportFragmentManager.findFragmentById(R.id.loginFragmentContainer) - - private val commonOption: (FragmentTransaction) -> Unit = { ft -> - // Find the loginLogo on the current Fragment, this should not return null - (topFragment?.view as? ViewGroup) - // Find findViewById does not work, I do not know why - // findViewById(R.id.loginLogo) - ?.children - ?.firstOrNull { it.id == R.id.loginLogo } - ?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } - ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) - } - - override fun initUiAndData(isFirstCreation: Boolean) { - if (isFirstCreation) { - addFirstFragment() - } - - with(activity) { - ftueViewModel.onEach { - updateWithState(it) - } - ftueViewModel.observeViewEvents { handleLoginViewEvents(it) } - } - - // Get config extra - val loginConfig = activity.intent.getParcelableExtra(FTUEActivity.EXTRA_CONFIG) - if (isFirstCreation) { - ftueViewModel.handle(FTUEAction.InitWith(loginConfig)) - } - } - - override fun setIsLoading(isLoading: Boolean) { - // do nothing - } - - private fun addFirstFragment() { - activity.addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java) - } - - private fun handleLoginViewEvents(ftueViewEvents: FTUEViewEvents) { - when (ftueViewEvents) { - is FTUEViewEvents.RegistrationFlowResult -> { - // Check that all flows are supported by the application - if (ftueViewEvents.flowResult.missingStages.any { !it.isSupported() }) { - // Display a popup to propose use web fallback - onRegistrationStageNotSupported() - } else { - if (ftueViewEvents.isRegistrationStarted) { - // Go on with registration flow - handleRegistrationNavigation(ftueViewEvents.flowResult) - } else { - // First ask for login and password - // I add a tag to indicate that this fragment is a registration stage. - // This way it will be automatically popped in when starting the next registration stage - activity.addFragmentToBackstack(R.id.loginFragmentContainer, - LoginFragment::class.java, - tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption - ) - } - } - } - is FTUEViewEvents.OutdatedHomeserver -> { - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.login_error_outdated_homeserver_title) - .setMessage(R.string.login_error_outdated_homeserver_warning_content) - .setPositiveButton(R.string.ok, null) - .show() - Unit - } - is FTUEViewEvents.OpenServerSelection -> - activity.addFragmentToBackstack(R.id.loginFragmentContainer, - LoginServerSelectionFragment::class.java, - option = { ft -> - activity.findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } - // Disable transition of text - // findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } - // No transition here now actually - // findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } - // TODO Disabled because it provokes a flickering - // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) - }) - is FTUEViewEvents.OnServerSelectionDone -> onServerSelectionDone(ftueViewEvents) - is FTUEViewEvents.OnSignModeSelected -> onSignModeSelected(ftueViewEvents) - is FTUEViewEvents.OnLoginFlowRetrieved -> - activity.addFragmentToBackstack(R.id.loginFragmentContainer, - LoginSignUpSignInSelectionFragment::class.java, - option = commonOption) - is FTUEViewEvents.OnWebLoginError -> onWebLoginError(ftueViewEvents) - is FTUEViewEvents.OnForgetPasswordClicked -> - activity.addFragmentToBackstack(R.id.loginFragmentContainer, - LoginResetPasswordFragment::class.java, - option = commonOption) - is FTUEViewEvents.OnResetPasswordSendThreePidDone -> { - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - activity.addFragmentToBackstack(R.id.loginFragmentContainer, - LoginResetPasswordMailConfirmationFragment::class.java, - option = commonOption) - } - is FTUEViewEvents.OnResetPasswordMailConfirmationSuccess -> { - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - activity.addFragmentToBackstack(R.id.loginFragmentContainer, - LoginResetPasswordSuccessFragment::class.java, - option = commonOption) - } - is FTUEViewEvents.OnResetPasswordMailConfirmationSuccessDone -> { - // Go back to the login fragment - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - } - 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(ftueViewEvents.email), - tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - } - 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, ftueViewEvents.msisdn), - tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - } - is FTUEViewEvents.Failure, - is FTUEViewEvents.Loading -> - // This is handled by the Fragments - Unit - }.exhaustive - } - - private fun updateWithState(ftueViewState: FTUEViewState) { - if (ftueViewState.isUserLogged()) { - val intent = HomeActivity.newIntent( - activity, - accountCreation = ftueViewState.signMode == SignMode.SignUp - ) - activity.startActivity(intent) - activity.finish() - return - } - - // Loading - views.loginLoading.isVisible = ftueViewState.isLoading() - } - - private fun onWebLoginError(onWebLoginError: FTUEViewEvents.OnWebLoginError) { - // Pop the backstack - supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - - // And inform the user - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.dialog_title_error) - .setMessage(activity.getString(R.string.login_sso_error_message, onWebLoginError.description, onWebLoginError.errorCode)) - .setPositiveButton(R.string.ok, null) - .show() - } - - private fun onServerSelectionDone(loginViewEvents: FTUEViewEvents.OnServerSelectionDone) { - when (loginViewEvents.serverType) { - ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow - ServerType.EMS, - ServerType.Other -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, - LoginServerUrlFormFragment::class.java, - option = commonOption) - ServerType.Unknown -> Unit /* Should not happen */ - } - } - - 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") - SignMode.SignUp -> { - // This is managed by the LoginViewEvents - } - SignMode.SignIn -> { - // It depends on the LoginMode - when (state.loginMode) { - LoginMode.Unknown, - is LoginMode.Sso -> error("Developer error") - is LoginMode.SsoAndPassword, - LoginMode.Password -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, - LoginFragment::class.java, - tag = FRAGMENT_LOGIN_TAG, - option = commonOption) - LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) - }.exhaustive - } - SignMode.SignInWithMatrixId -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, - LoginFragment::class.java, - tag = FRAGMENT_LOGIN_TAG, - option = commonOption) - }.exhaustive - } - - /** - * Handle the SSO redirection here - */ - override fun onNewIntent(intent: Intent?) { - intent?.data - ?.let { tryOrNull { it.getQueryParameter("loginToken") } } - ?.let { ftueViewModel.handle(FTUEAction.LoginWithToken(it)) } - } - - private fun onRegistrationStageNotSupported() { - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.app_name) - .setMessage(activity.getString(R.string.login_registration_not_supported)) - .setPositiveButton(R.string.yes) { _, _ -> - activity.addFragmentToBackstack(R.id.loginFragmentContainer, - LoginWebFragment::class.java, - option = commonOption) - } - .setNegativeButton(R.string.no, null) - .show() - } - - private fun onLoginModeNotSupported(supportedTypes: List) { - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.app_name) - .setMessage(activity.getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" })) - .setPositiveButton(R.string.yes) { _, _ -> - activity.addFragmentToBackstack(R.id.loginFragmentContainer, - LoginWebFragment::class.java, - option = commonOption) - } - .setNegativeButton(R.string.no, null) - .show() - } - - private fun handleRegistrationNavigation(flowResult: FlowResult) { - // Complete all mandatory stages first - val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory } - - if (mandatoryStage != null) { - doStage(mandatoryStage) - } else { - // Consider optional stages - val optionalStage = flowResult.missingStages.firstOrNull { !it.mandatory && it !is Stage.Dummy } - if (optionalStage == null) { - // Should not happen... - } else { - doStage(optionalStage) - } - } - } - - private fun doStage(stage: Stage) { - // Ensure there is no fragment for registration stage in the backstack - supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) - - when (stage) { - is Stage.ReCaptcha -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, - LoginCaptchaFragment::class.java, - LoginCaptchaFragmentArgument(stage.publicKey), - tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - is Stage.Email -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, - LoginGenericTextInputFormFragment::class.java, - LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), - tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - is Stage.Msisdn -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, - LoginGenericTextInputFormFragment::class.java, - LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), - tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - is Stage.Terms -> activity.addFragmentToBackstack(R.id.loginFragmentContainer, - LoginTermsFragment::class.java, - LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))), - tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - else -> Unit // Should not happen - } - } -} 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 deleted file mode 100644 index b43ffd3331..0000000000 --- a/vector/src/main/java/im/vector/app/features/ftue/FTUEAction.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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 deleted file mode 100644 index c54c0547a4..0000000000 --- a/vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2021 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.content.Intent -import android.net.Uri -import com.google.android.material.appbar.MaterialToolbar -import dagger.hilt.android.AndroidEntryPoint -import im.vector.app.core.extensions.lazyViewModel -import im.vector.app.core.platform.ToolbarConfigurable -import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.core.platform.lifecycleAwareLazy -import im.vector.app.databinding.ActivityLoginBinding -import im.vector.app.features.login.LoginConfig -import im.vector.app.features.pin.UnlockedActivity -import javax.inject.Inject - -@AndroidEntryPoint -class FTUEActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { - - private val ftueVariant by lifecycleAwareLazy { - ftueVariantFactory.create(this, ftueViewModel = lazyViewModel(), loginViewModel2 = lazyViewModel()) - } - - @Inject lateinit var ftueVariantFactory: FTUEVariantFactory - - override fun getBinding() = ActivityLoginBinding.inflate(layoutInflater) - - override fun getCoordinatorLayout() = views.coordinatorLayout - - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } - - override fun onNewIntent(intent: Intent?) { - super.onNewIntent(intent) - ftueVariant.onNewIntent(intent) - } - - override fun initUiAndData() { - ftueVariant.initUiAndData(isFirstCreation()) - } - - // Hack for AccountCreatedFragment - fun setIsLoading(isLoading: Boolean) { - ftueVariant.setIsLoading(isLoading) - } - - companion object { - const val EXTRA_CONFIG = "EXTRA_CONFIG" - - fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { - return Intent(context, FTUEActivity::class.java).apply { - putExtra(EXTRA_CONFIG, loginConfig) - } - } - - fun redirectIntent(context: Context, data: Uri?): Intent { - return Intent(context, FTUEActivity::class.java).apply { - setData(data) - } - } - } -} - -interface FTUEVariant { - fun onNewIntent(intent: Intent?) - fun initUiAndData(isFirstCreation: Boolean) - fun setIsLoading(isLoading: Boolean) -} 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 deleted file mode 100644 index aabd0338a2..0000000000 --- a/vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2021 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.features.VectorFeatures -import im.vector.app.features.login2.LoginViewModel2 -import javax.inject.Inject - -class FTUEVariantFactory @Inject constructor( - private val vectorFeatures: VectorFeatures, -) { - - 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(), - ftueViewModel = ftueViewModel.value, - activity = activity, - supportFragmentManager = activity.supportFragmentManager - ) - VectorFeatures.LoginVariant.FTUE_WIP -> FTUEWipVariant( - views = activity.getBinding(), - loginViewModel = loginViewModel2.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 deleted file mode 100644 index d10063c797..0000000000 --- a/vector/src/main/java/im/vector/app/features/ftue/FTUEViewEvents.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 deleted file mode 100644 index 4c78d6fd30..0000000000 --- a/vector/src/main/java/im/vector/app/features/ftue/FTUEViewModel.kt +++ /dev/null @@ -1,840 +0,0 @@ -/* - * 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 deleted file mode 100644 index 9f34ed3782..0000000000 --- a/vector/src/main/java/im/vector/app/features/ftue/FTUEViewState.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 - } -} diff --git a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt index 9127e3400e..8223053ad8 100644 --- a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt @@ -34,7 +34,6 @@ import im.vector.app.core.resources.ColorProvider import im.vector.app.databinding.DialogBaseEditTextBinding import im.vector.app.databinding.FragmentLoginAccountCreatedBinding import im.vector.app.features.displayname.getBestName -import im.vector.app.features.ftue.FTUEActivity import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.login2.AbstractLoginFragment2 diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index dbf0024ab6..30ead8a6bf 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -52,7 +52,6 @@ import im.vector.app.features.crypto.verification.SupportedVerificationMethodsPr import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.debug.DebugMenuActivity import im.vector.app.features.devtools.RoomDevToolActivity -import im.vector.app.features.ftue.FTUEActivity import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs import im.vector.app.features.home.room.detail.search.SearchActivity From 7b3163e1405513ac9aef91ced2048067bb3c6083 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 8 Dec 2021 17:07:57 +0000 Subject: [PATCH 04/18] cloning the splash fragment into the onboarding --- .../onboarding/OnboardingVariantFactory.kt | 3 +- .../ftueauth/AbstractFtueAuthFragment.kt | 6 +- .../ftueauth/FtueAuthSplashFragment.kt | 11 +- .../FtueAuthVariant.kt} | 53 ++--- .../main/res/layout/fragment_ftue_splash.xml | 212 ++++++++++++++++++ 5 files changed, 244 insertions(+), 41 deletions(-) rename vector/src/main/java/im/vector/app/features/onboarding/{OnboardingAuthVariant.kt => ftueauth/FtueAuthVariant.kt} (87%) create mode 100644 vector/src/main/res/layout/fragment_ftue_splash.xml diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingVariantFactory.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingVariantFactory.kt index ea0ada56ba..58b5f6265d 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingVariantFactory.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingVariantFactory.kt @@ -18,6 +18,7 @@ package im.vector.app.features.onboarding import im.vector.app.features.VectorFeatures import im.vector.app.features.login2.LoginViewModel2 +import im.vector.app.features.onboarding.ftueauth.FtueAuthVariant import javax.inject.Inject class OnboardingVariantFactory @Inject constructor( @@ -29,7 +30,7 @@ class OnboardingVariantFactory @Inject constructor( loginViewModel2: Lazy ) = when (vectorFeatures.onboardingVariant()) { VectorFeatures.OnboardingVariant.LEGACY -> error("Legacy is not supported by the FTUE") - VectorFeatures.OnboardingVariant.FTUE_AUTH -> OnboardingAuthVariant( + VectorFeatures.OnboardingVariant.FTUE_AUTH -> FtueAuthVariant( views = activity.getBinding(), onboardingViewModel = onboardingViewModel.value, activity = activity, diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractFtueAuthFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractFtueAuthFragment.kt index 0caf2ea152..05dda789bb 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractFtueAuthFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractFtueAuthFragment.kt @@ -63,11 +63,11 @@ abstract class AbstractFtueAuthFragment : VectorBaseFragment showFailure(viewEvents.throwable) else -> @@ -178,6 +178,6 @@ abstract class AbstractFtueAuthFragment : VectorBaseFragment() { +) : AbstractFtueAuthFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSplashBinding { return FragmentLoginSplashBinding.inflate(inflater, container, false) @@ -63,7 +62,7 @@ class FtueAuthSplashFragment @Inject constructor( } private fun getStarted() { - loginViewModel.handle(LoginAction.OnGetStarted(resetLoginConfig = false)) + viewModel.handle(OnboardingAction.OnGetStarted(resetLoginConfig = false)) } override fun resetViewModel() { @@ -74,12 +73,12 @@ class FtueAuthSplashFragment @Inject constructor( if (throwable is Failure.NetworkConnection && throwable.ioException is UnknownHostException) { // Invalid homeserver from URL config - val url = loginViewModel.getInitialHomeServerUrl().orEmpty() + val url = viewModel.getInitialHomeServerUrl().orEmpty() MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.dialog_title_error) .setMessage(getString(R.string.login_error_homeserver_from_url_not_found, url)) .setPositiveButton(R.string.login_error_homeserver_from_url_not_found_enter_manual) { _, _ -> - loginViewModel.handle(LoginAction.OnGetStarted(resetLoginConfig = true)) + viewModel.handle(OnboardingAction.OnGetStarted(resetLoginConfig = true)) } .setNegativeButton(R.string.action_cancel, null) .show() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt similarity index 87% rename from vector/src/main/java/im/vector/app/features/onboarding/OnboardingAuthVariant.kt rename to vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index 49ceedc381..f17899dff6 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.onboarding +package im.vector.app.features.onboarding.ftueauth import android.content.Intent import android.view.View @@ -42,21 +42,12 @@ import im.vector.app.features.login.SignMode import im.vector.app.features.login.TextInputFormFragmentMode import im.vector.app.features.login.isSupported import im.vector.app.features.login.terms.toLocalizedLoginTerms -import im.vector.app.features.onboarding.ftueauth.FtueAuthCaptchaFragment -import im.vector.app.features.onboarding.ftueauth.FtueAuthCaptchaFragmentArgument -import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragment -import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragmentArgument -import im.vector.app.features.onboarding.ftueauth.FtueAuthLoginFragment -import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordFragment -import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordMailConfirmationFragment -import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordSuccessFragment -import im.vector.app.features.onboarding.ftueauth.FtueAuthServerSelectionFragment -import im.vector.app.features.onboarding.ftueauth.FtueAuthServerUrlFormFragment -import im.vector.app.features.onboarding.ftueauth.FtueAuthSignUpSignInSelectionFragment -import im.vector.app.features.onboarding.ftueauth.FtueAuthSplashFragment -import im.vector.app.features.onboarding.ftueauth.FtueAuthWaitForEmailFragment -import im.vector.app.features.onboarding.ftueauth.FtueAuthWaitForEmailFragmentArgument -import im.vector.app.features.onboarding.ftueauth.FtueAuthWebFragment +import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.OnboardingActivity +import im.vector.app.features.onboarding.OnboardingVariant +import im.vector.app.features.onboarding.OnboardingViewEvents +import im.vector.app.features.onboarding.OnboardingViewModel +import im.vector.app.features.onboarding.OnboardingViewState import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsFragment import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsFragmentArgument import org.matrix.android.sdk.api.auth.registration.FlowResult @@ -66,7 +57,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG" -class OnboardingAuthVariant( +class FtueAuthVariant( private val views: ActivityLoginBinding, private val onboardingViewModel: OnboardingViewModel, private val activity: VectorBaseActivity, @@ -120,17 +111,17 @@ class OnboardingAuthVariant( activity.addFragment(views.loginFragmentContainer, FtueAuthSplashFragment::class.java) } - private fun handleOnboardingViewEvents(onboardingViewEvents: OnboardingViewEvents) { - when (onboardingViewEvents) { + private fun handleOnboardingViewEvents(viewEvents: OnboardingViewEvents) { + when (viewEvents) { is OnboardingViewEvents.RegistrationFlowResult -> { // Check that all flows are supported by the application - if (onboardingViewEvents.flowResult.missingStages.any { !it.isSupported() }) { + if (viewEvents.flowResult.missingStages.any { !it.isSupported() }) { // Display a popup to propose use web fallback onRegistrationStageNotSupported() } else { - if (onboardingViewEvents.isRegistrationStarted) { + if (viewEvents.isRegistrationStarted) { // Go on with registration flow - handleRegistrationNavigation(onboardingViewEvents.flowResult) + handleRegistrationNavigation(viewEvents.flowResult) } else { // First ask for login and password // I add a tag to indicate that this fragment is a registration stage. @@ -163,13 +154,13 @@ class OnboardingAuthVariant( // TODO Disabled because it provokes a flickering // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) }) - is OnboardingViewEvents.OnServerSelectionDone -> onServerSelectionDone(onboardingViewEvents) - is OnboardingViewEvents.OnSignModeSelected -> onSignModeSelected(onboardingViewEvents) + is OnboardingViewEvents.OnServerSelectionDone -> onServerSelectionDone(viewEvents) + is OnboardingViewEvents.OnSignModeSelected -> onSignModeSelected(viewEvents) is OnboardingViewEvents.OnLoginFlowRetrieved -> activity.addFragmentToBackstack(views.loginFragmentContainer, FtueAuthSignUpSignInSelectionFragment::class.java, option = commonOption) - is OnboardingViewEvents.OnWebLoginError -> onWebLoginError(onboardingViewEvents) + is OnboardingViewEvents.OnWebLoginError -> onWebLoginError(viewEvents) is OnboardingViewEvents.OnForgetPasswordClicked -> activity.addFragmentToBackstack(views.loginFragmentContainer, FtueAuthResetPasswordFragment::class.java, @@ -195,7 +186,7 @@ class OnboardingAuthVariant( supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) activity.addFragmentToBackstack(views.loginFragmentContainer, FtueAuthWaitForEmailFragment::class.java, - FtueAuthWaitForEmailFragmentArgument(onboardingViewEvents.email), + FtueAuthWaitForEmailFragmentArgument(viewEvents.email), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) } @@ -204,7 +195,7 @@ class OnboardingAuthVariant( supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) activity.addFragmentToBackstack(views.loginFragmentContainer, FtueAuthGenericTextInputFormFragment::class.java, - FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, onboardingViewEvents.msisdn), + FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, viewEvents.msisdn), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) } @@ -215,11 +206,11 @@ class OnboardingAuthVariant( }.exhaustive } - private fun updateWithState(onboardingViewState: OnboardingViewState) { - if (onboardingViewState.isUserLogged()) { + private fun updateWithState(viewState: OnboardingViewState) { + if (viewState.isUserLogged()) { val intent = HomeActivity.newIntent( activity, - accountCreation = onboardingViewState.signMode == SignMode.SignUp + accountCreation = viewState.signMode == SignMode.SignUp ) activity.startActivity(intent) activity.finish() @@ -227,7 +218,7 @@ class OnboardingAuthVariant( } // Loading - views.loginLoading.isVisible = onboardingViewState.isLoading() + views.loginLoading.isVisible = viewState.isLoading() } private fun onWebLoginError(onWebLoginError: OnboardingViewEvents.OnWebLoginError) { diff --git a/vector/src/main/res/layout/fragment_ftue_splash.xml b/vector/src/main/res/layout/fragment_ftue_splash.xml new file mode 100644 index 0000000000..96b3a7cbfb --- /dev/null +++ b/vector/src/main/res/layout/fragment_ftue_splash.xml @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +