mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-16 02:05:06 +08:00
Merge pull request #5691 from vector-im/feature/adm/ftue-server-selection
FTUE - Server selection
This commit is contained in:
commit
bd3e98078c
1
changelog.d/2396.wip
Normal file
1
changelog.d/2396.wip
Normal file
@ -0,0 +1 @@
|
||||
Adds a new homeserver selection screen when creating an account
|
@ -3,4 +3,5 @@
|
||||
<!-- This file contains url values-->
|
||||
|
||||
<string name="threads_learn_more_url" translatable="false">https://element.io/help#threads</string>
|
||||
<string name="ftue_ems_url">https://element.io/ems</string>
|
||||
</resources>
|
@ -18,3 +18,5 @@ package im.vector.app.core.extensions
|
||||
|
||||
inline fun <reified T> List<T>.nextOrNull(index: Int) = getOrNull(index + 1)
|
||||
inline fun <reified T> List<T>.prevOrNull(index: Int) = getOrNull(index - 1)
|
||||
|
||||
fun <T> List<T>.containsAllItems(vararg items: T) = this.containsAll(items.toList())
|
||||
|
@ -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 == '/' }
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<String> = emptyList(),
|
||||
val knownCustomHomeServersUrls: List<String> = 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<String> = emptyList(),
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class PersonalizationState(
|
||||
val supportsChangingDisplayName: Boolean = false,
|
||||
|
@ -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
|
||||
)
|
||||
}
|
@ -37,7 +37,7 @@ abstract class AbstractSSOFtueAuthFragment<VB : ViewBinding> : 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<VB : ViewBinding> : 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<VB : ViewBinding> : 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,
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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<FragmentFtueServerSelectionCombinedBinding>() {
|
||||
|
||||
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://"))
|
||||
}
|
@ -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(
|
||||
|
@ -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() {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
9
vector/src/main/res/drawable/ic_choose_server.xml
Normal file
9
vector/src/main/res/drawable/ic_choose_server.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="70dp"
|
||||
android:height="70dp"
|
||||
android:viewportWidth="70"
|
||||
android:viewportHeight="70">
|
||||
<path
|
||||
android:pathData="M47.25,36.75H22.75C20.825,36.75 19.25,38.325 19.25,40.25V47.25C19.25,49.175 20.825,50.75 22.75,50.75H47.25C49.175,50.75 50.75,49.175 50.75,47.25V40.25C50.75,38.325 49.175,36.75 47.25,36.75ZM26.25,47.25C24.325,47.25 22.75,45.675 22.75,43.75C22.75,41.825 24.325,40.25 26.25,40.25C28.175,40.25 29.75,41.825 29.75,43.75C29.75,45.675 28.175,47.25 26.25,47.25ZM47.25,19.25H22.75C20.825,19.25 19.25,20.825 19.25,22.75V29.75C19.25,31.675 20.825,33.25 22.75,33.25H47.25C49.175,33.25 50.75,31.675 50.75,29.75V22.75C50.75,20.825 49.175,19.25 47.25,19.25ZM26.25,29.75C24.325,29.75 22.75,28.175 22.75,26.25C22.75,24.325 24.325,22.75 26.25,22.75C28.175,22.75 29.75,24.325 29.75,26.25C29.75,28.175 28.175,29.75 26.25,29.75Z"
|
||||
android:fillColor="#ff0000"/>
|
||||
</vector>
|
22
vector/src/main/res/drawable/ic_ems_logo.xml
Normal file
22
vector/src/main/res/drawable/ic_ems_logo.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="50dp"
|
||||
android:height="50dp"
|
||||
android:viewportWidth="50"
|
||||
android:viewportHeight="50">
|
||||
<path
|
||||
android:pathData="M20.43,11.649C20.43,10.64 21.249,9.822 22.26,9.822C29.114,9.822 34.671,15.366 34.671,22.206C34.671,23.215 33.851,24.033 32.84,24.033C31.828,24.033 31.008,23.215 31.008,22.206C31.008,17.385 27.092,13.476 22.26,13.476C21.249,13.476 20.43,12.658 20.43,11.649Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M38.347,20.379C39.359,20.379 40.178,21.197 40.178,22.206C40.178,29.045 34.622,34.59 27.768,34.59C26.757,34.59 25.937,33.772 25.937,32.763C25.937,31.754 26.757,30.936 27.768,30.936C32.6,30.936 36.516,27.027 36.516,22.206C36.516,21.197 37.336,20.379 38.347,20.379Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M29.6,38.352C29.6,39.361 28.78,40.18 27.768,40.18C20.915,40.18 15.359,34.635 15.359,27.795C15.359,26.786 16.178,25.968 17.19,25.968C18.201,25.968 19.021,26.786 19.021,27.795C19.021,32.617 22.937,36.525 27.768,36.525C28.78,36.525 29.6,37.343 29.6,38.352Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M11.653,29.622C10.642,29.622 9.822,28.804 9.822,27.795C9.822,20.956 15.378,15.411 22.232,15.411C23.243,15.411 24.063,16.229 24.063,17.238C24.063,18.247 23.243,19.065 22.232,19.065C17.401,19.065 13.484,22.974 13.484,27.795C13.484,28.804 12.664,29.622 11.653,29.622Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
@ -0,0 +1,257 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/LoginFormScrollView"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:colorBackground"
|
||||
android:fillViewport="true"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/chooseServerRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/chooseServerGutterStart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_start_percent" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/chooseServerGutterEnd"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_end_percent" />
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/chooseServerToolbar"
|
||||
style="@style/Widget.Vector.Toolbar.Settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
app:layout_constraintBottom_toTopOf="@id/chooseServerHeaderIcon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
app:navigationIcon="@drawable/ic_close_24dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/chooseServerHeaderIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/circle"
|
||||
android:backgroundTint="?colorSecondary"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_choose_server"
|
||||
app:layout_constraintBottom_toTopOf="@id/chooseServerHeaderTitle"
|
||||
app:layout_constraintEnd_toEndOf="@id/chooseServerGutterEnd"
|
||||
app:layout_constraintHeight_percent="0.10"
|
||||
app:layout_constraintStart_toStartOf="@id/chooseServerGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/chooseServerToolbar"
|
||||
app:tint="@color/palette_white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chooseServerHeaderTitle"
|
||||
style="@style/Widget.Vector.TextView.Title.Medium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/ftue_auth_choose_server_title"
|
||||
android:textColor="?vctr_content_primary"
|
||||
app:layout_constraintBottom_toTopOf="@id/chooseServerHeaderSubtitle"
|
||||
app:layout_constraintEnd_toEndOf="@id/chooseServerGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/chooseServerGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/chooseServerHeaderIcon" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chooseServerHeaderSubtitle"
|
||||
style="@style/Widget.Vector.TextView.Subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/ftue_auth_choose_server_subtitle"
|
||||
android:textColor="?vctr_content_secondary"
|
||||
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
||||
app:layout_constraintEnd_toEndOf="@id/chooseServerGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/chooseServerGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/chooseServerHeaderTitle" />
|
||||
|
||||
<Space
|
||||
android:id="@+id/titleContentSpacing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/chooseServerInput"
|
||||
app:layout_constraintHeight_percent="0.03"
|
||||
app:layout_constraintTop_toBottomOf="@id/chooseServerHeaderSubtitle" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/chooseServerInput"
|
||||
style="@style/Widget.Vector.TextInputLayout.Username"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/ftue_auth_choose_server_entry_hint"
|
||||
app:layout_constraintBottom_toTopOf="@id/chooseServerEntryFooter"
|
||||
app:layout_constraintEnd_toEndOf="@id/chooseServerGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/chooseServerGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/titleContentSpacing">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="text"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chooseServerEntryFooter"
|
||||
style="@style/Widget.Vector.TextView.Micro"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/ftue_auth_choose_server_entry_footer"
|
||||
app:layout_constraintBottom_toTopOf="@id/actionSpacing"
|
||||
app:layout_constraintEnd_toEndOf="@id/chooseServerGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/chooseServerGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/chooseServerInput" />
|
||||
|
||||
<Space
|
||||
android:id="@+id/actionSpacing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/chooseServerSubmit"
|
||||
app:layout_constraintHeight_percent="0.02"
|
||||
app:layout_constraintTop_toBottomOf="@id/chooseServerEntryFooter" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/chooseServerSubmit"
|
||||
style="@style/Widget.Vector.Button.Login"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login_signup_submit"
|
||||
android:textAllCaps="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/chooseServerEmsContainer"
|
||||
app:layout_constraintEnd_toEndOf="@id/chooseServerGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/chooseServerGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/actionSpacing" />
|
||||
|
||||
<Space
|
||||
android:id="@+id/submitSpacing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/emsTopSpacing"
|
||||
app:layout_constraintHeight_percent="0.02"
|
||||
app:layout_constraintTop_toBottomOf="@id/chooseServerSubmit" />
|
||||
|
||||
<View
|
||||
android:id="@+id/chooseServerEmsContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/rounded_rect_shape_8"
|
||||
android:backgroundTint="?vctr_system"
|
||||
app:layout_constraintBottom_toBottomOf="@id/emsCtaSpacing"
|
||||
app:layout_constraintEnd_toEndOf="@id/chooseServerGutterEnd"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="@id/chooseServerGutterStart"
|
||||
app:layout_constraintTop_toTopOf="@id/emsTopSpacing"
|
||||
app:layout_constraintVertical_bias="0.0" />
|
||||
|
||||
<Space
|
||||
android:id="@+id/emsTopSpacing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="24dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/chooseServerEmsIcon"
|
||||
app:layout_constraintTop_toBottomOf="@id/chooseServerSubmit"
|
||||
app:layout_constraintVertical_bias="1"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/chooseServerEmsIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="22dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/circle"
|
||||
android:backgroundTint="@color/palette_ems"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_ems_logo"
|
||||
app:layout_constraintBottom_toTopOf="@id/chooseServerEmsTitle"
|
||||
app:layout_constraintEnd_toEndOf="@id/chooseServerGutterEnd"
|
||||
app:layout_constraintHeight_percent="0.08"
|
||||
app:layout_constraintStart_toStartOf="@id/chooseServerGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/emsTopSpacing"
|
||||
app:layout_constraintVertical_bias="1"
|
||||
app:tint="@color/palette_white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chooseServerEmsTitle"
|
||||
style="@style/Widget.Vector.TextView.HeadlineMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/ftue_auth_choose_server_ems_title"
|
||||
app:layout_constraintBottom_toTopOf="@id/chooseServerEmsSubtitle"
|
||||
app:layout_constraintEnd_toEndOf="@id/chooseServerGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/chooseServerGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/chooseServerEmsIcon" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chooseServerEmsSubtitle"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:autoLink="web"
|
||||
android:gravity="center"
|
||||
android:text="@string/ftue_auth_choose_server_ems_subtitle"
|
||||
app:layout_constraintBottom_toTopOf="@id/chooseServerGetInTouch"
|
||||
app:layout_constraintEnd_toEndOf="@id/chooseServerGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/chooseServerGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/chooseServerEmsTitle" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/chooseServerGetInTouch"
|
||||
style="@style/Widget.Vector.Button.Login"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:backgroundTint="@color/palette_ems"
|
||||
android:text="@string/ftue_auth_choose_server_ems_cta"
|
||||
android:textAllCaps="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/emsCtaSpacing"
|
||||
app:layout_constraintEnd_toEndOf="@id/chooseServerGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/chooseServerGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/chooseServerEmsSubtitle" />
|
||||
|
||||
<Space
|
||||
android:id="@+id/emsCtaSpacing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/footerSpacing"
|
||||
app:layout_constraintTop_toBottomOf="@id/chooseServerGetInTouch" />
|
||||
|
||||
<Space
|
||||
android:id="@+id/footerSpacing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintHeight_percent="0.03"
|
||||
app:layout_constraintTop_toBottomOf="@id/emsCtaSpacing" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
@ -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" />
|
||||
|
||||
|
@ -17,7 +17,15 @@
|
||||
<string name="ftue_auth_create_account_choose_server_header">Choose your server to store your data</string>
|
||||
<string name="ftue_auth_create_account_sso_section_header">Or</string>
|
||||
<string name="ftue_auth_create_account_matrix_dot_org_server_description">Join millions for free on the largest public server</string>
|
||||
<string name="ftue_auth_create_account_matrix_dot_org_server_name">matrix.org</string>
|
||||
<string name="ftue_auth_create_account_edit_server_selection">Edit</string>
|
||||
|
||||
<string name="ftue_auth_choose_server_title">Choose your server</string>
|
||||
<string name="ftue_auth_choose_server_subtitle">What is the address of your server? Server is like a home for all your data.</string>
|
||||
<string name="ftue_auth_choose_server_entry_hint">Server URL</string>
|
||||
<string name="ftue_auth_choose_server_entry_footer">You can only connect to a server that has already been set up</string>
|
||||
<string name="ftue_auth_choose_server_ems_title">Want to host your own server?</string>
|
||||
|
||||
<string name="ftue_auth_choose_server_ems_subtitle">Element Matrix Services (EMS) is a robust and reliable hosting service for fast, secure and real time communication. Find out how on <a href="${ftue_ems_url}">element.io/ems</a></string>
|
||||
<string name="ftue_auth_choose_server_ems_cta">Get in touch</string>
|
||||
|
||||
</resources>
|
||||
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
@ -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<SsoIdentityProvider>()
|
||||
|
||||
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<String> = 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<String> = 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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<String>()
|
||||
fun expectUrlToBeAdded(url: String) {
|
||||
justRun { addHomeServerToHistory(url) }
|
||||
}
|
||||
}
|
||||
|
@ -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<StartAuthenticationFlowUseCase>()
|
||||
|
||||
fun givenResult(config: HomeServerConnectionConfig, result: StartAuthenticationResult) {
|
||||
coEvery { instance.execute(config) } returns result
|
||||
}
|
||||
}
|
@ -21,7 +21,6 @@ import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
||||
class FakeStringProvider {
|
||||
|
||||
val instance = mockk<StringProvider>()
|
||||
|
||||
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"
|
||||
|
@ -25,7 +25,10 @@ class FakeUri(contentEquals: String? = null) {
|
||||
val instance = mockk<Uri>()
|
||||
|
||||
init {
|
||||
contentEquals?.let { givenEquals(it) }
|
||||
contentEquals?.let {
|
||||
givenEquals(it)
|
||||
every { instance.toString() } returns it
|
||||
}
|
||||
}
|
||||
|
||||
fun givenNonHierarchical() {
|
||||
|
Loading…
Reference in New Issue
Block a user