Default the account provider to the most recently used one

This commit is contained in:
Jenna Vassar
2026-08-03 10:54:45 -07:00
parent 21471e07fb
commit 4714127cc0
11 changed files with 130 additions and 28 deletions
+2
View File
@@ -69,6 +69,7 @@ dependencies {
implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.qrcode)
implementation(projects.libraries.oauth.api)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.uiUtils)
implementation(projects.libraries.wellknown.api)
implementation(libs.androidx.browser)
@@ -80,6 +81,7 @@ dependencies {
testImplementation(projects.features.login.test)
testImplementation(projects.features.enterprise.test)
testImplementation(projects.features.preferences.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oauth.test)
testImplementation(projects.libraries.permissions.test)
@@ -13,27 +13,58 @@ import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.canConnectToAnyHomeserver
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@SingleIn(AppScope::class)
@Inject
class AccountProviderDataSource(
enterpriseService: EnterpriseService,
private val enterpriseService: EnterpriseService,
private val appPreferencesStore: AppPreferencesStore,
@AppCoroutineScope private val coroutineScope: CoroutineScope,
) {
private val defaultAccountProvider = createAccountProvider(
// The provider used when the user has not selected one: an enterprise/MDM-configured provider,
// else matrix.org. The most recently used provider (from history) can override it, see init.
private val configuredAccountProvider = createAccountProvider(
url = enterpriseService.defaultHomeserverList()
.firstOrNull { it != EnterpriseService.ANY_ACCOUNT_PROVIDER }
?: AuthenticationConfig.MATRIX_ORG_URL
)
private val accountProvider: MutableStateFlow<AccountProvider> = MutableStateFlow(defaultAccountProvider)
private val accountProvider: MutableStateFlow<AccountProvider> = MutableStateFlow(configuredAccountProvider)
val flow: StateFlow<AccountProvider> = accountProvider.asStateFlow()
init {
// Seed the default from the last used provider, unless the user has already selected one.
coroutineScope.launch {
val default = defaultAccountProvider()
accountProvider.update { current -> if (current == configuredAccountProvider) default else current }
}
}
suspend fun reset() {
accountProvider.emit(defaultAccountProvider)
accountProvider.emit(defaultAccountProvider())
}
/**
* The provider to default to: the most recently used one from history when the user is free to
* connect to any provider, otherwise the enterprise/MDM-configured provider.
*/
private suspend fun defaultAccountProvider(): AccountProvider {
if (!enterpriseService.canConnectToAnyHomeserver()) {
return configuredAccountProvider
}
val lastUsedProvider = appPreferencesStore.getHomeserverHistoryFlow().first().firstOrNull()
return lastUsedProvider?.let { createAccountProvider(it) } ?: configuredAccountProvider
}
suspend fun setUrl(url: String) {
@@ -12,9 +12,8 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource
import io.element.android.features.login.impl.classic.FakeElementClassicConnection
import io.element.android.features.preferences.test.FakePreferencesEntryPoint
import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
@@ -38,7 +37,7 @@ class DefaultLoginEntryPointTest {
LoginFlowNode(
buildContext = buildContext,
plugins = plugins,
accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
accountProviderDataSource = anAccountProviderDataSource(),
oAuthActionFlow = FakeOAuthActionFlow(),
appCoroutineScope = backgroundScope,
elementClassicConnection = FakeElementClassicConnection(),
@@ -0,0 +1,25 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.accountprovider
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
fun anAccountProviderDataSource(
enterpriseService: EnterpriseService = FakeEnterpriseService(),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Unconfined),
) = AccountProviderDataSource(
enterpriseService = enterpriseService,
appPreferencesStore = appPreferencesStore,
coroutineScope = coroutineScope,
)
@@ -13,6 +13,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
import org.junit.Rule
@@ -24,7 +25,7 @@ class AccountProviderDataSourceTest {
@Test
fun `present - initial state`() = runTest {
val sut = AccountProviderDataSource(FakeEnterpriseService())
val sut = anAccountProviderDataSource()
sut.flow.test {
val initialState = awaitItem()
assertThat(initialState).isEqualTo(
@@ -41,10 +42,10 @@ class AccountProviderDataSourceTest {
@Test
fun `present - initial state - matrix org`() = runTest {
val sut = AccountProviderDataSource(
FakeEnterpriseService(
val sut = anAccountProviderDataSource(
enterpriseService = FakeEnterpriseService(
defaultHomeserverListResult = { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
)
),
)
sut.flow.test {
val initialState = awaitItem()
@@ -62,10 +63,10 @@ class AccountProviderDataSourceTest {
@Test
fun `present - ensure that default homeserver is not star char`() = runTest {
val sut = AccountProviderDataSource(
FakeEnterpriseService(
val sut = anAccountProviderDataSource(
enterpriseService = FakeEnterpriseService(
defaultHomeserverListResult = { listOf(EnterpriseService.ANY_ACCOUNT_PROVIDER, AuthenticationConfig.MATRIX_ORG_URL) }
)
),
)
sut.flow.test {
val initialState = awaitItem()
@@ -83,7 +84,7 @@ class AccountProviderDataSourceTest {
@Test
fun `present - user change and reset`() = runTest {
val sut = AccountProviderDataSource(FakeEnterpriseService())
val sut = anAccountProviderDataSource()
sut.flow.test {
val initialState = awaitItem()
assertThat(initialState.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL)
@@ -106,7 +107,7 @@ class AccountProviderDataSourceTest {
@Test
fun `present - set url and reset`() = runTest {
val sut = AccountProviderDataSource(FakeEnterpriseService())
val sut = anAccountProviderDataSource()
sut.flow.test {
val initialState = awaitItem()
assertThat(initialState.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL)
@@ -126,4 +127,47 @@ class AccountProviderDataSourceTest {
assertThat(resetState.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL)
}
}
@Test
fun `present - defaults to the most recently used provider from history`() = runTest {
val sut = anAccountProviderDataSource(
appPreferencesStore = InMemoryAppPreferencesStore(
homeserverHistory = listOf("https://example.com", "https://matrix.org"),
),
)
sut.flow.test {
assertThat(awaitItem().url).isEqualTo("https://example.com")
}
}
@Test
fun `present - history is ignored when the account provider is enforced`() = runTest {
val sut = anAccountProviderDataSource(
enterpriseService = FakeEnterpriseService(
defaultHomeserverListResult = { listOf("https://enforced.org") }
),
appPreferencesStore = InMemoryAppPreferencesStore(
homeserverHistory = listOf("https://example.com"),
),
)
sut.flow.test {
assertThat(awaitItem().url).isEqualTo("https://enforced.org")
}
}
@Test
fun `present - reset returns to the most recently used provider from history`() = runTest {
val sut = anAccountProviderDataSource(
appPreferencesStore = InMemoryAppPreferencesStore(
homeserverHistory = listOf("https://example.com"),
),
)
sut.flow.test {
assertThat(awaitItem().url).isEqualTo("https://example.com")
sut.setUrl("https://other.com")
assertThat(awaitItem().url).isEqualTo("https://other.com")
sut.reset()
assertThat(awaitItem().url).isEqualTo("https://example.com")
}
}
}
@@ -14,6 +14,7 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
import io.element.android.features.wellknown.test.FakeWellknownRetriever
@@ -259,7 +260,7 @@ class ChangeServerPresenterTest {
private fun createPresenter(
authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(),
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
accountProviderDataSource: AccountProviderDataSource = anAccountProviderDataSource(),
enterpriseService: EnterpriseService = FakeEnterpriseService(),
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
localNetworkPermissionAdvisor: FakeLocalNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
@@ -8,8 +8,8 @@
package io.element.android.features.login.impl.screens.classic.loginwithclassic
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource
import io.element.android.features.login.impl.classic.ElementClassicConnection
import io.element.android.features.login.impl.classic.ElementClassicConnectionState
import io.element.android.features.login.impl.classic.FakeElementClassicConnection
@@ -249,7 +249,7 @@ private fun createPresenter(
navigator: LoginWithClassicNavigator = FakeLoginWithClassicNavigator(),
loginModePresenter: LoginModePresenter = createLoginModePresenter(),
elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(),
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
accountProviderDataSource: AccountProviderDataSource = anAccountProviderDataSource(),
isEnterpriseBuild: Boolean = false,
) = LoginWithClassicPresenter(
userId = userId,
@@ -10,8 +10,8 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider
import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource
import io.element.android.features.login.impl.login.LoginMode
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
import io.element.android.features.login.impl.screens.onboarding.createLoginModePresenter
@@ -378,7 +378,7 @@ class ConfirmAccountProviderPresenterTest {
private fun createConfirmAccountProviderPresenter(
params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
accountProviderDataSource: AccountProviderDataSource = anAccountProviderDataSource(),
matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
defaultOAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(),
webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(),
@@ -10,8 +10,7 @@ package io.element.android.features.login.impl.screens.createaccount
import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource
import io.element.android.libraries.androidutils.json.DefaultJsonProvider
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
import kotlinx.serialization.SerializationException
@@ -70,7 +69,7 @@ class DefaultMessageParserTest {
private fun createDefaultMessageParser(): DefaultMessageParser {
return DefaultMessageParser(
accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
accountProviderDataSource = anAccountProviderDataSource(),
json = DefaultJsonProvider(),
)
}
@@ -10,8 +10,8 @@ package io.element.android.features.login.impl.screens.loginpassword
import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.AN_EXCEPTION
@@ -157,7 +157,7 @@ class LoginPasswordPresenterTest {
private fun createLoginPasswordPresenter(
initialLogin: String = "",
authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(),
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
accountProviderDataSource: AccountProviderDataSource = anAccountProviderDataSource(),
): LoginPasswordPresenter = LoginPasswordPresenter(
initialLogin = initialLogin,
authenticationService = authenticationService,
@@ -15,6 +15,7 @@ import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
import io.element.android.features.login.impl.login.LoginModePresenter
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
@@ -247,7 +248,7 @@ class OnBoardingPresenterTest {
Result.failure(AN_EXCEPTION)
},
)
val accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService())
val accountProviderDataSource = anAccountProviderDataSource()
val presenter = createPresenter(
params = OnBoardingNode.Params(
accountProvider = A_HOMESERVER_URL,
@@ -297,7 +298,7 @@ private fun createPresenter(
loginModePresenter: LoginModePresenter = createLoginModePresenter(),
onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null },
sessionStore: SessionStore = InMemorySessionStore(),
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
accountProviderDataSource: AccountProviderDataSource = anAccountProviderDataSource(),
) = OnBoardingPresenter(
params = params,
buildMeta = buildMeta,