diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt index 3209fb4631..4e2a3cc1f2 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt @@ -21,4 +21,14 @@ interface EnterpriseService { fun firebasePushGateway(): String? fun unifiedPushDefaultPushGateway(): String? + + companion object { + const val ANY_ACCOUNT_PROVIDER = "*" + } +} + +fun EnterpriseService.canConnectToAnyHomeserver(): Boolean { + return defaultHomeserverList().let { + it.isEmpty() || it.contains(EnterpriseService.ANY_ACCOUNT_PROVIDER) + } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt index 9efce719d8..fb21367501 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt @@ -30,6 +30,7 @@ import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode +import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderNode import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode import io.element.android.features.login.impl.screens.createaccount.CreateAccountNode import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode @@ -107,6 +108,9 @@ class LoginFlowNode @AssistedInject constructor( val isAccountCreation: Boolean, ) : NavTarget + @Parcelize + data object ChooseAccountProvider : NavTarget + @Parcelize data object ChangeAccountProvider : NavTarget @@ -133,9 +137,13 @@ class LoginFlowNode @AssistedInject constructor( ) } - override fun onSignIn() { + override fun onSignIn(mustChooseAccountProvider: Boolean) { backstack.push( - NavTarget.ConfirmAccountProvider(isAccountCreation = false) + if (mustChooseAccountProvider) { + NavTarget.ChooseAccountProvider + } else { + NavTarget.ConfirmAccountProvider(isAccountCreation = false) + } ) } @@ -166,6 +174,22 @@ class LoginFlowNode @AssistedInject constructor( ) createNode(buildContext, listOf(callback, inputs)) } + NavTarget.ChooseAccountProvider -> { + val callback = object : ChooseAccountProviderNode.Callback { + override fun onOidcDetails(oidcDetails: OidcDetails) { + navigateToMas(oidcDetails) + } + + override fun onCreateAccountContinue(url: String) { + backstack.push(NavTarget.CreateAccount(url)) + } + + override fun onLoginPasswordNeeded() { + backstack.push(NavTarget.LoginPassword) + } + } + createNode(buildContext, listOf(callback)) + } NavTarget.QrCode -> { createNode(buildContext) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt index 23d60abd01..9ebc246e25 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt @@ -20,15 +20,16 @@ import javax.inject.Inject class AccountProviderDataSource @Inject constructor( enterpriseService: EnterpriseService, ) { - private val defaultAccountProvider = (enterpriseService.defaultHomeserverList().firstOrNull() ?: AuthenticationConfig.MATRIX_ORG_URL) - .let { url -> - AccountProvider( - url = url, - subtitle = null, - isPublic = url == AuthenticationConfig.MATRIX_ORG_URL, - isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL, - ) - } + private val defaultAccountProvider = + (enterpriseService.defaultHomeserverList().firstOrNull { it != EnterpriseService.ANY_ACCOUNT_PROVIDER } ?: AuthenticationConfig.MATRIX_ORG_URL) + .let { url -> + AccountProvider( + url = url, + subtitle = null, + isPublic = url == AuthenticationConfig.MATRIX_ORG_URL, + isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL, + ) + } private val accountProvider: MutableStateFlow = MutableStateFlow( defaultAccountProvider diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderOtherView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderOtherView.kt new file mode 100644 index 0000000000..92fda4cf14 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderOtherView.kt @@ -0,0 +1,75 @@ +/* + * 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 androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.login.impl.R +import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom +import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.HorizontalDivider +import io.element.android.libraries.designsystem.theme.components.Text + +/** + * https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817 + */ +@Composable +fun AccountProviderOtherView( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .clickable { onClick() } + ) { + HorizontalDivider() + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 44.dp) + .padding(vertical = 4.dp, horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RoundedIconAtom( + size = RoundedIconAtomSize.Medium, + imageVector = CompoundIcons.Search(), + tint = ElementTheme.colors.iconPrimary, + ) + Text( + modifier = Modifier + .padding(start = 16.dp) + .weight(1f), + text = stringResource(R.string.screen_change_account_provider_other), + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun AccountProviderOtherViewPreview() = ElementPreview { + AccountProviderOtherView( + onClick = { }, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt index 75a6127d9f..a5f0fd7d3b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt @@ -23,10 +23,14 @@ open class AccountProviderProvider : PreviewParameterProvider { fun anAccountProvider( url: String = AuthenticationConfig.MATRIX_ORG_URL, + subtitle: String? = "Matrix.org is an open network for secure, decentralized communication.", + isPublic: Boolean = true, + isMatrixOrg: Boolean = true, + isValid: Boolean = true, ) = AccountProvider( url = url, - subtitle = "Matrix.org is an open network for secure, decentralized communication.", - isPublic = true, - isMatrixOrg = true, - isValid = true, + subtitle = subtitle, + isPublic = isPublic, + isMatrixOrg = isMatrixOrg, + isValid = isValid, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt index 091f2e6e02..b61f0283a7 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt @@ -39,6 +39,7 @@ fun AccountProviderView( item: AccountProvider, onClick: () -> Unit, modifier: Modifier = Modifier, + selected: Boolean = false, ) { Column( modifier = modifier @@ -66,7 +67,7 @@ fun AccountProviderView( } else { RoundedIconAtom( size = RoundedIconAtomSize.Medium, - imageVector = CompoundIcons.Search(), + imageVector = CompoundIcons.Host(), tint = ElementTheme.colors.iconPrimary, ) } @@ -88,6 +89,15 @@ fun AccountProviderView( tint = ElementTheme.colors.iconSecondary, ) } + if (selected) { + Icon( + modifier = Modifier + .padding(start = 10.dp), + imageVector = CompoundIcons.Check(), + contentDescription = null, + tint = ElementTheme.colors.iconAccentPrimary, + ) + } } if (item.subtitle != null) { Text( diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt index 2d401d45aa..459f45a06c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.error.ChangeServerError +import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter @@ -31,7 +32,8 @@ import javax.inject.Inject /** * This class is responsible for managing the login flow, including handling OIDC actions and * submitting login requests. - * It's an helper to avoid code duplication. It is used by [OnBoardingPresenter] and [ConfirmAccountProviderPresenter]. + * It's an helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter] + * and [ChooseAccountProviderPresenter]. */ class LoginHelper @Inject constructor( private val oidcActionFlow: OidcActionFlow, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt index 590be0d6a7..bb3da316b1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember 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.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.libraries.architecture.Presenter @@ -25,6 +26,7 @@ class ChangeAccountProviderPresenter @Inject constructor( override fun present(): ChangeAccountProviderState { val staticAccountProviderList = remember { enterpriseService.defaultHomeserverList() + .filter { it != EnterpriseService.ANY_ACCOUNT_PROVIDER } .map { it.ensureProtocol() } .ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) } .map { url -> @@ -38,9 +40,14 @@ class ChangeAccountProviderPresenter @Inject constructor( } } + val canSearchForAccountProviders = remember { + enterpriseService.canConnectToAnyHomeserver() + } + val changeServerState = changeServerPresenter.present() return ChangeAccountProviderState( accountProviders = staticAccountProviderList, + canSearchForAccountProviders = canSearchForAccountProviders, changeServerState = changeServerState, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt index ecd2b8a3d6..e8e1f21cd8 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt @@ -13,5 +13,6 @@ import io.element.android.features.login.impl.changeserver.ChangeServerState // Do not use default value, so no member get forgotten in the presenters. data class ChangeAccountProviderState( val accountProviders: List, + val canSearchForAccountProviders: Boolean, val changeServerState: ChangeServerState, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt index d061303f3a..435eee7f89 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt @@ -8,20 +8,28 @@ package io.element.android.features.login.impl.screens.changeaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider +import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.features.login.impl.changeserver.aChangeServerState open class ChangeAccountProviderStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aChangeAccountProviderState(), + aChangeAccountProviderState(canSearchForAccountProviders = false), // Add other state here ) } -fun aChangeAccountProviderState() = ChangeAccountProviderState( - accountProviders = listOf( +fun aChangeAccountProviderState( + accountProviders: List = listOf( anAccountProvider() ), - changeServerState = aChangeServerState(), + canSearchForAccountProviders: Boolean = true, + changeServerState: ChangeServerState = aChangeServerState(), +) = ChangeAccountProviderState( + accountProviders = accountProviders, + canSearchForAccountProviders = canSearchForAccountProviders, + changeServerState = changeServerState, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt index ad2befb628..c4542dfb0d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.accountprovider.AccountProviderOtherView import io.element.android.features.login.impl.accountprovider.AccountProviderView import io.element.android.features.login.impl.changeserver.ChangeServerEvents import io.element.android.features.login.impl.changeserver.ChangeServerView @@ -95,13 +95,11 @@ fun ChangeAccountProviderView( ) } // Other - AccountProviderView( - item = AccountProvider( - url = "", - title = stringResource(id = R.string.screen_change_account_provider_other), - ), - onClick = onOtherProviderClick - ) + if (state.canSearchForAccountProviders) { + AccountProviderOtherView( + onClick = onOtherProviderClick + ) + } Spacer(Modifier.height(32.dp)) } ChangeServerView( diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderEvents.kt new file mode 100644 index 0000000000..76c86d18c2 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderEvents.kt @@ -0,0 +1,16 @@ +/* + * 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.screens.chooseaccountprovider + +import io.element.android.features.login.impl.accountprovider.AccountProvider + +sealed interface ChooseAccountProviderEvents { + data class SelectAccountProvider(val accountProvider: AccountProvider) : ChooseAccountProviderEvents + data object Continue : ChooseAccountProviderEvents + data object ClearError : ChooseAccountProviderEvents +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderNode.kt new file mode 100644 index 0000000000..2189252d01 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderNode.kt @@ -0,0 +1,62 @@ +/* + * 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.screens.chooseaccountprovider + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.login.impl.util.openLearnMorePage +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.auth.OidcDetails + +@ContributesNode(AppScope::class) +class ChooseAccountProviderNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: ChooseAccountProviderPresenter, +) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onLoginPasswordNeeded() + fun onOidcDetails(oidcDetails: OidcDetails) + fun onCreateAccountContinue(url: String) + } + + private fun onOidcDetails(oidcDetails: OidcDetails) { + plugins().forEach { it.onOidcDetails(oidcDetails) } + } + + private fun onLoginPasswordNeeded() { + plugins().forEach { it.onLoginPasswordNeeded() } + } + + private fun onCreateAccountContinue(url: String) { + plugins().forEach { it.onCreateAccountContinue(url) } + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + val context = LocalContext.current + ChooseAccountProviderView( + state = state, + modifier = modifier, + onBackClick = ::navigateUp, + onOidcDetails = ::onOidcDetails, + onNeedLoginPassword = ::onLoginPasswordNeeded, + onLearnMoreClick = { openLearnMorePage(context) }, + onCreateAccountContinue = ::onCreateAccountContinue, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt new file mode 100644 index 0000000000..464e30936f --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt @@ -0,0 +1,80 @@ +/* + * 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.screens.chooseaccountprovider + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import io.element.android.appconfig.AuthenticationConfig +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.uri.ensureProtocol +import javax.inject.Inject + +class ChooseAccountProviderPresenter @Inject constructor( + private val enterpriseService: EnterpriseService, + private val loginHelper: LoginHelper, +) : Presenter { + @Composable + override fun present(): ChooseAccountProviderState { + val localCoroutineScope = rememberCoroutineScope() + val loginMode by loginHelper.collectLoginMode() + + var selectedAccountProvider: AccountProvider? by remember { mutableStateOf(null) } + + fun handleEvent(event: ChooseAccountProviderEvents) { + when (event) { + ChooseAccountProviderEvents.Continue -> { + selectedAccountProvider?.let { + loginHelper.submit( + coroutineScope = localCoroutineScope, + isAccountCreation = false, + homeserverUrl = it.url, + loginHint = null, + ) + } + } + is ChooseAccountProviderEvents.SelectAccountProvider -> { + // Ensure that the user do not change the server during processing + if (loginMode is AsyncData.Uninitialized) { + selectedAccountProvider = event.accountProvider + } + } + ChooseAccountProviderEvents.ClearError -> loginHelper.clearError() + } + } + + val staticAccountProviderList = remember { + // The list cannot contains ANY_ACCOUNT_PROVIDER ("*") and cannot be empty at this point + enterpriseService.defaultHomeserverList() + .map { it.ensureProtocol() } + .map { url -> + AccountProvider( + url = url, + subtitle = null, + isPublic = url == AuthenticationConfig.MATRIX_ORG_URL, + isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL, + isValid = true, + ) + } + } + + return ChooseAccountProviderState( + accountProviders = staticAccountProviderList, + selectedAccountProvider = selectedAccountProvider, + loginMode = loginMode, + eventSink = ::handleEvent, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt new file mode 100644 index 0000000000..3591334047 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt @@ -0,0 +1,23 @@ +/* + * 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.screens.chooseaccountprovider + +import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.login.LoginMode +import io.element.android.libraries.architecture.AsyncData + +// Do not use default value, so no member get forgotten in the presenters. +data class ChooseAccountProviderState( + val accountProviders: List, + val selectedAccountProvider: AccountProvider?, + val loginMode: AsyncData, + val eventSink: (ChooseAccountProviderEvents) -> Unit, +) { + val submitEnabled: Boolean + get() = selectedAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt new file mode 100644 index 0000000000..b921fee330 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt @@ -0,0 +1,77 @@ +/* + * 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.screens.chooseaccountprovider + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.accountprovider.anAccountProvider +import io.element.android.features.login.impl.login.LoginMode +import io.element.android.libraries.architecture.AsyncData + +open class ChooseAccountProviderStateProvider : PreviewParameterProvider { + private val server1 = anAccountProvider( + url = "https://server1.io", + subtitle = null, + isPublic = false, + isMatrixOrg = false, + ) + private val server2 = anAccountProvider( + url = "https://server2.io", + subtitle = null, + isPublic = false, + isMatrixOrg = false, + ) + private val server3 = anAccountProvider( + url = "https://server3.io", + subtitle = null, + isPublic = false, + isMatrixOrg = false, + ) + override val values: Sequence + get() = sequenceOf( + aChooseAccountProviderState( + accountProviders = listOf( + server1, + server2, + server3, + ) + ), + aChooseAccountProviderState( + accountProviders = listOf( + server1, + server2, + server3, + ), + selectedAccountProvider = server2, + ), + aChooseAccountProviderState( + accountProviders = listOf( + server1, + server2, + server3, + ), + selectedAccountProvider = server2, + loginMode = AsyncData.Loading(), + ), + // Add other state here + ) +} + +fun aChooseAccountProviderState( + accountProviders: List = listOf( + anAccountProvider() + ), + selectedAccountProvider: AccountProvider? = null, + loginMode: AsyncData = AsyncData.Uninitialized, + eventSink: (ChooseAccountProviderEvents) -> Unit = {}, +) = ChooseAccountProviderState( + accountProviders = accountProviders, + selectedAccountProvider = selectedAccountProvider, + loginMode = loginMode, + eventSink = eventSink, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt new file mode 100644 index 0000000000..760e39ce85 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt @@ -0,0 +1,150 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package io.element.android.features.login.impl.screens.chooseaccountprovider + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.login.impl.R +import io.element.android.features.login.impl.accountprovider.AccountProviderView +import io.element.android.features.login.impl.login.LoginModeView +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun ChooseAccountProviderView( + state: ChooseAccountProviderState, + onBackClick: () -> Unit, + onOidcDetails: (OidcDetails) -> Unit, + onNeedLoginPassword: () -> Unit, + onLearnMoreClick: () -> Unit, + onCreateAccountContinue: (url: String) -> Unit, + modifier: Modifier = Modifier, +) { + val isLoading by remember(state.loginMode) { + derivedStateOf { + state.loginMode is AsyncData.Loading + } + } + + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = {}, + navigationIcon = { BackButton(onClick = onBackClick) } + ) + } + ) { padding -> + Box( + modifier = Modifier + .fillMaxSize() + .imePadding() + .padding(padding) + .consumeWindowInsets(padding) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(state = rememberScrollState()) + ) { + IconTitleSubtitleMolecule( + modifier = Modifier.padding(top = 16.dp, bottom = 32.dp, start = 16.dp, end = 16.dp), + iconStyle = BigIcon.Style.Default(CompoundIcons.HomeSolid()), + title = stringResource(id = R.string.screen_server_confirmation_title_picker_mode), + subTitle = null, + ) + + state.accountProviders.forEach { item -> + val alteredItem = if (item.isMatrixOrg) { + // Set the subtitle from the resource + item.copy( + subtitle = stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle), + ) + } else { + item + } + AccountProviderView( + item = alteredItem, + selected = item == state.selectedAccountProvider, + onClick = { + state.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(item)) + } + ) + } + Spacer(Modifier.height(32.dp)) + // Flexible spacing to keep the submit button at the bottom + Spacer(modifier = Modifier.weight(1f)) + Button( + text = stringResource(id = CommonStrings.action_continue), + showProgress = isLoading, + onClick = { + state.eventSink(ChooseAccountProviderEvents.Continue) + }, + enabled = state.submitEnabled || isLoading, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + Spacer(modifier = Modifier.height(48.dp)) + } + LoginModeView( + loginMode = state.loginMode, + onClearError = { + state.eventSink(ChooseAccountProviderEvents.ClearError) + }, + onLearnMoreClick = onLearnMoreClick, + onOidcDetails = onOidcDetails, + onNeedLoginPassword = onNeedLoginPassword, + onCreateAccountContinue = onCreateAccountContinue, + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun ChooseAccountProviderViewPreview(@PreviewParameter(ChooseAccountProviderStateProvider::class) state: ChooseAccountProviderState) = ElementPreview { + ChooseAccountProviderView( + state = state, + onBackClick = { }, + onLearnMoreClick = { }, + onOidcDetails = { }, + onNeedLoginPassword = { }, + onCreateAccountContinue = { }, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt index f17372b3ab..d9c1615fde 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt @@ -34,7 +34,7 @@ class OnBoardingNode @AssistedInject constructor( ) { interface Callback : Plugin { fun onSignUp() - fun onSignIn() + fun onSignIn(mustChooseAccountProvider: Boolean) fun onSignInWithQrCode() fun onReportProblem() fun onLoginPasswordNeeded() @@ -53,8 +53,8 @@ class OnBoardingNode @AssistedInject constructor( params = params, ) - private fun onSignIn() { - plugins().forEach { it.onSignIn() } + private fun onSignIn(mustChooseAccountProvider: Boolean) { + plugins().forEach { it.onSignIn(mustChooseAccountProvider) } } private fun onSignUp() { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt index 38ea9c3b49..f4696133fe 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt @@ -16,6 +16,8 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.appconfig.OnBoardingConfig +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.enterprise.api.canConnectToAnyHomeserver import io.element.android.features.login.impl.login.LoginHelper import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.libraries.architecture.Presenter @@ -27,6 +29,7 @@ class OnBoardingPresenter @AssistedInject constructor( @Assisted private val params: OnBoardingNode.Params, private val buildMeta: BuildMeta, private val featureFlagService: FeatureFlagService, + private val enterpriseService: EnterpriseService, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, private val loginHelper: LoginHelper, ) : Presenter { @@ -37,15 +40,33 @@ class OnBoardingPresenter @AssistedInject constructor( ): OnBoardingPresenter } - private val defaultAccountProvider = params.accountProvider - private val loginHint = params.loginHint - @Composable override fun present(): OnBoardingState { val localCoroutineScope = rememberCoroutineScope() - - val canLoginWithQrCode by produceState(initialValue = false) { - value = defaultAccountProvider == null && + val forcedAccountProvider = remember { + // If defaultHomeserverList() returns a singleton list, this is the default account provider. + // In this case, the user can sign in using this homeserver, or use QrCode login + enterpriseService.defaultHomeserverList().singleOrNull() + } + val canConnectToAnyHomeserver = remember { + enterpriseService.canConnectToAnyHomeserver() + } + val mustChooseAccountProvider = remember { + !canConnectToAnyHomeserver && enterpriseService.defaultHomeserverList().size > 1 + } + val linkAccountProvider by produceState(initialValue = null) { + // Account provider from the link, if allowed by the enterprise service + value = params.accountProvider?.takeIf { + enterpriseService.isAllowedToConnectToHomeserver(it) + } + } + val defaultAccountProvider = remember(linkAccountProvider) { + // If there is a forced account provider, this is the default account provider + // Else use the account provider passed in the params if any and if allowed + forcedAccountProvider ?: linkAccountProvider + } + val canLoginWithQrCode by produceState(initialValue = false, linkAccountProvider) { + value = linkAccountProvider == null && featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin) } val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() } @@ -58,7 +79,7 @@ class OnBoardingPresenter @AssistedInject constructor( coroutineScope = localCoroutineScope, isAccountCreation = false, homeserverUrl = event.defaultAccountProvider, - loginHint = loginHint, + loginHint = params.loginHint?.takeIf { forcedAccountProvider == null }, ) OnBoardingEvents.ClearError -> loginHelper.clearError() } @@ -67,8 +88,9 @@ class OnBoardingPresenter @AssistedInject constructor( return OnBoardingState( productionApplicationName = buildMeta.productionApplicationName, defaultAccountProvider = defaultAccountProvider, + mustChooseAccountProvider = mustChooseAccountProvider, canLoginWithQrCode = canLoginWithQrCode, - canCreateAccount = defaultAccountProvider == null && OnBoardingConfig.CAN_CREATE_ACCOUNT, + canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT, canReportBug = canReportBug, loginMode = loginMode, eventSink = ::handleEvent, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt index ded9ae7102..98484a1fc2 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt @@ -13,6 +13,7 @@ import io.element.android.libraries.architecture.AsyncData data class OnBoardingState( val productionApplicationName: String, val defaultAccountProvider: String?, + val mustChooseAccountProvider: Boolean, val canLoginWithQrCode: Boolean, val canCreateAccount: Boolean, val canReportBug: Boolean, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt index 975f9d2e7f..9be36719e2 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt @@ -26,6 +26,7 @@ open class OnBoardingStateProvider : PreviewParameterProvider { fun anOnBoardingState( productionApplicationName: String = "Element", defaultAccountProvider: String? = null, + mustChooseAccountProvider: Boolean = false, canLoginWithQrCode: Boolean = false, canCreateAccount: Boolean = false, canReportBug: Boolean = false, @@ -34,6 +35,7 @@ fun anOnBoardingState( ) = OnBoardingState( productionApplicationName = productionApplicationName, defaultAccountProvider = defaultAccountProvider, + mustChooseAccountProvider = mustChooseAccountProvider, canLoginWithQrCode = canLoginWithQrCode, canCreateAccount = canCreateAccount, canReportBug = canReportBug, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt index dd41e38e64..99ab348b06 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt @@ -56,7 +56,7 @@ import io.element.android.libraries.ui.strings.CommonStrings fun OnBoardingView( state: OnBoardingState, onSignInWithQrCode: () -> Unit, - onSignIn: () -> Unit, + onSignIn: (mustChooseAccountProvider: Boolean) -> Unit, onCreateAccount: () -> Unit, onOidcDetails: (OidcDetails) -> Unit, onNeedLoginPassword: () -> Unit, @@ -143,7 +143,7 @@ private fun OnBoardingContent(state: OnBoardingState) { private fun OnBoardingButtons( state: OnBoardingState, onSignInWithQrCode: () -> Unit, - onSignIn: () -> Unit, + onSignIn: (mustChooseAccountProvider: Boolean) -> Unit, onCreateAccount: () -> Unit, onReportProblem: () -> Unit, ) { @@ -171,7 +171,9 @@ private fun OnBoardingButtons( if (defaultAccountProvider == null) { Button( text = stringResource(id = signInButtonStringRes), - onClick = onSignIn, + onClick = { + onSignIn(state.mustChooseAccountProvider) + }, modifier = Modifier .fillMaxWidth() .testTag(TestTags.onBoardingSignIn) diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index 998a0bc775..4f4d98baaf 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -89,5 +89,6 @@ Try signing in manually, or scan the QR code with another device." "Matrix is an open network for secure, decentralised communication." "This is where your conversations will live — just like you would use an email provider to keep your emails." "You’re about to sign in to %1$s" + "Choose account provider" "You’re about to create an account on %1$s" diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceTest.kt index a5ede23b2e..3d84a1da41 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceTest.kt @@ -10,6 +10,7 @@ package io.element.android.features.login.impl.accountprovider import app.cash.turbine.test 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.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest @@ -60,6 +61,28 @@ class AccountProviderDataSourceTest { } } + @Test + fun `present - ensure that default homeserver is not star char`() = runTest { + val sut = AccountProviderDataSource( + FakeEnterpriseService( + defaultHomeserverListResult = { listOf(EnterpriseService.ANY_ACCOUNT_PROVIDER, AuthenticationConfig.MATRIX_ORG_URL) } + ) + ) + sut.flow.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo( + AccountProvider( + url = AuthenticationConfig.MATRIX_ORG_URL, + title = "matrix.org", + subtitle = null, + isPublic = true, + isMatrixOrg = true, + isValid = false, + ) + ) + } + } + @Test fun `present - user change and reset`() = runTest { val sut = AccountProviderDataSource(FakeEnterpriseService()) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt index dbce8164c6..89abc6ddef 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt @@ -11,9 +11,12 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.changeserver.aChangeServerState +import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER +import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2 import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -27,7 +30,9 @@ class ChangeAccountProviderPresenterTest { fun `present - initial state`() = runTest { val presenter = ChangeAccountProviderPresenter( changeServerPresenter = { aChangeServerState() }, - enterpriseService = FakeEnterpriseService(), + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { emptyList() } + ), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -45,6 +50,75 @@ class ChangeAccountProviderPresenterTest { ) ) ) + assertThat(initialState.canSearchForAccountProviders).isTrue() + } + } + + @Test + fun `present - fixed list of account providers`() = runTest { + val presenter = ChangeAccountProviderPresenter( + changeServerPresenter = { aChangeServerState() }, + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { + listOf(AN_ACCOUNT_PROVIDER, AN_ACCOUNT_PROVIDER_2) + } + ), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.accountProviders).isEqualTo( + listOf( + AccountProvider( + url = "https://matrix.org", + title = "matrix.org", + subtitle = null, + isPublic = true, + isMatrixOrg = true, + isValid = true, + ), + AccountProvider( + url = "https://element.io", + title = "element.io", + subtitle = null, + isPublic = false, + isMatrixOrg = false, + isValid = true, + ) + ) + ) + assertThat(initialState.canSearchForAccountProviders).isFalse() + } + } + + @Test + fun `present - opened list of account providers`() = runTest { + val presenter = ChangeAccountProviderPresenter( + changeServerPresenter = { aChangeServerState() }, + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { + listOf(AN_ACCOUNT_PROVIDER, EnterpriseService.ANY_ACCOUNT_PROVIDER) + } + ), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.accountProviders).isEqualTo( + listOf( + AccountProvider( + url = "https://matrix.org", + title = "matrix.org", + subtitle = null, + isPublic = true, + isMatrixOrg = true, + isValid = true, + ) + ) + ) + assertThat(initialState.canSearchForAccountProviders).isTrue() } } } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt new file mode 100644 index 0000000000..98a185cac4 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt @@ -0,0 +1,167 @@ +/* + * 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.screens.chooseaccountprovider + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.enterprise.test.FakeEnterpriseService +import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.screens.onboarding.createLoginHelper +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.core.uri.ensureProtocol +import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2 +import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_3 +import io.element.android.libraries.matrix.test.A_THROWABLE +import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class ChooseAccountProviderPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + companion object { + private const val ACCOUNT_PROVIDER_FROM_CONFIG_1 = AN_ACCOUNT_PROVIDER_2 + private const val ACCOUNT_PROVIDER_FROM_CONFIG_2 = AN_ACCOUNT_PROVIDER_3 + val accountProvider1 = AccountProvider( + url = ACCOUNT_PROVIDER_FROM_CONFIG_1.ensureProtocol(), + subtitle = null, + isPublic = false, + isMatrixOrg = false, + isValid = true, + ) + val accountProvider2 = AccountProvider( + url = ACCOUNT_PROVIDER_FROM_CONFIG_2.ensureProtocol(), + subtitle = null, + isPublic = false, + isMatrixOrg = false, + isValid = true, + ) + } + + @Test + fun `present - ensure initial conditions`() { + assertThat( + setOf( + ACCOUNT_PROVIDER_FROM_CONFIG_1, + ACCOUNT_PROVIDER_FROM_CONFIG_2, + ).size + ).isEqualTo(2) + } + + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter( + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, + ), + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.accountProviders).containsExactly( + accountProvider1, + accountProvider2, + ) + assertThat(initialState.selectedAccountProvider).isNull() + } + } + + @Test + fun `present - Continue when no account provider is selected has no effect`() = runTest { + val authenticationService = FakeMatrixAuthenticationService() + val presenter = createPresenter( + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, + ), + loginHelper = createLoginHelper( + authenticationService = authenticationService, + ), + ) + presenter.test { + awaitItem().also { + assertThat(it.selectedAccountProvider).isNull() + it.eventSink(ChooseAccountProviderEvents.Continue) + expectNoEvents() + } + } + } + + @Test + fun `present - select account provider and continue - error then clear error`() = runTest { + val authenticationService = FakeMatrixAuthenticationService() + val presenter = createPresenter( + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, + ), + loginHelper = createLoginHelper( + authenticationService = authenticationService, + ), + ) + presenter.test { + awaitItem().also { + assertThat(it.selectedAccountProvider).isNull() + it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider1)) + } + awaitItem().also { + assertThat(it.selectedAccountProvider).isEqualTo(accountProvider1) + authenticationService.givenChangeServerError(A_THROWABLE) + it.eventSink(ChooseAccountProviderEvents.Continue) + skipItems(1) // Loading + + // Check an error was returned + val submittedState = awaitItem() + assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + + // Assert the error is then cleared + submittedState.eventSink(ChooseAccountProviderEvents.ClearError) + val clearedState = awaitItem() + assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) + } + } + } + + @Test + fun `present - default account provider - select account provider during login has no effect`() = runTest { + val authenticationService = FakeMatrixAuthenticationService() + val presenter = createPresenter( + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, + ), + loginHelper = createLoginHelper( + authenticationService = authenticationService, + ), + ) + presenter.test { + awaitItem().also { + assertThat(it.selectedAccountProvider).isNull() + it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider1)) + } + awaitItem().also { + assertThat(it.selectedAccountProvider).isEqualTo(accountProvider1) + it.eventSink(ChooseAccountProviderEvents.Continue) + } + awaitItem().also { + assertThat(it.loginMode.isLoading()).isTrue() + it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider2)) + } + expectNoEvents() + } + } +} + +private fun createPresenter( + enterpriseService: EnterpriseService = FakeEnterpriseService(), + loginHelper: LoginHelper = createLoginHelper(), +) = ChooseAccountProviderPresenter( + enterpriseService = enterpriseService, + loginHelper = loginHelper, +) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt new file mode 100644 index 0000000000..a044874eed --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt @@ -0,0 +1,102 @@ +/* + * 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.screens.chooseaccountprovider + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.login.impl.accountprovider.anAccountProvider +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class ChooseAccountProviderViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `clicking on back invokes the expected callback`() { + val eventSink = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setChooseAccountProviderView( + state = aChooseAccountProviderState( + eventSink = eventSink, + ), + onBackClick = it, + ) + rule.pressBack() + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `selecting an account provider emits the the expected event`() { + val eventSink = EventsRecorder() + rule.setChooseAccountProviderView( + state = aChooseAccountProviderState( + accountProviders = listOf( + ChooseAccountProviderPresenterTest.accountProvider1, + ChooseAccountProviderPresenterTest.accountProvider2, + ), + selectedAccountProvider = anAccountProvider(), + eventSink = eventSink, + ), + ) + rule.onNodeWithText(ChooseAccountProviderPresenterTest.accountProvider1.title).performClick() + eventSink.assertSingle(ChooseAccountProviderEvents.SelectAccountProvider(ChooseAccountProviderPresenterTest.accountProvider1)) + } + + @Test + fun `when error is displayed - closing the dialog emits the expected event`() { + val eventSink = EventsRecorder() + rule.setChooseAccountProviderView( + state = aChooseAccountProviderState( + loginMode = AsyncData.Failure(AN_EXCEPTION), + eventSink = eventSink, + ), + ) + rule.clickOn(CommonStrings.action_ok) + eventSink.assertSingle(ChooseAccountProviderEvents.ClearError) + } + + private fun AndroidComposeTestRule.setChooseAccountProviderView( + state: ChooseAccountProviderState, + onBackClick: () -> Unit = EnsureNeverCalled(), + onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(), + onNeedLoginPassword: () -> Unit = EnsureNeverCalled(), + onLearnMoreClick: () -> Unit = EnsureNeverCalled(), + onCreateAccountContinue: (url: String) -> Unit = EnsureNeverCalledWithParam(), + ) { + setContent { + ChooseAccountProviderView( + state = state, + onBackClick = onBackClick, + onOidcDetails = onOidcDetails, + onNeedLoginPassword = onNeedLoginPassword, + onLearnMoreClick = onLearnMoreClick, + onCreateAccountContinue = onCreateAccountContinue, + ) + } + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt index 06034fef19..45be02b128 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt @@ -9,6 +9,8 @@ package io.element.android.features.login.impl.screens.onboarding import com.google.common.truth.Truth.assertThat import io.element.android.appconfig.OnBoardingConfig +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.login.LoginHelper import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever @@ -19,6 +21,9 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER +import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2 +import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_3 import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_LOGIN_HINT import io.element.android.libraries.matrix.test.A_THROWABLE @@ -36,6 +41,23 @@ class OnBoardingPresenterTest { @get:Rule val warmUpRule = WarmUpRule() + companion object { + private const val ACCOUNT_PROVIDER_FROM_LINK = AN_ACCOUNT_PROVIDER + private const val ACCOUNT_PROVIDER_FROM_CONFIG = AN_ACCOUNT_PROVIDER_2 + private const val ACCOUNT_PROVIDER_FROM_CONFIG_2 = AN_ACCOUNT_PROVIDER_3 + } + + @Test + fun `present - ensure initial conditions`() { + assertThat( + setOf( + ACCOUNT_PROVIDER_FROM_LINK, + ACCOUNT_PROVIDER_FROM_CONFIG, + ACCOUNT_PROVIDER_FROM_CONFIG_2, + ).size + ).isEqualTo(3) + } + @Test fun `present - initial state`() = runTest { val buildMeta = aBuildMeta( @@ -50,10 +72,14 @@ class OnBoardingPresenterTest { val presenter = createPresenter( buildMeta = buildMeta, featureFlagService = featureFlagService, + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) }, + ), rageshakeFeatureAvailability = { true }, ) presenter.test { val initialState = awaitItem() + assertThat(initialState.defaultAccountProvider).isNull() assertThat(initialState.canLoginWithQrCode).isFalse() assertThat(initialState.productionApplicationName).isEqualTo("B") assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT) @@ -74,22 +100,79 @@ class OnBoardingPresenterTest { } @Test - fun `present - default account provider`() = runTest { + fun `present - opening the app using link with allowed account provider, and the app does not force account provider`() = runTest { val presenter = createPresenter( params = OnBoardingNode.Params( - accountProvider = A_HOMESERVER_URL, + accountProvider = ACCOUNT_PROVIDER_FROM_LINK, loginHint = null, ), + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.QrCodeLogin.key to true), + ), + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) }, + isAllowedToConnectToHomeserverResult = { true }, + ), ) presenter.test { + skipItems(3) awaitItem().also { - assertThat(it.defaultAccountProvider).isEqualTo(A_HOMESERVER_URL) + assertThat(it.defaultAccountProvider).isEqualTo(ACCOUNT_PROVIDER_FROM_LINK) assertThat(it.canLoginWithQrCode).isFalse() assertThat(it.canCreateAccount).isFalse() } } } + @Test + fun `present - opening the app using link with not allowed account provider, and the app does not force account provider`() = runTest { + val presenter = createPresenter( + params = OnBoardingNode.Params( + accountProvider = ACCOUNT_PROVIDER_FROM_LINK, + loginHint = null, + ), + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.QrCodeLogin.key to true), + ), + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, + isAllowedToConnectToHomeserverResult = { false }, + ), + ) + presenter.test { + skipItems(1) + awaitItem().also { + assertThat(it.defaultAccountProvider).isNull() + assertThat(it.canLoginWithQrCode).isTrue() + assertThat(it.canCreateAccount).isFalse() + } + } + } + + @Test + fun `present - opening the app using link, and the app forces account provider`() = runTest { + val presenter = createPresenter( + params = OnBoardingNode.Params( + accountProvider = ACCOUNT_PROVIDER_FROM_LINK, + loginHint = null, + ), + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.QrCodeLogin.key to true), + ), + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG) }, + ) + ) + presenter.test { + skipItems(1) + awaitItem().also { + assertThat(it.defaultAccountProvider).isEqualTo(ACCOUNT_PROVIDER_FROM_CONFIG) + assertThat(it.canLoginWithQrCode).isTrue() + assertThat(it.canCreateAccount).isFalse() + } + } + } + @Test fun `present - default account provider - login and clear error`() = runTest { val authenticationService = FakeMatrixAuthenticationService() @@ -98,11 +181,15 @@ class OnBoardingPresenterTest { accountProvider = A_HOMESERVER_URL, loginHint = A_LOGIN_HINT, ), + enterpriseService = FakeEnterpriseService( + isAllowedToConnectToHomeserverResult = { true }, + ), loginHelper = createLoginHelper( authenticationService = authenticationService, ), ) presenter.test { + skipItems(3) awaitItem().also { assertThat(it.defaultAccountProvider).isEqualTo(A_HOMESERVER_URL) authenticationService.givenChangeServerError(A_THROWABLE) @@ -126,12 +213,14 @@ private fun createPresenter( params: OnBoardingNode.Params = OnBoardingNode.Params(null, null), buildMeta: BuildMeta = aBuildMeta(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(), + enterpriseService: EnterpriseService = FakeEnterpriseService(), rageshakeFeatureAvailability: () -> Boolean = { true }, loginHelper: LoginHelper = createLoginHelper(), ) = OnBoardingPresenter( params = params, buildMeta = buildMeta, featureFlagService = featureFlagService, + enterpriseService = enterpriseService, rageshakeFeatureAvailability = rageshakeFeatureAvailability, loginHelper = loginHelper, ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt index 141b5c29ae..52af14cfe4 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt @@ -23,6 +23,7 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.ensureCalledOnceWithParam import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -56,10 +57,28 @@ class OnboardingViewTest { } @Test - fun `when can login with QR code - clicking on sign in manually calls the expected callback`() { - ensureCalledOnce { callback -> + fun `when can login with QR code - clicking on sign in manually calls the expected callback - can search account provider`() { + `when can login with QR code - clicking on sign in manually calls the expected callback`( + mustChooseAccountProvider = false, + ) + } + + @Test + fun `when can login with QR code - clicking on sign in manually calls the expected callback - cannot search account provider`() { + `when can login with QR code - clicking on sign in manually calls the expected callback`( + mustChooseAccountProvider = true, + ) + } + + private fun `when can login with QR code - clicking on sign in manually calls the expected callback`( + mustChooseAccountProvider: Boolean, + ) { + ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> rule.setOnboardingView( - state = anOnBoardingState(canLoginWithQrCode = true), + state = anOnBoardingState( + canLoginWithQrCode = true, + mustChooseAccountProvider = mustChooseAccountProvider, + ), onSignIn = callback, ) rule.clickOn(R.string.screen_onboarding_sign_in_manually) @@ -67,12 +86,28 @@ class OnboardingViewTest { } @Test - fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback`() { - ensureCalledOnce { callback -> + fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback - can search account provider`() { + `when cannot login with QR code or create account - clicking on continue calls the sign in callback`( + mustChooseAccountProvider = false, + ) + } + + @Test + fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback - cannot search account provider`() { + `when cannot login with QR code or create account - clicking on continue calls the sign in callback`( + mustChooseAccountProvider = true, + ) + } + + private fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback`( + mustChooseAccountProvider: Boolean, + ) { + ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> rule.setOnboardingView( state = anOnBoardingState( canLoginWithQrCode = false, canCreateAccount = false, + mustChooseAccountProvider = mustChooseAccountProvider, ), onSignIn = callback, ) @@ -137,7 +172,7 @@ class OnboardingViewTest { private fun AndroidComposeTestRule.setOnboardingView( state: OnBoardingState, onSignInWithQrCode: () -> Unit = EnsureNeverCalled(), - onSignIn: () -> Unit = EnsureNeverCalled(), + onSignIn: (Boolean) -> Unit = EnsureNeverCalledWithParam(), onCreateAccount: () -> Unit = EnsureNeverCalled(), onReportProblem: () -> Unit = EnsureNeverCalled(), onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(), diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index 11a0257685..3a354df6b9 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -69,6 +69,10 @@ const val A_REDACTION_REASON = "A redaction reason" const val A_HOMESERVER_URL = "matrix.org" const val A_HOMESERVER_URL_2 = "matrix-client.org" +const val AN_ACCOUNT_PROVIDER = "matrix.org" +const val AN_ACCOUNT_PROVIDER_2 = "element.io" +const val AN_ACCOUNT_PROVIDER_3 = "other.io" + val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidcLogin = false) val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true) val A_ROOM_NOTIFICATION_MODE = RoomNotificationMode.MUTE diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en.png new file mode 100644 index 0000000000..31b7ce8217 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b4adf1bb086cb46f6a9cc20dffe23cba656a571a4f6651a82063907e82a117e +size 5843 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en.png new file mode 100644 index 0000000000..e96cb9b338 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d7ed9363bd7b6f09be20f58019260dceb36c8cbebf1c74474b94e227bebb37d +size 5926 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_3_en.png index cccd463c66..e7c3642e9f 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d742af6422e7f5ce0070831e921180006a5b48d52c042fdc52030435597f242 -size 5835 +oid sha256:ef744b07b6e40d1017713b3e44c2e6d358495ea9e1ff88c183c963994e216738 +size 5554 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_3_en.png index 6e685f122d..ab8bcda276 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4731443df36855c8785072a8085fc9968ec231ff46ff24012b0ae72cd084e46 -size 5867 +oid sha256:04a8e2e856805b0888f901278537a217e233445b9f09060e1f4b764db87146c8 +size 5589 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png index c76898c457..28032f8d60 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5d38292c3927910a4ac9df5d2ad63dddf41818611096db2ba4a2101b880c8c0 -size 47316 +oid sha256:4406e5c57eea02b3ac315b303e0995e6ad0ae1b25f751cb8a154220e08a2bf23 +size 47284 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en.png new file mode 100644 index 0000000000..fcaae762cc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc85189c4ebe4539c02294c5fa98d41816321848de5d68718de2dffef010719e +size 45074 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png index 81774f463f..5f4d6441ca 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a954fc1d3091b50581dcb4cdd9ca89acb0c9c92e35fc9ffd6bfe240685d37b0 -size 46333 +oid sha256:35820145f9f9fe836035774390808d5778cdca064730a3b4ebe62f66a26fafae +size 46350 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en.png new file mode 100644 index 0000000000..1bcba3b056 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a3e19f270562bbd81c9508e90319b819c08ecc766f5d08adc657aa2781ad679 +size 44112 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en.png new file mode 100644 index 0000000000..3ca626466a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4051b09fbcb571273ff3edf21d482fa75684b73846a4992aa495274dbc8fcb97 +size 22602 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en.png new file mode 100644 index 0000000000..d8831b99f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2f1145b53dd1b031d3fb213217fd5128b5dae6f5882f0af5f339973f61d8748 +size 23556 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en.png new file mode 100644 index 0000000000..605f09a379 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8dc7e4a04653998514fb3ca02bd220fdb0979c5ce8251031cf90e51f6a7265c +size 24082 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en.png new file mode 100644 index 0000000000..37a1c8060a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf820f7343125063ff3e92f3d4902e427a424d148760168884cd143628925010 +size 22111 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en.png new file mode 100644 index 0000000000..0b3b32b30f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9bd58639caaa55c921244a15b0f4c1d3997b4c591cab4ad46ddcfc07d7a4fdc1 +size 23010 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en.png new file mode 100644 index 0000000000..7c80a5e9f4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9dfd90cd62e5511075857c5642660228b553208bda00cf07d51db087b8b37a9 +size 23508 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png index 469acb982f..f5f864b6aa 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5ac88572a7fbcacbf571b86586a887ec4931c9e92c15f2ac3633e0adc4088e4 -size 51364 +oid sha256:cb99a065e901322c590b5331880e8035b6ab6101d4e2f4603ceb3abce2344147 +size 50804 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png index 751f19d89c..485a3a2552 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02f1214c9ced09fcb178d04ec0e8683ecb1f31c169056db9a3849e6680d060fe -size 50318 +oid sha256:ecc79b3e57881971e5bfc7d8e2937b4e9ccb18536851be3fc79154b544aa6895 +size 49800