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:
parent
0e5fc8f634
commit
b926f5f5dc
46 changed files with 1164 additions and 64 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4b4adf1bb086cb46f6a9cc20dffe23cba656a571a4f6651a82063907e82a117e
|
||||
size 5843
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6d7ed9363bd7b6f09be20f58019260dceb36c8cbebf1c74474b94e227bebb37d
|
||||
size 5926
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6d742af6422e7f5ce0070831e921180006a5b48d52c042fdc52030435597f242
|
||||
size 5835
|
||||
oid sha256:ef744b07b6e40d1017713b3e44c2e6d358495ea9e1ff88c183c963994e216738
|
||||
size 5554
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b4731443df36855c8785072a8085fc9968ec231ff46ff24012b0ae72cd084e46
|
||||
size 5867
|
||||
oid sha256:04a8e2e856805b0888f901278537a217e233445b9f09060e1f4b764db87146c8
|
||||
size 5589
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c5d38292c3927910a4ac9df5d2ad63dddf41818611096db2ba4a2101b880c8c0
|
||||
size 47316
|
||||
oid sha256:4406e5c57eea02b3ac315b303e0995e6ad0ae1b25f751cb8a154220e08a2bf23
|
||||
size 47284
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bc85189c4ebe4539c02294c5fa98d41816321848de5d68718de2dffef010719e
|
||||
size 45074
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4a954fc1d3091b50581dcb4cdd9ca89acb0c9c92e35fc9ffd6bfe240685d37b0
|
||||
size 46333
|
||||
oid sha256:35820145f9f9fe836035774390808d5778cdca064730a3b4ebe62f66a26fafae
|
||||
size 46350
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9a3e19f270562bbd81c9508e90319b819c08ecc766f5d08adc657aa2781ad679
|
||||
size 44112
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4051b09fbcb571273ff3edf21d482fa75684b73846a4992aa495274dbc8fcb97
|
||||
size 22602
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d2f1145b53dd1b031d3fb213217fd5128b5dae6f5882f0af5f339973f61d8748
|
||||
size 23556
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b8dc7e4a04653998514fb3ca02bd220fdb0979c5ce8251031cf90e51f6a7265c
|
||||
size 24082
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cf820f7343125063ff3e92f3d4902e427a424d148760168884cd143628925010
|
||||
size 22111
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9bd58639caaa55c921244a15b0f4c1d3997b4c591cab4ad46ddcfc07d7a4fdc1
|
||||
size 23010
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c9dfd90cd62e5511075857c5642660228b553208bda00cf07d51db087b8b37a9
|
||||
size 23508
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c5ac88572a7fbcacbf571b86586a887ec4931c9e92c15f2ac3633e0adc4088e4
|
||||
size 51364
|
||||
oid sha256:cb99a065e901322c590b5331880e8035b6ab6101d4e2f4603ceb3abce2344147
|
||||
size 50804
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:02f1214c9ced09fcb178d04ec0e8683ecb1f31c169056db9a3849e6680d060fe
|
||||
size 50318
|
||||
oid sha256:ecc79b3e57881971e5bfc7d8e2937b4e9ccb18536851be3fc79154b544aa6895
|
||||
size 49800
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue