diff --git a/vector/build.gradle b/vector/build.gradle index 07f80a70b2..1949710fc9 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -140,7 +140,7 @@ android { buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\"" resValue "string", "build_number", "\"${buildNumber}\"" - buildConfigField "im.vector.app.features.VectorFeatures.LoginVariant", "LOGIN_VARIANT", "im.vector.app.features.VectorFeatures.LoginVariant.LEGACY" + buildConfigField "im.vector.app.features.VectorFeatures.OnboardingVariant", "ONBOARDING_VARIANT", "im.vector.app.features.VectorFeatures.OnboardingVariant.LEGACY" buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping" diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt index ca5d26aaeb..637071dd59 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt @@ -27,9 +27,9 @@ class DebugFeaturesStateFactory @Inject constructor( fun create(): FeaturesState { return FeaturesState(listOf( createEnumFeature( - label = "Login version", - selection = debugFeatures.loginVariant(), - default = defaultFeatures.loginVariant() + label = "Onboarding variant", + selection = debugFeatures.onboardingVariant(), + default = defaultFeatures.onboardingVariant() ) )) } diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index 638509e76b..4dbb6a5698 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -38,8 +38,8 @@ class DebugVectorFeatures( private val dataStore = context.dataStore - override fun loginVariant(): VectorFeatures.LoginVariant { - return readPreferences().getEnum() ?: vectorFeatures.loginVariant() + override fun onboardingVariant(): VectorFeatures.OnboardingVariant { + return readPreferences().getEnum() ?: vectorFeatures.onboardingVariant() } fun > hasEnumOverride(type: KClass) = readPreferences().containsEnum(type) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 683aebe754..14796f9d2e 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -137,7 +137,7 @@ android:windowSoftInputMode="adjustResize" /> 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 09e161bc99..168c5a3819 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 @@ -58,6 +58,7 @@ import im.vector.app.features.login.LoginViewModel import im.vector.app.features.login2.LoginViewModel2 import im.vector.app.features.login2.created.AccountCreatedViewModel import im.vector.app.features.matrixto.MatrixToBottomSheetViewModel +import im.vector.app.features.onboarding.OnboardingViewModel import im.vector.app.features.poll.create.CreatePollViewModel import im.vector.app.features.rageshake.BugReportViewModel import im.vector.app.features.reactions.EmojiSearchResultViewModel @@ -453,6 +454,11 @@ interface MavericksViewModelModule { @MavericksViewModelKey(AccountCreatedViewModel::class) fun accountCreatedViewModelFactory(factory: AccountCreatedViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds + @IntoMap + @MavericksViewModelKey(OnboardingViewModel::class) + fun ftueViewModelFactory(factory: OnboardingViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds @IntoMap @MavericksViewModelKey(LoginViewModel2::class) diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index 58594be293..4228c6ebcd 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -20,12 +20,12 @@ import im.vector.app.BuildConfig interface VectorFeatures { - fun loginVariant(): LoginVariant + fun onboardingVariant(): OnboardingVariant - enum class LoginVariant { + enum class OnboardingVariant { LEGACY, - FTUE, - FTUE_WIP + LOGIN_2, + FTUE_AUTH } enum class NotificationSettingsVersion { @@ -35,5 +35,5 @@ interface VectorFeatures { } class DefaultVectorFeatures : VectorFeatures { - override fun loginVariant(): VectorFeatures.LoginVariant = BuildConfig.LOGIN_VARIANT + override fun onboardingVariant(): VectorFeatures.OnboardingVariant = BuildConfig.ONBOARDING_VARIANT } 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 92fd26e5e8..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,12 +34,12 @@ 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 import im.vector.app.features.login2.LoginAction2 import im.vector.app.features.login2.LoginViewState2 +import im.vector.app.features.onboarding.OnboardingActivity import org.matrix.android.sdk.api.util.MatrixItem import java.util.UUID import javax.inject.Inject @@ -130,7 +130,7 @@ class AccountCreatedFragment @Inject constructor( private fun invalidateState(state: AccountCreatedViewState) { // Ugly hack... - (activity as? FTUEActivity)?.setIsLoading(state.isLoading) + (activity as? OnboardingActivity)?.setIsLoading(state.isLoading) views.loginAccountCreatedSubtitle.text = getString(R.string.login_account_created_subtitle, state.userId) 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 393406a07d..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 @@ -38,6 +38,7 @@ import im.vector.app.core.error.fatalError import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.toast import im.vector.app.features.VectorFeatures +import im.vector.app.features.VectorFeatures.OnboardingVariant import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity import im.vector.app.features.call.conference.JitsiCallViewModel import im.vector.app.features.call.conference.VectorJitsiActivity @@ -51,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 @@ -64,6 +64,7 @@ import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.BigImageViewerActivity import im.vector.app.features.media.VectorAttachmentViewerActivity +import im.vector.app.features.onboarding.OnboardingActivity import im.vector.app.features.pin.PinActivity import im.vector.app.features.pin.PinArgs import im.vector.app.features.pin.PinMode @@ -111,20 +112,20 @@ class DefaultNavigator @Inject constructor( ) : Navigator { override fun openLogin(context: Context, loginConfig: LoginConfig?, flags: Int) { - val intent = when (features.loginVariant()) { - VectorFeatures.LoginVariant.LEGACY -> LoginActivity.newIntent(context, loginConfig) - VectorFeatures.LoginVariant.FTUE, - VectorFeatures.LoginVariant.FTUE_WIP -> FTUEActivity.newIntent(context, loginConfig) + val intent = when (features.onboardingVariant()) { + OnboardingVariant.LEGACY -> LoginActivity.newIntent(context, loginConfig) + OnboardingVariant.LOGIN_2, + OnboardingVariant.FTUE_AUTH -> OnboardingActivity.newIntent(context, loginConfig) } intent.addFlags(flags) context.startActivity(intent) } override fun loginSSORedirect(context: Context, data: Uri?) { - val intent = when (features.loginVariant()) { - VectorFeatures.LoginVariant.LEGACY -> LoginActivity.redirectIntent(context, data) - VectorFeatures.LoginVariant.FTUE, - VectorFeatures.LoginVariant.FTUE_WIP -> FTUEActivity.redirectIntent(context, data) + val intent = when (features.onboardingVariant()) { + OnboardingVariant.LEGACY -> LoginActivity.redirectIntent(context, data) + OnboardingVariant.LOGIN_2, + OnboardingVariant.FTUE_AUTH -> OnboardingActivity.redirectIntent(context, data) } context.startActivity(intent) } diff --git a/vector/src/main/java/im/vector/app/features/ftue/FTUEWipVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt similarity index 99% rename from vector/src/main/java/im/vector/app/features/ftue/FTUEWipVariant.kt rename to vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt index c1fc49db00..107c08da5a 100644 --- a/vector/src/main/java/im/vector/app/features/ftue/FTUEWipVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.ftue +package im.vector.app.features.onboarding import android.content.Intent import android.view.View @@ -72,12 +72,12 @@ 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 FTUEWipVariant( +class Login2Variant( private val views: ActivityLoginBinding, private val loginViewModel: LoginViewModel2, private val activity: VectorBaseActivity, private val supportFragmentManager: FragmentManager -) : FTUEVariant { +) : OnboardingVariant { private val enterAnim = R.anim.enter_fade_in private val exitAnim = R.anim.exit_fade_out @@ -112,7 +112,7 @@ class FTUEWipVariant( } // Get config extra - val loginConfig = activity.intent.getParcelableExtra(FTUEActivity.EXTRA_CONFIG) + val loginConfig = activity.intent.getParcelableExtra(OnboardingActivity.EXTRA_CONFIG) if (isFirstCreation) { // TODO Check this loginViewModel.handle(LoginAction2.InitWith(loginConfig)) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt new file mode 100644 index 0000000000..4af0825044 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.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.onboarding + +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 OnboardingAction : VectorViewModelAction { + data class OnGetStarted(val resetLoginConfig: Boolean) : OnboardingAction() + + data class UpdateServerType(val serverType: ServerType) : OnboardingAction() + data class UpdateHomeServer(val homeServerUrl: String) : OnboardingAction() + data class UpdateSignMode(val signMode: SignMode) : OnboardingAction() + data class LoginWithToken(val loginToken: String) : OnboardingAction() + data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction() + data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction() + data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction() + object ResetPasswordMailConfirmed : OnboardingAction() + + // Login or Register, depending on the signMode + data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction() + + // Register actions + open class RegisterAction : OnboardingAction() + + 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 : OnboardingAction() + + object ResetHomeServerType : ResetAction() + object ResetHomeServerUrl : ResetAction() + object ResetSignMode : ResetAction() + object ResetLogin : ResetAction() + object ResetResetPassword : ResetAction() + + // Homeserver history + object ClearHomeServerHistory : OnboardingAction() + + // For the soft logout case + data class SetupSsoForSessionRecovery(val homeServerUrl: String, + val deviceId: String, + val ssoIdentityProviders: List?) : OnboardingAction() + + data class PostViewEvent(val viewEvent: OnboardingViewEvents) : OnboardingAction() + + data class UserAcceptCertificate(val fingerprint: Fingerprint) : OnboardingAction() +} diff --git a/vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingActivity.kt similarity index 71% rename from vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt rename to vector/src/main/java/im/vector/app/features/onboarding/OnboardingActivity.kt index 805e39c48d..0db9fb0c76 100644 --- a/vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingActivity.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.ftue +package im.vector.app.features.onboarding import android.content.Context import android.content.Intent @@ -31,13 +31,13 @@ import im.vector.app.features.pin.UnlockedActivity import javax.inject.Inject @AndroidEntryPoint -class FTUEActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { +class OnboardingActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { - private val ftueVariant by lifecycleAwareLazy { - ftueVariantFactory.create(this, loginViewModel = lazyViewModel(), loginViewModel2 = lazyViewModel()) + private val onboardingVariant by lifecycleAwareLazy { + onboardingVariantFactory.create(this, onboardingViewModel = lazyViewModel(), loginViewModel2 = lazyViewModel()) } - @Inject lateinit var ftueVariantFactory: FTUEVariantFactory + @Inject lateinit var onboardingVariantFactory: OnboardingVariantFactory override fun getBinding() = ActivityLoginBinding.inflate(layoutInflater) @@ -49,37 +49,31 @@ class FTUEActivity : VectorBaseActivity(), ToolbarConfigur override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - ftueVariant.onNewIntent(intent) + onboardingVariant.onNewIntent(intent) } override fun initUiAndData() { - ftueVariant.initUiAndData(isFirstCreation()) + onboardingVariant.initUiAndData(isFirstCreation()) } // Hack for AccountCreatedFragment fun setIsLoading(isLoading: Boolean) { - ftueVariant.setIsLoading(isLoading) + onboardingVariant.setIsLoading(isLoading) } companion object { const val EXTRA_CONFIG = "EXTRA_CONFIG" fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { - return Intent(context, FTUEActivity::class.java).apply { + return Intent(context, OnboardingActivity::class.java).apply { putExtra(EXTRA_CONFIG, loginConfig) } } fun redirectIntent(context: Context, data: Uri?): Intent { - return Intent(context, FTUEActivity::class.java).apply { + return Intent(context, OnboardingActivity::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/DefaultFTUEVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAuthVariant.kt similarity index 84% rename from vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt rename to vector/src/main/java/im/vector/app/features/onboarding/OnboardingAuthVariant.kt index 98b1f98df0..8e26a17138 100644 --- a/vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAuthVariant.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.ftue +package im.vector.app.features.onboarding import android.content.Intent import android.view.View @@ -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 @@ -70,12 +66,12 @@ 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( +class OnboardingAuthVariant( private val views: ActivityLoginBinding, - private val loginViewModel: LoginViewModel, + private val onboardingViewModel: OnboardingViewModel, private val activity: VectorBaseActivity, private val supportFragmentManager: FragmentManager -) : FTUEVariant { +) : OnboardingVariant { private val enterAnim = R.anim.enter_fade_in private val exitAnim = R.anim.exit_fade_out @@ -103,16 +99,16 @@ class DefaultFTUEVariant( } with(activity) { - loginViewModel.onEach { + onboardingViewModel.onEach { updateWithState(it) } - loginViewModel.observeViewEvents { handleLoginViewEvents(it) } + onboardingViewModel.observeViewEvents { handleLoginViewEvents(it) } } // Get config extra - val loginConfig = activity.intent.getParcelableExtra(FTUEActivity.EXTRA_CONFIG) + val loginConfig = activity.intent.getParcelableExtra(OnboardingActivity.EXTRA_CONFIG) if (isFirstCreation) { - loginViewModel.handle(LoginAction.InitWith(loginConfig)) + onboardingViewModel.handle(OnboardingAction.InitWith(loginConfig)) } } @@ -124,17 +120,17 @@ class DefaultFTUEVariant( activity.addFragment(views.loginFragmentContainer, LoginSplashFragment::class.java) } - private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) { - when (loginViewEvents) { - is LoginViewEvents.RegistrationFlowResult -> { + private fun handleLoginViewEvents(onboardingViewEvents: OnboardingViewEvents) { + when (onboardingViewEvents) { + is OnboardingViewEvents.RegistrationFlowResult -> { // Check that all flows are supported by the application - if (loginViewEvents.flowResult.missingStages.any { !it.isSupported() }) { + if (onboardingViewEvents.flowResult.missingStages.any { !it.isSupported() }) { // Display a popup to propose use web fallback onRegistrationStageNotSupported() } else { - if (loginViewEvents.isRegistrationStarted) { + if (onboardingViewEvents.isRegistrationStarted) { // Go on with registration flow - handleRegistrationNavigation(loginViewEvents.flowResult) + handleRegistrationNavigation(onboardingViewEvents.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 OnboardingViewEvents.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 OnboardingViewEvents.OpenServerSelection -> activity.addFragmentToBackstack(views.loginFragmentContainer, LoginServerSelectionFragment::class.java, option = { ft -> @@ -167,63 +163,63 @@ class DefaultFTUEVariant( // TODO Disabled because it provokes a flickering // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) }) - is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone(loginViewEvents) - is LoginViewEvents.OnSignModeSelected -> onSignModeSelected(loginViewEvents) - is LoginViewEvents.OnLoginFlowRetrieved -> + is OnboardingViewEvents.OnServerSelectionDone -> onServerSelectionDone(onboardingViewEvents) + is OnboardingViewEvents.OnSignModeSelected -> onSignModeSelected(onboardingViewEvents) + is OnboardingViewEvents.OnLoginFlowRetrieved -> activity.addFragmentToBackstack(views.loginFragmentContainer, LoginSignUpSignInSelectionFragment::class.java, option = commonOption) - is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents) - is LoginViewEvents.OnForgetPasswordClicked -> + is OnboardingViewEvents.OnWebLoginError -> onWebLoginError(onboardingViewEvents) + is OnboardingViewEvents.OnForgetPasswordClicked -> activity.addFragmentToBackstack(views.loginFragmentContainer, LoginResetPasswordFragment::class.java, option = commonOption) - is LoginViewEvents.OnResetPasswordSendThreePidDone -> { + is OnboardingViewEvents.OnResetPasswordSendThreePidDone -> { supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) activity.addFragmentToBackstack(views.loginFragmentContainer, LoginResetPasswordMailConfirmationFragment::class.java, option = commonOption) } - is LoginViewEvents.OnResetPasswordMailConfirmationSuccess -> { + is OnboardingViewEvents.OnResetPasswordMailConfirmationSuccess -> { supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) activity.addFragmentToBackstack(views.loginFragmentContainer, LoginResetPasswordSuccessFragment::class.java, option = commonOption) } - is LoginViewEvents.OnResetPasswordMailConfirmationSuccessDone -> { + is OnboardingViewEvents.OnResetPasswordMailConfirmationSuccessDone -> { // Go back to the login fragment supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) } - is LoginViewEvents.OnSendEmailSuccess -> { + is OnboardingViewEvents.OnSendEmailSuccess -> { // Pop the enter email Fragment supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) activity.addFragmentToBackstack(views.loginFragmentContainer, LoginWaitForEmailFragment::class.java, - LoginWaitForEmailFragmentArgument(loginViewEvents.email), + LoginWaitForEmailFragmentArgument(onboardingViewEvents.email), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) } - is LoginViewEvents.OnSendMsisdnSuccess -> { + is OnboardingViewEvents.OnSendMsisdnSuccess -> { // Pop the enter Msisdn Fragment supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) activity.addFragmentToBackstack(views.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, - LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginViewEvents.msisdn), + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, onboardingViewEvents.msisdn), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) } - is LoginViewEvents.Failure, - is LoginViewEvents.Loading -> + is OnboardingViewEvents.Failure, + is OnboardingViewEvents.Loading -> // This is handled by the Fragments Unit }.exhaustive } - private fun updateWithState(loginViewState: LoginViewState) { - if (loginViewState.isUserLogged()) { + private fun updateWithState(onboardingViewState: OnboardingViewState) { + if (onboardingViewState.isUserLogged()) { val intent = HomeActivity.newIntent( activity, - accountCreation = loginViewState.signMode == SignMode.SignUp + accountCreation = onboardingViewState.signMode == SignMode.SignUp ) activity.startActivity(intent) activity.finish() @@ -231,10 +227,10 @@ class DefaultFTUEVariant( } // Loading - views.loginLoading.isVisible = loginViewState.isLoading() + views.loginLoading.isVisible = onboardingViewState.isLoading() } - private fun onWebLoginError(onWebLoginError: LoginViewEvents.OnWebLoginError) { + private fun onWebLoginError(onWebLoginError: OnboardingViewEvents.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: OnboardingViewEvents.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: OnboardingViewEvents.OnSignModeSelected) = withState(onboardingViewModel) { 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 { onboardingViewModel.handle(OnboardingAction.LoginWithToken(it)) } } private fun onRegistrationStageNotSupported() { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingVariant.kt new file mode 100644 index 0000000000..91c125fa5b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingVariant.kt @@ -0,0 +1,25 @@ +/* + * 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.onboarding + +import android.content.Intent + +interface OnboardingVariant { + 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/onboarding/OnboardingVariantFactory.kt similarity index 65% rename from vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt rename to vector/src/main/java/im/vector/app/features/onboarding/OnboardingVariantFactory.kt index 7efd6023fe..ea0ada56ba 100644 --- a/vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingVariantFactory.kt @@ -14,26 +14,28 @@ * limitations under the License. */ -package im.vector.app.features.ftue +package im.vector.app.features.onboarding 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( +class OnboardingVariantFactory @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( + fun create(activity: OnboardingActivity, + onboardingViewModel: Lazy, + loginViewModel2: Lazy + ) = when (vectorFeatures.onboardingVariant()) { + VectorFeatures.OnboardingVariant.LEGACY -> error("Legacy is not supported by the FTUE") + VectorFeatures.OnboardingVariant.FTUE_AUTH -> OnboardingAuthVariant( views = activity.getBinding(), - loginViewModel = loginViewModel.value, + onboardingViewModel = onboardingViewModel.value, activity = activity, supportFragmentManager = activity.supportFragmentManager ) - VectorFeatures.LoginVariant.FTUE_WIP -> FTUEWipVariant( + VectorFeatures.OnboardingVariant.LOGIN_2 -> Login2Variant( views = activity.getBinding(), loginViewModel = loginViewModel2.value, activity = activity, diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt new file mode 100644 index 0000000000..ab782a9908 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.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.onboarding + +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 OnboardingViewEvents : VectorViewEvents { + data class Loading(val message: CharSequence? = null) : OnboardingViewEvents() + data class Failure(val throwable: Throwable) : OnboardingViewEvents() + + data class RegistrationFlowResult(val flowResult: FlowResult, val isRegistrationStarted: Boolean) : OnboardingViewEvents() + object OutdatedHomeserver : OnboardingViewEvents() + + // Navigation event + + object OpenServerSelection : OnboardingViewEvents() + data class OnServerSelectionDone(val serverType: ServerType) : OnboardingViewEvents() + object OnLoginFlowRetrieved : OnboardingViewEvents() + data class OnSignModeSelected(val signMode: SignMode) : OnboardingViewEvents() + object OnForgetPasswordClicked : OnboardingViewEvents() + object OnResetPasswordSendThreePidDone : OnboardingViewEvents() + object OnResetPasswordMailConfirmationSuccess : OnboardingViewEvents() + object OnResetPasswordMailConfirmationSuccessDone : OnboardingViewEvents() + + data class OnSendEmailSuccess(val email: String) : OnboardingViewEvents() + data class OnSendMsisdnSuccess(val msisdn: String) : OnboardingViewEvents() + + data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : OnboardingViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt new file mode 100644 index 0000000000..32a6d63f9b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.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.onboarding + +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 OnboardingViewModel @AssistedInject constructor( + @Assisted initialState: OnboardingViewState, + 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: OnboardingViewState): OnboardingViewModel + } + + 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: OnboardingAction? = 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: OnboardingAction) { + when (action) { + is OnboardingAction.OnGetStarted -> handleOnGetStarted(action) + is OnboardingAction.UpdateServerType -> handleUpdateServerType(action) + is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action) + is OnboardingAction.InitWith -> handleInitWith(action) + is OnboardingAction.UpdateHomeServer -> handleUpdateHomeserver(action).also { lastAction = action } + is OnboardingAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action } + is OnboardingAction.LoginWithToken -> handleLoginWithToken(action) + is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action) + is OnboardingAction.ResetPassword -> handleResetPassword(action) + is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() + is OnboardingAction.RegisterAction -> handleRegisterAction(action) + is OnboardingAction.ResetAction -> handleResetAction(action) + is OnboardingAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action) + is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action) + OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory() + is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent) + }.exhaustive + } + + private fun handleOnGetStarted(action: OnboardingAction.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(OnboardingViewEvents.OpenServerSelection) + } else { + getLoginFlow(homeServerConnectionConfig, ServerType.Other) + } + } else { + _viewEvents.post(OnboardingViewEvents.OpenServerSelection) + } + } + + private fun handleUserAcceptCertificate(action: OnboardingAction.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 OnboardingAction.UpdateHomeServer -> { + currentHomeServerConnectionConfig + ?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) } + ?.let { getLoginFlow(it) } + } + is OnboardingAction.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: OnboardingAction.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(OnboardingViewEvents.Failure(failure)) + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + null + } + ?.let { onSessionCreated(it) } + } + } + } + + private fun handleSetupSsoForSessionRecovery(action: OnboardingAction.SetupSsoForSessionRecovery) { + setState { + copy( + signMode = SignMode.SignIn, + loginMode = LoginMode.Sso(action.ssoIdentityProviders), + homeServerUrlFromUser = action.homeServerUrl, + homeServerUrl = action.homeServerUrl, + deviceId = action.deviceId + ) + } + } + + private fun handleRegisterAction(action: OnboardingAction.RegisterAction) { + when (action) { + is OnboardingAction.CaptchaDone -> handleCaptchaDone(action) + is OnboardingAction.AcceptTerms -> handleAcceptTerms() + is OnboardingAction.RegisterDummy -> handleRegisterDummy() + is OnboardingAction.AddThreePid -> handleAddThreePid(action) + is OnboardingAction.SendAgainThreePid -> handleSendAgainThreePid() + is OnboardingAction.ValidateThreePid -> handleValidateThreePid(action) + is OnboardingAction.CheckIfEmailHasBeenValidated -> handleCheckIfEmailHasBeenValidated(action) + is OnboardingAction.StopEmailValidationCheck -> handleStopEmailValidationCheck() + } + } + + private fun handleCheckIfEmailHasBeenValidated(action: OnboardingAction.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: OnboardingAction.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(OnboardingViewEvents.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: OnboardingAction.AddThreePid) { + setState { copy(asyncRegistration = Loading()) } + currentJob = viewModelScope.launch { + try { + registrationWizard?.addThreePid(action.threePid) + } catch (failure: Throwable) { + _viewEvents.post(OnboardingViewEvents.Failure(failure)) + } + setState { + copy( + asyncRegistration = Uninitialized + ) + } + } + } + + private fun handleSendAgainThreePid() { + setState { copy(asyncRegistration = Loading()) } + currentJob = viewModelScope.launch { + try { + registrationWizard?.sendAgainThreePid() + } catch (failure: Throwable) { + _viewEvents.post(OnboardingViewEvents.Failure(failure)) + } + setState { + copy( + asyncRegistration = Uninitialized + ) + } + } + } + + private fun handleAcceptTerms() { + currentJob = executeRegistrationStep { + it.acceptTerms() + } + } + + private fun handleRegisterDummy() { + currentJob = executeRegistrationStep { + it.dummy() + } + } + + private fun handleRegisterWith(action: OnboardingAction.LoginOrRegister) { + reAuthHelper.data = action.password + currentJob = executeRegistrationStep { + it.createAccount( + action.username, + action.password, + action.initialDeviceName + ) + } + } + + private fun handleCaptchaDone(action: OnboardingAction.CaptchaDone) { + currentJob = executeRegistrationStep { + it.performReCaptcha(action.captchaResponse) + } + } + + private fun handleResetAction(action: OnboardingAction.ResetAction) { + // Cancel any request + currentJob = null + + when (action) { + OnboardingAction.ResetHomeServerType -> { + setState { + copy( + serverType = ServerType.Unknown + ) + } + } + OnboardingAction.ResetHomeServerUrl -> { + viewModelScope.launch { + authenticationService.reset() + setState { + copy( + asyncHomeServerLoginFlowRequest = Uninitialized, + homeServerUrlFromUser = null, + homeServerUrl = null, + loginMode = LoginMode.Unknown, + serverType = ServerType.Unknown, + loginModeSupportedTypes = emptyList() + ) + } + } + } + OnboardingAction.ResetSignMode -> { + setState { + copy( + asyncHomeServerLoginFlowRequest = Uninitialized, + signMode = SignMode.Unknown, + loginMode = LoginMode.Unknown, + loginModeSupportedTypes = emptyList() + ) + } + } + OnboardingAction.ResetLogin -> { + viewModelScope.launch { + authenticationService.cancelPendingLoginOrRegistration() + setState { + copy( + asyncLoginAction = Uninitialized, + asyncRegistration = Uninitialized + ) + } + } + } + OnboardingAction.ResetResetPassword -> { + setState { + copy( + asyncResetPassword = Uninitialized, + asyncResetMailConfirmed = Uninitialized, + resetPasswordEmail = null + ) + } + } + } + } + + private fun handleUpdateSignMode(action: OnboardingAction.UpdateSignMode) { + setState { + copy( + signMode = action.signMode + ) + } + + when (action.signMode) { + SignMode.SignUp -> startRegistrationFlow() + SignMode.SignIn -> startAuthenticationFlow() + SignMode.SignInWithMatrixId -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId)) + SignMode.Unknown -> Unit + } + } + + private fun handleUpdateServerType(action: OnboardingAction.UpdateServerType) { + setState { + copy( + serverType = action.serverType + ) + } + + when (action.serverType) { + ServerType.Unknown -> Unit /* Should not happen */ + ServerType.MatrixOrg -> + // Request login flow here + handle(OnboardingAction.UpdateHomeServer(matrixOrgUrl)) + ServerType.EMS, + ServerType.Other -> _viewEvents.post(OnboardingViewEvents.OnServerSelectionDone(action.serverType)) + }.exhaustive + } + + private fun handleInitWith(action: OnboardingAction.InitWith) { + loginConfig = action.loginConfig + + // If there is a pending email validation continue on this step + try { + if (registrationWizard?.isRegistrationStarted == true) { + currentThreePid?.let { + handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.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: OnboardingAction.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(OnboardingViewEvents.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(OnboardingViewEvents.OnResetPasswordMailConfirmationSuccess) + } + } + } + + private fun handleLoginOrRegister(action: OnboardingAction.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: OnboardingAction.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(OnboardingViewEvents.Failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error)))) + } + + private suspend fun onWellknownSuccess(action: OnboardingAction.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(OnboardingViewEvents.Failure(failure)) + setState { + copy( + asyncLoginAction = Uninitialized + ) + } + } + else -> { + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + } + } + + private fun handleLogin(action: OnboardingAction.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(OnboardingViewEvents.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(OnboardingViewEvents.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: OnboardingAction.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: OnboardingAction.UpdateHomeServer) { + val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl) + if (homeServerConnectionConfig == null) { + // This is invalid + _viewEvents.post(OnboardingViewEvents.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(OnboardingViewEvents.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(OnboardingViewEvents.OutdatedHomeserver) + } + _viewEvents.post(OnboardingViewEvents.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/onboarding/OnboardingViewState.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt new file mode 100644 index 0000000000..7a6537f433 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.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.onboarding + +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 OnboardingViewState( + 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 + } +}