Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c3cf221
adding barebones server selection UI
ouchadam Mar 30, 2022
7f90dda
adding dedicated server selection state to onboarding state
ouchadam Mar 31, 2022
985dbfe
keeping the http:// schema in the server selection input field
ouchadam Mar 31, 2022
51c294a
launching the edit selection from the ftue variant entry point
ouchadam Mar 31, 2022
f34df39
extracting the authentication start logic to a dedicated use case
ouchadam Apr 1, 2022
1e52012
renaming update homeserver to select homeserver
ouchadam Apr 1, 2022
e9f5003
splitting the homeserver edit from the selection so that we can handl…
ouchadam Apr 1, 2022
8b2e2a1
adding tests around the editing of the homeserver url
ouchadam Apr 4, 2022
c022a38
checking the input field content for emptyness rather than its child …
ouchadam Apr 4, 2022
89f182a
adding missing IME handling for the register next focus and server se…
ouchadam Apr 4, 2022
1d92b42
launching the ems site when tapping get in touch
ouchadam Apr 4, 2022
2ba3bd3
addressing line length
ouchadam Apr 4, 2022
0ce2012
adding link to ems copy
ouchadam Apr 5, 2022
21102a2
renaming homeserver url properties to better define their content
ouchadam Apr 5, 2022
db50225
adding changelog entry
ouchadam Apr 5, 2022
f70d613
renaming extension to help with discoverability
ouchadam Apr 8, 2022
6304d5d
creating a common type for the edit/selecting of homeserver urls to a…
ouchadam Apr 8, 2022
05a7d40
extracting view setup to its own method -
ouchadam Apr 8, 2022
a2bcbfb
extracting server information rendering to a reusable method to reduc…
ouchadam Apr 8, 2022
5f9d3e1
moving ftue ems url to the urls config file
ouchadam Apr 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/2396.wip
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Adds a new homeserver selection screen when creating an account
1 change: 1 addition & 0 deletions vector-config/src/main/res/values/urls.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Original file line number Diff line number Diff line change
Expand Up @@ -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 == '/' }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -70,6 +61,15 @@ enum class OnboardingFlow {
SignInSignUp
}

@Parcelize
data class SelectedHomeserverState(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the individual homeserver state has been bundled into a single SelectedHomeserverState, this object is created/updated when starting the authentication flow (by selecting or editing a homeserver url) through a single entry point StartAuthenticationFlowUseCase.kt

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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -164,15 +167,18 @@ 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()
}
}

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()
}
}
Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
@@ -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?) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, I would have created some more methods instead of having all the login in onViewCreated

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

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://"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 -> {
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}
Expand Down
Loading