On boarding flow: add a screen to select account provider among a fixed list (#4769)
* Hide login with QrCode when the app is opened by a link * Fix UI on ChangeAccountProviderView. * Add flow to choose between a fixed list of account provider * Update screenshots * Fix licence header * Rename preview. * Ensure that the default account provider cannot be "*" This should not happen IRL, but better be robust against issue in application configuration. * Create const of any account provider value * Fix typo --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<OnBoardingNode>(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<ChooseAccountProviderNode>(buildContext, listOf(callback))
|
||||
}
|
||||
NavTarget.QrCode -> {
|
||||
createNode<QrCodeLoginFlowNode>(buildContext)
|
||||
}
|
||||
|
||||
@@ -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<AccountProvider> = MutableStateFlow(
|
||||
defaultAccountProvider
|
||||
|
||||
@@ -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 = { },
|
||||
)
|
||||
}
|
||||
@@ -23,10 +23,14 @@ open class AccountProviderProvider : PreviewParameterProvider<AccountProvider> {
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<AccountProvider>,
|
||||
val canSearchForAccountProviders: Boolean,
|
||||
val changeServerState: ChangeServerState,
|
||||
)
|
||||
|
||||
@@ -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<ChangeAccountProviderState> {
|
||||
override val values: Sequence<ChangeAccountProviderState>
|
||||
get() = sequenceOf(
|
||||
aChangeAccountProviderState(),
|
||||
aChangeAccountProviderState(canSearchForAccountProviders = false),
|
||||
// Add other state here
|
||||
)
|
||||
}
|
||||
|
||||
fun aChangeAccountProviderState() = ChangeAccountProviderState(
|
||||
accountProviders = listOf(
|
||||
fun aChangeAccountProviderState(
|
||||
accountProviders: List<AccountProvider> = listOf(
|
||||
anAccountProvider()
|
||||
),
|
||||
changeServerState = aChangeServerState(),
|
||||
canSearchForAccountProviders: Boolean = true,
|
||||
changeServerState: ChangeServerState = aChangeServerState(),
|
||||
) = ChangeAccountProviderState(
|
||||
accountProviders = accountProviders,
|
||||
canSearchForAccountProviders = canSearchForAccountProviders,
|
||||
changeServerState = changeServerState,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<Plugin>,
|
||||
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<Callback>().forEach { it.onOidcDetails(oidcDetails) }
|
||||
}
|
||||
|
||||
private fun onLoginPasswordNeeded() {
|
||||
plugins<Callback>().forEach { it.onLoginPasswordNeeded() }
|
||||
}
|
||||
|
||||
private fun onCreateAccountContinue(url: String) {
|
||||
plugins<Callback>().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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<ChooseAccountProviderState> {
|
||||
@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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<AccountProvider>,
|
||||
val selectedAccountProvider: AccountProvider?,
|
||||
val loginMode: AsyncData<LoginMode>,
|
||||
val eventSink: (ChooseAccountProviderEvents) -> Unit,
|
||||
) {
|
||||
val submitEnabled: Boolean
|
||||
get() = selectedAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading)
|
||||
}
|
||||
@@ -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<ChooseAccountProviderState> {
|
||||
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<ChooseAccountProviderState>
|
||||
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<AccountProvider> = listOf(
|
||||
anAccountProvider()
|
||||
),
|
||||
selectedAccountProvider: AccountProvider? = null,
|
||||
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
||||
eventSink: (ChooseAccountProviderEvents) -> Unit = {},
|
||||
) = ChooseAccountProviderState(
|
||||
accountProviders = accountProviders,
|
||||
selectedAccountProvider = selectedAccountProvider,
|
||||
loginMode = loginMode,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
@@ -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 = { },
|
||||
)
|
||||
}
|
||||
@@ -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<Callback>().forEach { it.onSignIn() }
|
||||
private fun onSignIn(mustChooseAccountProvider: Boolean) {
|
||||
plugins<Callback>().forEach { it.onSignIn(mustChooseAccountProvider) }
|
||||
}
|
||||
|
||||
private fun onSignUp() {
|
||||
|
||||
@@ -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<OnBoardingState> {
|
||||
@@ -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<String?>(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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -26,6 +26,7 @@ open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
|
||||
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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -89,5 +89,6 @@ Try signing in manually, or scan the QR code with another device."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix is an open network for secure, decentralised communication."</string>
|
||||
<string name="screen_server_confirmation_message_register">"This is where your conversations will live — just like you would use an email provider to keep your emails."</string>
|
||||
<string name="screen_server_confirmation_title_login">"You’re about to sign in to %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_picker_mode">"Choose account provider"</string>
|
||||
<string name="screen_server_confirmation_title_register">"You’re about to create an account on %1$s"</string>
|
||||
</resources>
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back invokes the expected callback`() {
|
||||
val eventSink = EventsRecorder<ChooseAccountProviderEvents>(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<ChooseAccountProviderEvents>()
|
||||
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<ChooseAccountProviderEvents>()
|
||||
rule.setChooseAccountProviderView(
|
||||
state = aChooseAccountProviderState(
|
||||
loginMode = AsyncData.Failure(AN_EXCEPTION),
|
||||
eventSink = eventSink,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
eventSink.assertSingle(ChooseAccountProviderEvents.ClearError)
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setOnboardingView(
|
||||
state: OnBoardingState,
|
||||
onSignInWithQrCode: () -> Unit = EnsureNeverCalled(),
|
||||
onSignIn: () -> Unit = EnsureNeverCalled(),
|
||||
onSignIn: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onCreateAccount: () -> Unit = EnsureNeverCalled(),
|
||||
onReportProblem: () -> Unit = EnsureNeverCalled(),
|
||||
onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(),
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user