diff --git a/changelog.d/2396.wip b/changelog.d/2396.wip new file mode 100644 index 0000000000..9af8fa16c0 --- /dev/null +++ b/changelog.d/2396.wip @@ -0,0 +1 @@ +Adds a new homeserver selection screen when creating an account \ No newline at end of file diff --git a/vector-config/src/main/res/values/urls.xml b/vector-config/src/main/res/values/urls.xml index 22e3a9ac72..70f5227f5f 100644 --- a/vector-config/src/main/res/values/urls.xml +++ b/vector-config/src/main/res/values/urls.xml @@ -3,4 +3,5 @@ https://element.io/help#threads + https://element.io/ems \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/core/extensions/Collections.kt b/vector/src/main/java/im/vector/app/core/extensions/Collections.kt index 5168915c9c..d48875983d 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Collections.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Collections.kt @@ -18,3 +18,5 @@ package im.vector.app.core.extensions inline fun List.nextOrNull(index: Int) = getOrNull(index + 1) inline fun List.prevOrNull(index: Int) = getOrNull(index - 1) + +fun List.containsAllItems(vararg items: T) = this.containsAll(items.toList()) diff --git a/vector/src/main/java/im/vector/app/core/extensions/UrlExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/UrlExtensions.kt index 5037f78445..749da0d987 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/UrlExtensions.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/UrlExtensions.kt @@ -19,8 +19,8 @@ package im.vector.app.core.extensions /** * Ex: "https://matrix.org/" -> "matrix.org" */ -fun String?.toReducedUrl(): String { +fun String?.toReducedUrl(keepSchema: Boolean = false): String { return (this ?: "") - .substringAfter("://") + .run { if (keepSchema) this else substringAfter("://") } .trim { it == '/' } } 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 index 7510f23584..59c3d10a5e 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt @@ -29,7 +29,14 @@ sealed interface OnboardingAction : VectorViewModelAction { data class OnIAlreadyHaveAnAccount(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction data class UpdateServerType(val serverType: ServerType) : OnboardingAction - data class UpdateHomeServer(val homeServerUrl: String) : OnboardingAction + + sealed interface HomeServerChange : OnboardingAction { + val homeServerUrl: String + + data class SelectHomeServer(override val homeServerUrl: String) : HomeServerChange + data class EditHomeServer(override val homeServerUrl: String) : HomeServerChange + } + data class UpdateUseCase(val useCase: FtueUseCase) : OnboardingAction object ResetUseCase : OnboardingAction data class UpdateSignMode(val signMode: SignMode) : OnboardingAction 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 index 853b0c330e..ee406aff48 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt @@ -37,8 +37,10 @@ sealed class OnboardingViewEvents : VectorViewEvents { object OpenUseCaseSelection : OnboardingViewEvents() object OpenServerSelection : OnboardingViewEvents() object OpenCombinedRegister : OnboardingViewEvents() + object EditServerSelection : OnboardingViewEvents() data class OnServerSelectionDone(val serverType: ServerType) : OnboardingViewEvents() object OnLoginFlowRetrieved : OnboardingViewEvents() + object OnHomeserverEdited : OnboardingViewEvents() data class OnSignModeSelected(val signMode: SignMode) : OnboardingViewEvents() object OnForgetPasswordClicked : OnboardingViewEvents() object OnResetPasswordSendThreePidDone : 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 index dbd257464e..54aea0185c 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -47,7 +47,6 @@ import kotlinx.coroutines.launch 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 @@ -75,6 +74,7 @@ class OnboardingViewModel @AssistedInject constructor( private val uriFilenameResolver: UriFilenameResolver, private val registrationActionHandler: RegistrationActionHandler, private val directLoginUseCase: DirectLoginUseCase, + private val startAuthenticationFlowUseCase: StartAuthenticationFlowUseCase, private val vectorOverrides: VectorOverrides ) : VectorViewModel(initialState) { @@ -107,6 +107,7 @@ class OnboardingViewModel @AssistedInject constructor( private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash() + private val defaultHomeserverUrl = matrixOrgUrl private val registrationWizard: RegistrationWizard get() = authenticationService.getRegistrationWizard() @@ -139,7 +140,7 @@ class OnboardingViewModel @AssistedInject constructor( 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.HomeServerChange -> withAction(action) { handleHomeserverChange(action.homeServerUrl) } is OnboardingAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action } is OnboardingAction.Register -> handleRegisterWith(action).also { lastAction = action } is OnboardingAction.LoginWithToken -> handleLoginWithToken(action) @@ -161,25 +162,30 @@ class OnboardingViewModel @AssistedInject constructor( } } + private fun withAction(action: OnboardingAction, block: (OnboardingAction) -> Unit) { + lastAction = action + block(action) + } + private fun handleSplashAction(resetConfig: Boolean, onboardingFlow: OnboardingFlow) { if (resetConfig) { loginConfig = null } setState { copy(onboardingFlow = onboardingFlow) } - 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") - continueToPageAfterSplash(onboardingFlow) - } else { - getLoginFlow(homeServerConnectionConfig, ServerType.Other) + return when (val config = loginConfig.toHomeserverConfig()) { + null -> continueToPageAfterSplash(onboardingFlow) + else -> startAuthenticationFlow(config, ServerType.Other) + } + } + + private fun LoginConfig?.toHomeserverConfig(): HomeServerConnectionConfig? { + return this?.homeServerUrl?.takeIf { it.isNotEmpty() }?.let { url -> + homeServerConnectionConfigFactory.create(url).also { + if (it == null) { + Timber.w("Url from config url was invalid: $url") + } } - } else { - continueToPageAfterSplash(onboardingFlow) } } @@ -200,10 +206,10 @@ class OnboardingViewModel @AssistedInject constructor( // 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 -> { + is OnboardingAction.HomeServerChange.SelectHomeServer -> { currentHomeServerConnectionConfig ?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) } - ?.let { getLoginFlow(it) } + ?.let { startAuthenticationFlow(it) } } is OnboardingAction.LoginOrRegister -> handleDirectLogin( @@ -291,24 +297,16 @@ class OnboardingViewModel @AssistedInject constructor( currentJob = null when (action) { - OnboardingAction.ResetHomeServerType -> { - setState { - copy( - serverType = ServerType.Unknown - ) - } + OnboardingAction.ResetHomeServerType -> { + setState { copy(serverType = ServerType.Unknown) } } - OnboardingAction.ResetHomeServerUrl -> { + OnboardingAction.ResetHomeServerUrl -> { viewModelScope.launch { authenticationService.reset() setState { copy( isLoading = false, - homeServerUrlFromUser = null, - homeServerUrl = null, - loginMode = LoginMode.Unknown, - serverType = ServerType.Unknown, - loginModeSupportedTypes = emptyList() + selectedHomeserver = SelectedHomeserverState(), ) } } @@ -318,8 +316,6 @@ class OnboardingViewModel @AssistedInject constructor( copy( isLoading = false, signMode = SignMode.Unknown, - loginMode = LoginMode.Unknown, - loginModeSupportedTypes = emptyList() ) } } @@ -358,10 +354,7 @@ class OnboardingViewModel @AssistedInject constructor( private fun handleUpdateUseCase(action: OnboardingAction.UpdateUseCase) { setState { copy(useCase = action.useCase) } when (vectorFeatures.isOnboardingCombinedRegisterEnabled()) { - true -> { - handle(OnboardingAction.UpdateHomeServer(matrixOrgUrl)) - OnboardingViewEvents.OpenCombinedRegister - } + true -> handle(OnboardingAction.HomeServerChange.SelectHomeServer(defaultHomeserverUrl)) false -> _viewEvents.post(OnboardingViewEvents.OpenServerSelection) } } @@ -381,7 +374,7 @@ class OnboardingViewModel @AssistedInject constructor( ServerType.Unknown -> Unit /* Should not happen */ ServerType.MatrixOrg -> // Request login flow here - handle(OnboardingAction.UpdateHomeServer(matrixOrgUrl)) + handle(OnboardingAction.HomeServerChange.SelectHomeServer(matrixOrgUrl)) ServerType.EMS, ServerType.Other -> _viewEvents.post(OnboardingViewEvents.OnServerSelectionDone(action.serverType)) } @@ -571,7 +564,7 @@ class OnboardingViewModel @AssistedInject constructor( } private fun handleWebLoginSuccess(action: OnboardingAction.WebLoginSuccess) = withState { state -> - val homeServerConnectionConfigFinal = homeServerConnectionConfigFactory.create(state.homeServerUrl) + val homeServerConnectionConfigFinal = homeServerConnectionConfigFactory.create(state.selectedHomeserver.upstreamUrl) if (homeServerConnectionConfigFinal == null) { // Should not happen @@ -588,93 +581,77 @@ class OnboardingViewModel @AssistedInject constructor( } } - private fun handleUpdateHomeserver(action: OnboardingAction.UpdateHomeServer) { - val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl) + private fun handleHomeserverChange(homeserverUrl: String) { + val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(homeserverUrl) if (homeServerConnectionConfig == null) { // This is invalid _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig"))) } else { - getLoginFlow(homeServerConnectionConfig) + startAuthenticationFlow(homeServerConnectionConfig) } } - private fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, - serverTypeOverride: ServerType? = null) { + private fun startAuthenticationFlow(homeServerConnectionConfig: HomeServerConnectionConfig, serverTypeOverride: ServerType? = null) { currentHomeServerConnectionConfig = homeServerConnectionConfig currentJob = viewModelScope.launch { - authenticationService.cancelPendingLoginOrRegistration() + setState { copy(isLoading = true) } - setState { - copy( - isLoading = true, - // 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 + runCatching { startAuthenticationFlowUseCase.execute(homeServerConnectionConfig) }.fold( + onSuccess = { + rememberHomeServer(homeServerConnectionConfig.homeServerUri.toString()) + if (it.isHomeserverOutdated) { + _viewEvents.post(OnboardingViewEvents.OutdatedHomeserver) } - ) - } - val data = try { - authenticationService.getLoginFlow(homeServerConnectionConfig) - } catch (failure: Throwable) { - setState { - copy( - isLoading = false, - // If we were trying to retrieve matrix.org login flow, also reset the serverType - serverType = if (serverType == ServerType.MatrixOrg) ServerType.Unknown else serverType - ) - } - _viewEvents.post(OnboardingViewEvents.Failure(failure)) - 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( - isLoading = false, - 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) - } - - withState { - if (loginMode.supportsSignModeScreen()) { - when (it.onboardingFlow) { - OnboardingFlow.SignIn -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignIn)) - OnboardingFlow.SignUp -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignUp)) - OnboardingFlow.SignInSignUp, - null -> { - _viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved) + setState { + copy( + serverType = alignServerTypeAfterSubmission(homeServerConnectionConfig, serverTypeOverride), + selectedHomeserver = it.selectedHomeserver, + isLoading = false, + ) } + onAuthenticationStartedSuccess() + }, + onFailure = { + setState { copy(isLoading = false) } + _viewEvents.post(OnboardingViewEvents.Failure(it)) + } + ) + } + } + + /** + * 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 + **/ + private fun OnboardingViewState.alignServerTypeAfterSubmission(config: HomeServerConnectionConfig, serverTypeOverride: ServerType?): ServerType { + return if (config.homeServerUri.toString() == matrixOrgUrl) { + ServerType.MatrixOrg + } else { + serverTypeOverride ?: serverType + } + } + + private fun onAuthenticationStartedSuccess() { + withState { + when (lastAction) { + is OnboardingAction.HomeServerChange.EditHomeServer -> _viewEvents.post(OnboardingViewEvents.OnHomeserverEdited) + is OnboardingAction.HomeServerChange.SelectHomeServer -> { + if (it.selectedHomeserver.preferredLoginMode.supportsSignModeScreen()) { + when (it.onboardingFlow) { + OnboardingFlow.SignIn -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignIn)) + OnboardingFlow.SignUp -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignUp)) + OnboardingFlow.SignInSignUp, + null -> { + _viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved) + } + } + } else { + _viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved) } - } else { - _viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved) } + else -> _viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved) } } } 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 index b98e811679..442a0a7df1 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt @@ -40,26 +40,17 @@ data class OnboardingViewState( 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(), val isForceLoginFallbackEnabled: Boolean = false, + @PersistState + val selectedHomeserver: SelectedHomeserverState = SelectedHomeserverState(), + @PersistState val personalizationState: PersonalizationState = PersonalizationState() ) : MavericksState @@ -70,6 +61,15 @@ enum class OnboardingFlow { SignInSignUp } +@Parcelize +data class SelectedHomeserverState( + val description: String? = null, + val userFacingUrl: String? = null, + val upstreamUrl: String? = null, + val preferredLoginMode: LoginMode = LoginMode.Unknown, + val supportedLoginTypes: List = emptyList(), +) : Parcelable + @Parcelize data class PersonalizationState( val supportsChangingDisplayName: Boolean = false, diff --git a/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt b/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt new file mode 100644 index 0000000000..53e1c47fa1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 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.R +import im.vector.app.core.extensions.containsAllItems +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.ensureTrailingSlash +import im.vector.app.features.login.LoginMode +import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.LoginFlowResult +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import javax.inject.Inject + +class StartAuthenticationFlowUseCase @Inject constructor( + private val authenticationService: AuthenticationService, + private val stringProvider: StringProvider +) { + + suspend fun execute(config: HomeServerConnectionConfig): StartAuthenticationResult { + authenticationService.cancelPendingLoginOrRegistration() + val authFlow = authenticationService.getLoginFlow(config) + val preferredLoginMode = authFlow.findPreferredLoginMode() + val selection = createSelectedHomeserver(authFlow, config, preferredLoginMode) + val isOutdated = (preferredLoginMode == LoginMode.Password && !authFlow.isLoginAndRegistrationSupported) || authFlow.isOutdatedHomeserver + return StartAuthenticationResult(isOutdated, selection) + } + + private fun createSelectedHomeserver( + authFlow: LoginFlowResult, + config: HomeServerConnectionConfig, + preferredLoginMode: LoginMode + ) = SelectedHomeserverState( + description = when (config.homeServerUri.toString()) { + matrixOrgUrl() -> stringProvider.getString(R.string.ftue_auth_create_account_matrix_dot_org_server_description) + else -> null + }, + userFacingUrl = config.homeServerUri.toString(), + upstreamUrl = authFlow.homeServerUrl, + preferredLoginMode = preferredLoginMode, + supportedLoginTypes = authFlow.supportedLoginTypes + ) + + private fun matrixOrgUrl() = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash() + + private fun LoginFlowResult.findPreferredLoginMode() = when { + supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(ssoIdentityProviders) + supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders) + supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported + } + + data class StartAuthenticationResult( + val isHomeserverOutdated: Boolean, + val selectedHomeserver: SelectedHomeserverState + ) +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractSSOFtueAuthFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractSSOFtueAuthFragment.kt index 2e9925516c..a032181e4d 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractSSOFtueAuthFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractSSOFtueAuthFragment.kt @@ -37,7 +37,7 @@ abstract class AbstractSSOFtueAuthFragment : AbstractFtueAuthF override fun onStart() { super.onStart() - val hasSSO = withState(viewModel) { it.loginMode.hasSso() } + val hasSSO = withState(viewModel) { it.selectedHomeserver.preferredLoginMode.hasSso() } if (hasSSO) { val packageName = CustomTabsClient.getPackageName(requireContext(), null) @@ -67,7 +67,7 @@ abstract class AbstractSSOFtueAuthFragment : AbstractFtueAuthF override fun onStop() { super.onStop() - val hasSSO = withState(viewModel) { it.loginMode.hasSso() } + val hasSSO = withState(viewModel) { it.selectedHomeserver.preferredLoginMode.hasSso() } if (hasSSO) { customTabsServiceConnection?.let { requireContext().unbindService(it) } customTabsServiceConnection = null @@ -88,7 +88,7 @@ abstract class AbstractSSOFtueAuthFragment : AbstractFtueAuthF private fun prefetchIfNeeded() { withState(viewModel) { state -> - if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) { + if (state.selectedHomeserver.preferredLoginMode.hasSso() && state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders().isNullOrEmpty()) { // in this case we can prefetch (not other cases for privacy concerns) viewModel.getSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt index c9c693ef3b..0631149bcd 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt @@ -77,7 +77,7 @@ class FtueAuthCaptchaFragment @Inject constructor( val mime = "text/html" val encoding = "utf-8" - val homeServerUrl = state.homeServerUrl ?: error("missing url of homeserver") + val homeServerUrl = state.selectedHomeserver.upstreamUrl ?: error("missing url of homeserver") views.loginCaptchaWevView.loadDataWithBaseURL(homeServerUrl, html, mime, encoding, null) views.loginCaptchaWevView.requestLayout() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt index 3a7d51d14b..441fd64b0b 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt @@ -36,12 +36,13 @@ import im.vector.app.core.extensions.hasSurroundingSpaces import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hidePassword import im.vector.app.core.extensions.realignPercentagesToParent +import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentFtueSignUpCombinedBinding import im.vector.app.features.login.LoginMode import im.vector.app.features.login.SSORedirectRouterActivity -import im.vector.app.features.login.ServerType import im.vector.app.features.login.SocialLoginButtonsView import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewState import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn @@ -64,8 +65,10 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupSubmitButton() - views.createAccountRoot.realignPercentagesToParent() + views.editServerButton.debouncedClicks { + viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) + } views.createAccountPasswordInput.editText().setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { @@ -164,6 +167,9 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu setupUi(state) setupAutoFill() + views.selectedServerName.text = state.selectedHomeserver.userFacingUrl.toReducedUrl() + views.selectedServerDescription.text = state.selectedHomeserver.description + if (state.isLoading) { // Ensure password is hidden views.createAccountPasswordInput.editText().hidePassword() @@ -171,8 +177,8 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu } private fun setupUi(state: OnboardingViewState) { - when (state.loginMode) { - is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.loginMode.ssoIdentityProviders) + when (state.selectedHomeserver.preferredLoginMode) { + is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders) else -> hideSsoProviders() } } @@ -201,6 +207,6 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu views.createAccountPasswordInput.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) } } -} -private fun OnboardingViewState.isNumericOnlyUserIdForbidden() = serverType == ServerType.MatrixOrg + private fun OnboardingViewState.isNumericOnlyUserIdForbidden() = selectedHomeserver.userFacingUrl == getString(R.string.matrix_org_server_url) +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt new file mode 100644 index 0000000000..d1560d7be0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2022 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.ftueauth + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import im.vector.app.R +import im.vector.app.core.extensions.content +import im.vector.app.core.extensions.editText +import im.vector.app.core.extensions.realignPercentagesToParent +import im.vector.app.core.extensions.toReducedUrl +import im.vector.app.core.utils.ensureProtocol +import im.vector.app.core.utils.ensureTrailingSlash +import im.vector.app.core.utils.openUrlInExternalBrowser +import im.vector.app.databinding.FragmentFtueServerSelectionCombinedBinding +import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.OnboardingViewEvents +import im.vector.app.features.onboarding.OnboardingViewState +import javax.inject.Inject + +class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFtueAuthFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueServerSelectionCombinedBinding { + return FragmentFtueServerSelectionCombinedBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupViews() + } + + private fun setupViews() { + views.chooseServerRoot.realignPercentagesToParent() + views.chooseServerToolbar.setNavigationOnClickListener { + viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnBack)) + } + views.chooseServerInput.editText?.setOnEditorActionListener { _, actionId, _ -> + when (actionId) { + EditorInfo.IME_ACTION_DONE -> { + updateServerUrl() + } + } + false + } + views.chooseServerGetInTouch.debouncedClicks { openUrlInExternalBrowser(requireContext(), getString(R.string.ftue_ems_url)) } + views.chooseServerSubmit.debouncedClicks { updateServerUrl() } + } + + private fun updateServerUrl() { + viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(views.chooseServerInput.content().ensureProtocol().ensureTrailingSlash())) + } + + override fun resetViewModel() { + // do nothing + } + + override fun updateWithState(state: OnboardingViewState) { + if (views.chooseServerInput.content().isEmpty()) { + val userUrlInput = state.selectedHomeserver.userFacingUrl?.toReducedUrlKeepingSchemaIfInsecure() + views.chooseServerInput.editText().setText(userUrlInput) + } + } + + private fun String.toReducedUrlKeepingSchemaIfInsecure() = toReducedUrl(keepSchema = this.startsWith("http://")) +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt index e561f85f25..4888b43946 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt @@ -184,7 +184,7 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment< ServerType.MatrixOrg -> { views.loginServerIcon.isVisible = true views.loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) - views.loginTitle.text = getString(resId, state.homeServerUrlFromUser.toReducedUrl()) + views.loginTitle.text = getString(resId, state.selectedHomeserver.userFacingUrl.toReducedUrl()) views.loginNotice.text = getString(R.string.login_server_matrix_org_text) } ServerType.EMS -> { @@ -195,16 +195,16 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment< } ServerType.Other -> { views.loginServerIcon.isVisible = false - views.loginTitle.text = getString(resId, state.homeServerUrlFromUser.toReducedUrl()) + views.loginTitle.text = getString(resId, state.selectedHomeserver.userFacingUrl.toReducedUrl()) views.loginNotice.text = getString(R.string.login_server_other_text) } ServerType.Unknown -> Unit /* Should not happen */ } views.loginPasswordNotice.isVisible = false - if (state.loginMode is LoginMode.SsoAndPassword) { + if (state.selectedHomeserver.preferredLoginMode is LoginMode.SsoAndPassword) { views.loginSocialLoginContainer.isVisible = true - views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders?.sorted() + views.loginSocialLoginButtons.ssoIdentityProviders = state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders?.sorted() views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { override fun onProviderSelected(id: String?) { viewModel.getSsoUrl( diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordFragment.kt index b612ec34b5..4ec02f677a 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordFragment.kt @@ -59,7 +59,7 @@ class FtueAuthResetPasswordFragment @Inject constructor() : AbstractFtueAuthFrag } private fun setupUi(state: OnboardingViewState) { - views.resetPasswordTitle.text = getString(R.string.login_reset_password_on, state.homeServerUrlFromUser.toReducedUrl()) + views.resetPasswordTitle.text = getString(R.string.login_reset_password_on, state.selectedHomeserver.userFacingUrl.toReducedUrl()) } private fun setupSubmitButton() { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthServerUrlFormFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthServerUrlFormFragment.kt index 2cae9743a7..df304d028d 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthServerUrlFormFragment.kt @@ -139,7 +139,7 @@ class FtueAuthServerUrlFormFragment @Inject constructor() : AbstractFtueAuthFrag } else -> { views.loginServerUrlFormHomeServerUrl.setText(serverUrl, false /* to avoid completion dialog flicker*/) - viewModel.handle(OnboardingAction.UpdateHomeServer(serverUrl)) + viewModel.handle(OnboardingAction.HomeServerChange.SelectHomeServer(serverUrl)) } } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSignUpSignInSelectionFragment.kt index e9ae5022e2..dda5e0c36a 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSignUpSignInSelectionFragment.kt @@ -20,6 +20,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.DrawableRes import androidx.core.view.isVisible import com.airbnb.mvrx.withState import im.vector.app.R @@ -55,32 +56,30 @@ class FtueAuthSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOF views.loginSignupSigninSignIn.setOnClickListener { signIn() } } - private fun setupUi(state: OnboardingViewState) { + private fun render(state: OnboardingViewState) { when (state.serverType) { - ServerType.MatrixOrg -> { - views.loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) - views.loginSignupSigninServerIcon.isVisible = true - views.loginSignupSigninTitle.text = getString(R.string.login_connect_to, state.homeServerUrlFromUser.toReducedUrl()) - views.loginSignupSigninText.text = getString(R.string.login_server_matrix_org_text) - } - ServerType.EMS -> { - views.loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_element_matrix_services) - views.loginSignupSigninServerIcon.isVisible = true - views.loginSignupSigninTitle.text = getString(R.string.login_connect_to_modular) - views.loginSignupSigninText.text = state.homeServerUrlFromUser.toReducedUrl() - } - ServerType.Other -> { - views.loginSignupSigninServerIcon.isVisible = false - views.loginSignupSigninTitle.text = getString(R.string.login_server_other_title) - views.loginSignupSigninText.text = getString(R.string.login_connect_to, state.homeServerUrlFromUser.toReducedUrl()) - } + ServerType.MatrixOrg -> renderServerInformation( + icon = R.drawable.ic_logo_matrix_org, + title = getString(R.string.login_connect_to, state.selectedHomeserver.userFacingUrl.toReducedUrl()), + subtitle = getString(R.string.login_server_matrix_org_text) + ) + ServerType.EMS -> renderServerInformation( + icon = R.drawable.ic_logo_element_matrix_services, + title = getString(R.string.login_connect_to_modular), + subtitle = state.selectedHomeserver.userFacingUrl.toReducedUrl() + ) + ServerType.Other -> renderServerInformation( + icon = null, + title = getString(R.string.login_server_other_title), + subtitle = getString(R.string.login_connect_to, state.selectedHomeserver.userFacingUrl.toReducedUrl()) + ) ServerType.Unknown -> Unit /* Should not happen */ } - when (state.loginMode) { + when (state.selectedHomeserver.preferredLoginMode) { is LoginMode.SsoAndPassword -> { views.loginSignupSigninSignInSocialLoginContainer.isVisible = true - views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders()?.sorted() + views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders()?.sorted() views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { override fun onProviderSelected(id: String?) { viewModel.getSsoUrl( @@ -100,8 +99,16 @@ class FtueAuthSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOF } } + private fun renderServerInformation(@DrawableRes icon: Int?, title: String, subtitle: String) { + icon?.let { views.loginSignupSigninServerIcon.setImageResource(it) } + views.loginSignupSigninServerIcon.isVisible = icon != null + views.loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) + views.loginSignupSigninTitle.text = title + views.loginSignupSigninText.text = subtitle + } + private fun setupButtons(state: OnboardingViewState) { - when (state.loginMode) { + when (state.selectedHomeserver.preferredLoginMode) { is LoginMode.Sso -> { // change to only one button that is sign in with sso views.loginSignupSigninSubmit.text = getString(R.string.login_signin_sso) @@ -115,7 +122,7 @@ class FtueAuthSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOF } private fun submit() = withState(viewModel) { state -> - if (state.loginMode is LoginMode.Sso) { + if (state.selectedHomeserver.preferredLoginMode is LoginMode.Sso) { viewModel.getSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, @@ -136,7 +143,7 @@ class FtueAuthSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOF } override fun updateWithState(state: OnboardingViewState) { - setupUi(state) + render(state) setupButtons(state) } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index 4c7bf47233..ea479b1cdc 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -233,6 +233,14 @@ class FtueAuthVariant( OnboardingViewEvents.OnChooseProfilePicture -> onChooseProfilePicture() OnboardingViewEvents.OnPersonalizationComplete -> onPersonalizationComplete() OnboardingViewEvents.OnBack -> activity.popBackstack() + OnboardingViewEvents.EditServerSelection -> { + activity.addFragmentToBackstack( + views.loginFragmentContainer, + FtueAuthCombinedServerSelectionFragment::class.java, + option = commonOption + ) + } + OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack() } } @@ -299,18 +307,18 @@ class FtueAuthVariant( private fun handleSignInSelected(state: OnboardingViewState) { if (isForceLoginFallbackEnabled) { - onLoginModeNotSupported(state.loginModeSupportedTypes) + onLoginModeNotSupported(state.selectedHomeserver.supportedLoginTypes) } else { disambiguateLoginMode(state) } } - private fun disambiguateLoginMode(state: OnboardingViewState) = when (state.loginMode) { + private fun disambiguateLoginMode(state: OnboardingViewState) = when (state.selectedHomeserver.preferredLoginMode) { LoginMode.Unknown, is LoginMode.Sso -> error("Developer error") is LoginMode.SsoAndPassword, LoginMode.Password -> openAuthLoginFragmentWithTag(FRAGMENT_LOGIN_TAG) - LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) + LoginMode.Unsupported -> onLoginModeNotSupported(state.selectedHomeserver.supportedLoginTypes) } private fun openAuthLoginFragmentWithTag(tag: String) { @@ -331,7 +339,7 @@ class FtueAuthVariant( private fun handleSignInWithMatrixId(state: OnboardingViewState) { if (isForceLoginFallbackEnabled) { - onLoginModeNotSupported(state.loginModeSupportedTypes) + onLoginModeNotSupported(state.selectedHomeserver.supportedLoginTypes) } else { openAuthLoginFragmentWithTag(FRAGMENT_LOGIN_TAG) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt index f6a7769cbd..aa626c6318 100755 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt @@ -116,7 +116,7 @@ class FtueAuthTermsFragment @Inject constructor( } override fun updateWithState(state: OnboardingViewState) { - policyController.homeServer = state.homeServerUrlFromUser.toReducedUrl() + policyController.homeServer = state.selectedHomeserver.userFacingUrl.toReducedUrl() renderState() } diff --git a/vector/src/main/res/drawable/ic_choose_server.xml b/vector/src/main/res/drawable/ic_choose_server.xml new file mode 100644 index 0000000000..26c8e75222 --- /dev/null +++ b/vector/src/main/res/drawable/ic_choose_server.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_ems_logo.xml b/vector/src/main/res/drawable/ic_ems_logo.xml new file mode 100644 index 0000000000..68c2aeb190 --- /dev/null +++ b/vector/src/main/res/drawable/ic_ems_logo.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/vector/src/main/res/layout/fragment_ftue_server_selection_combined.xml b/vector/src/main/res/layout/fragment_ftue_server_selection_combined.xml new file mode 100644 index 0000000000..8f4902a577 --- /dev/null +++ b/vector/src/main/res/layout/fragment_ftue_server_selection_combined.xml @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +