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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_ftue_sign_up_combined.xml b/vector/src/main/res/layout/fragment_ftue_sign_up_combined.xml
index 9602dd0231..9d61780ad0 100644
--- a/vector/src/main/res/layout/fragment_ftue_sign_up_combined.xml
+++ b/vector/src/main/res/layout/fragment_ftue_sign_up_combined.xml
@@ -109,7 +109,6 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
- android:text="@string/ftue_auth_create_account_matrix_dot_org_server_name"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toTopOf="@id/selectedServerDescription"
app:layout_constraintEnd_toStartOf="@id/editServerButton"
@@ -122,7 +121,6 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
- android:text="@string/ftue_auth_create_account_matrix_dot_org_server_description"
android:textColor="?vctr_content_tertiary"
app:layout_constraintBottom_toTopOf="@id/serverSelectionSpacing"
app:layout_constraintEnd_toStartOf="@id/editServerButton"
@@ -139,7 +137,6 @@
android:paddingEnd="12dp"
android:text="@string/ftue_auth_create_account_edit_server_selection"
android:textAllCaps="true"
- android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/selectedServerDescription"
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
app:layout_constraintTop_toTopOf="@id/chooseYourServerHeader" />
@@ -176,6 +173,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeOptions="actionNext"
+ android:nextFocusForward="@id/createAccountPasswordInput"
android:inputType="text"
android:maxLines="1" />
diff --git a/vector/src/main/res/values/donottranslate.xml b/vector/src/main/res/values/donottranslate.xml
index 48d2950cd5..889fcaaefe 100755
--- a/vector/src/main/res/values/donottranslate.xml
+++ b/vector/src/main/res/values/donottranslate.xml
@@ -17,7 +17,15 @@
Choose your server to store your data
Or
Join millions for free on the largest public server
- matrix.org
Edit
+ Choose your server
+ What is the address of your server? Server is like a home for all your data.
+ Server URL
+ You can only connect to a server that has already been set up
+ Want to host your own server?
+
+ Element Matrix Services (EMS) is a robust and reliable hosting service for fast, secure and real time communication. Find out how on element.io/ems
+ Get in touch
+
diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
index a682d025b8..77b3f495f0 100644
--- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
@@ -18,8 +18,10 @@ package im.vector.app.features.onboarding
import android.net.Uri
import com.airbnb.mvrx.test.MvRxTestRule
+import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.login.SignMode
+import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeAnalyticsTracker
import im.vector.app.test.fakes.FakeAuthenticationService
@@ -30,6 +32,7 @@ import im.vector.app.test.fakes.FakeHomeServerHistoryService
import im.vector.app.test.fakes.FakeRegisterActionHandler
import im.vector.app.test.fakes.FakeRegistrationWizard
import im.vector.app.test.fakes.FakeSession
+import im.vector.app.test.fakes.FakeStartAuthenticationFlowUseCase
import im.vector.app.test.fakes.FakeStringProvider
import im.vector.app.test.fakes.FakeUri
import im.vector.app.test.fakes.FakeUriFilenameResolver
@@ -41,6 +44,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
+import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.registration.FlowResult
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
@@ -58,6 +62,9 @@ private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canChangeDisplay
private val AN_IGNORED_FLOW_RESULT = FlowResult(missingStages = emptyList(), completedStages = emptyList())
private val ANY_CONTINUING_REGISTRATION_RESULT = RegistrationResult.FlowResponse(AN_IGNORED_FLOW_RESULT)
private val A_LOGIN_OR_REGISTER_ACTION = OnboardingAction.LoginOrRegister("@a-user:id.org", "a-password", "a-device-name")
+private const val A_HOMESERVER_URL = "https://edited-homeserver.org"
+private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance)
+private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password)
class OnboardingViewModelTest {
@@ -74,6 +81,9 @@ class OnboardingViewModelTest {
private val fakeRegisterActionHandler = FakeRegisterActionHandler()
private val fakeDirectLoginUseCase = FakeDirectLoginUseCase()
private val fakeVectorFeatures = FakeVectorFeatures()
+ private val fakeHomeServerConnectionConfigFactory = FakeHomeServerConnectionConfigFactory()
+ private val fakeStartAuthenticationFlowUseCase = FakeStartAuthenticationFlowUseCase()
+ private val fakeHomeServerHistoryService = FakeHomeServerHistoryService()
lateinit var viewModel: OnboardingViewModel
@@ -224,6 +234,25 @@ class OnboardingViewModelTest {
.finish()
}
+ @Test
+ fun `given when editing homeserver, then updates selected homeserver state and emits edited event`() = runTest {
+ val test = viewModel.test()
+ fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG)
+ fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(false, SELECTED_HOMESERVER_STATE))
+ fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
+
+ viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL))
+
+ test
+ .assertStatesChanges(
+ initialState,
+ { copy(isLoading = true) },
+ { copy(isLoading = false, selectedHomeserver = SELECTED_HOMESERVER_STATE) },
+ )
+ .assertEvents(OnboardingViewEvents.OnHomeserverEdited)
+ .finish()
+ }
+
@Test
fun `given personalisation enabled, when registering account, then updates state and emits account created event`() = runTest {
fakeVectorFeatures.givenPersonalisationEnabled()
@@ -383,15 +412,16 @@ class OnboardingViewModelTest {
fakeContext.instance,
fakeAuthenticationService,
fakeActiveSessionHolder.instance,
- FakeHomeServerConnectionConfigFactory().instance,
+ fakeHomeServerConnectionConfigFactory.instance,
ReAuthHelper(),
FakeStringProvider().instance,
- FakeHomeServerHistoryService(),
+ fakeHomeServerHistoryService,
fakeVectorFeatures,
FakeAnalyticsTracker(),
fakeUriFilenameResolver.instance,
fakeRegisterActionHandler.instance,
fakeDirectLoginUseCase.instance,
+ fakeStartAuthenticationFlowUseCase.instance,
FakeVectorOverrides()
)
}
diff --git a/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt
new file mode 100644
index 0000000000..b75ec231fd
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.features.login.LoginMode
+import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult
+import im.vector.app.test.fakes.FakeAuthenticationService
+import im.vector.app.test.fakes.FakeStringProvider
+import im.vector.app.test.fakes.FakeUri
+import im.vector.app.test.fakes.toTestString
+import io.mockk.coVerifyOrder
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Before
+import org.junit.Test
+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 org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
+
+private const val MATRIX_ORG_URL = "https://any-value.org/"
+private const val A_DECLARED_HOMESERVER_URL = "https://foo.bar"
+private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(homeServerUri = FakeUri().instance)
+private val SSO_IDENTITY_PROVIDERS = emptyList()
+
+class StartAuthenticationFlowUseCaseTest {
+
+ private val fakeAuthenticationService = FakeAuthenticationService()
+ private val fakeStringProvider = FakeStringProvider()
+
+ private val useCase = StartAuthenticationFlowUseCase(fakeAuthenticationService, fakeStringProvider.instance)
+
+ @Before
+ fun setUp() {
+ fakeAuthenticationService.expectedCancelsPendingLogin()
+ }
+
+ @Test
+ fun `given empty login result when starting authentication flow then returns empty result`() = runTest {
+ val loginResult = aLoginResult()
+ fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult)
+
+ val result = useCase.execute(A_HOMESERVER_CONFIG)
+
+ result shouldBeEqualTo expectedResult()
+ verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
+ }
+
+ @Test
+ fun `given login supports SSO and Password when starting authentication flow then prefers SsoAndPassword`() = runTest {
+ val supportedLoginTypes = listOf(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD)
+ val loginResult = aLoginResult(supportedLoginTypes = supportedLoginTypes)
+ fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult)
+
+ val result = useCase.execute(A_HOMESERVER_CONFIG)
+
+ result shouldBeEqualTo expectedResult(
+ supportedLoginTypes = supportedLoginTypes,
+ preferredLoginMode = LoginMode.SsoAndPassword(SSO_IDENTITY_PROVIDERS),
+ )
+ verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
+ }
+
+ @Test
+ fun `given login supports SSO when starting authentication flow then prefers Sso`() = runTest {
+ val supportedLoginTypes = listOf(LoginFlowTypes.SSO)
+ val loginResult = aLoginResult(supportedLoginTypes = supportedLoginTypes)
+ fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult)
+
+ val result = useCase.execute(A_HOMESERVER_CONFIG)
+
+ result shouldBeEqualTo expectedResult(
+ supportedLoginTypes = supportedLoginTypes,
+ preferredLoginMode = LoginMode.Sso(SSO_IDENTITY_PROVIDERS),
+ )
+ verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
+ }
+
+ @Test
+ fun `given login supports Password when starting authentication flow then prefers Password`() = runTest {
+ val supportedLoginTypes = listOf(LoginFlowTypes.PASSWORD)
+ val loginResult = aLoginResult(supportedLoginTypes = supportedLoginTypes)
+ fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult)
+
+ val result = useCase.execute(A_HOMESERVER_CONFIG)
+
+ result shouldBeEqualTo expectedResult(
+ supportedLoginTypes = supportedLoginTypes,
+ preferredLoginMode = LoginMode.Password,
+ )
+ verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
+ }
+
+ @Test
+ fun `given matrix dot org url when starting authentication flow then provides description`() = runTest {
+ val matrixOrgConfig = HomeServerConnectionConfig(homeServerUri = FakeUri(MATRIX_ORG_URL).instance)
+ fakeStringProvider.given(R.string.matrix_org_server_url, result = MATRIX_ORG_URL)
+ fakeAuthenticationService.givenLoginFlow(matrixOrgConfig, aLoginResult())
+
+ val result = useCase.execute(matrixOrgConfig)
+
+ result shouldBeEqualTo expectedResult(
+ description = R.string.ftue_auth_create_account_matrix_dot_org_server_description.toTestString(),
+ homeserverSourceUrl = MATRIX_ORG_URL
+ )
+ verifyClearsAndThenStartsLogin(matrixOrgConfig)
+ }
+
+ private fun aLoginResult(
+ supportedLoginTypes: List = emptyList()
+ ) = LoginFlowResult(
+ supportedLoginTypes = supportedLoginTypes,
+ ssoIdentityProviders = SSO_IDENTITY_PROVIDERS,
+ isLoginAndRegistrationSupported = true,
+ homeServerUrl = A_DECLARED_HOMESERVER_URL,
+ isOutdatedHomeserver = false
+ )
+
+ private fun expectedResult(
+ isHomeserverOutdated: Boolean = false,
+ description: String? = null,
+ preferredLoginMode: LoginMode = LoginMode.Unsupported,
+ supportedLoginTypes: List = emptyList(),
+ homeserverSourceUrl: String = A_HOMESERVER_CONFIG.homeServerUri.toString()
+ ) = StartAuthenticationResult(
+ isHomeserverOutdated,
+ SelectedHomeserverState(
+ description = description,
+ userFacingUrl = homeserverSourceUrl,
+ upstreamUrl = A_DECLARED_HOMESERVER_URL,
+ preferredLoginMode = preferredLoginMode,
+ supportedLoginTypes = supportedLoginTypes
+ )
+ )
+
+ private fun verifyClearsAndThenStartsLogin(homeServerConnectionConfig: HomeServerConnectionConfig) {
+ coVerifyOrder {
+ fakeAuthenticationService.cancelPendingLoginOrRegistration()
+ fakeAuthenticationService.getLoginFlow(homeServerConnectionConfig)
+ }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt
index 9175fd3750..fedda18aeb 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt
@@ -22,6 +22,7 @@ import io.mockk.every
import io.mockk.mockk
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.registration.RegistrationWizard
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
@@ -35,10 +36,18 @@ class FakeAuthenticationService : AuthenticationService by mockk() {
every { isRegistrationStarted } returns started
}
+ fun givenLoginFlow(config: HomeServerConnectionConfig, result: LoginFlowResult) {
+ coEvery { getLoginFlow(config) } returns result
+ }
+
fun expectReset() {
coJustRun { reset() }
}
+ fun expectedCancelsPendingLogin() {
+ coJustRun { cancelPendingLoginOrRegistration() }
+ }
+
fun givenWellKnown(matrixId: String, config: HomeServerConnectionConfig?, result: WellknownResult) {
coEvery { getWellKnownData(matrixId, config) } returns result
}
@@ -52,6 +61,6 @@ class FakeAuthenticationService : AuthenticationService by mockk() {
}
fun givenDirectAuthenticationThrows(config: HomeServerConnectionConfig, matrixId: String, password: String, deviceName: String, cause: Throwable) {
- coEvery { directAuthentication(config, matrixId, password, deviceName) } throws cause
+ coEvery { directAuthentication(config, matrixId, password, deviceName) } throws cause
}
}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerConnectionConfigFactory.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerConnectionConfigFactory.kt
index a0e60e5a18..553a35ad8c 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerConnectionConfigFactory.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerConnectionConfigFactory.kt
@@ -17,9 +17,14 @@
package im.vector.app.test.fakes
import im.vector.app.features.login.HomeServerConnectionConfigFactory
+import io.mockk.every
import io.mockk.mockk
+import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
class FakeHomeServerConnectionConfigFactory {
-
val instance: HomeServerConnectionConfigFactory = mockk()
+
+ fun givenConfigFor(url: String, config: HomeServerConnectionConfig) {
+ every { instance.create(url) } returns config
+ }
}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerHistoryService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerHistoryService.kt
index 6cdcb02156..4d9960991f 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerHistoryService.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerHistoryService.kt
@@ -16,9 +16,13 @@
package im.vector.app.test.fakes
+import io.mockk.justRun
import io.mockk.mockk
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
class FakeHomeServerHistoryService : HomeServerHistoryService by mockk() {
override fun getKnownServersUrls() = emptyList()
+ fun expectUrlToBeAdded(url: String) {
+ justRun { addHomeServerToHistory(url) }
+ }
}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeStartAuthenticationFlowUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeStartAuthenticationFlowUseCase.kt
new file mode 100644
index 0000000000..697de6bf25
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeStartAuthenticationFlowUseCase.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.test.fakes
+
+import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase
+import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult
+import io.mockk.coEvery
+import io.mockk.mockk
+import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
+
+class FakeStartAuthenticationFlowUseCase {
+
+ val instance = mockk()
+
+ fun givenResult(config: HomeServerConnectionConfig, result: StartAuthenticationResult) {
+ coEvery { instance.execute(config) } returns result
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt
index 1a4f5cb85b..e63550abe0 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt
@@ -21,7 +21,6 @@ import io.mockk.every
import io.mockk.mockk
class FakeStringProvider {
-
val instance = mockk()
init {
@@ -29,6 +28,10 @@ class FakeStringProvider {
"test-${args[0]}"
}
}
+
+ fun given(id: Int, result: String) {
+ every { instance.getString(id) } returns result
+ }
}
fun Int.toTestString() = "test-$this"
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUri.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUri.kt
index 675401d72f..08bfac8db1 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeUri.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUri.kt
@@ -25,7 +25,10 @@ class FakeUri(contentEquals: String? = null) {
val instance = mockk()
init {
- contentEquals?.let { givenEquals(it) }
+ contentEquals?.let {
+ givenEquals(it)
+ every { instance.toString() } returns it
+ }
}
fun givenNonHierarchical() {