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
0d5b8709b8
commit
dbe75cd4ea
46 changed files with 1164 additions and 64 deletions
|
|
@ -21,4 +21,14 @@ interface EnterpriseService {
|
||||||
|
|
||||||
fun firebasePushGateway(): String?
|
fun firebasePushGateway(): String?
|
||||||
fun unifiedPushDefaultPushGateway(): 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.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode
|
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.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.confirmaccountprovider.ConfirmAccountProviderNode
|
||||||
import io.element.android.features.login.impl.screens.createaccount.CreateAccountNode
|
import io.element.android.features.login.impl.screens.createaccount.CreateAccountNode
|
||||||
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
|
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
|
||||||
|
|
@ -107,6 +108,9 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
val isAccountCreation: Boolean,
|
val isAccountCreation: Boolean,
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object ChooseAccountProvider : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object ChangeAccountProvider : NavTarget
|
data object ChangeAccountProvider : NavTarget
|
||||||
|
|
||||||
|
|
@ -133,9 +137,13 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSignIn() {
|
override fun onSignIn(mustChooseAccountProvider: Boolean) {
|
||||||
backstack.push(
|
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))
|
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 -> {
|
NavTarget.QrCode -> {
|
||||||
createNode<QrCodeLoginFlowNode>(buildContext)
|
createNode<QrCodeLoginFlowNode>(buildContext)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,16 @@ import javax.inject.Inject
|
||||||
class AccountProviderDataSource @Inject constructor(
|
class AccountProviderDataSource @Inject constructor(
|
||||||
enterpriseService: EnterpriseService,
|
enterpriseService: EnterpriseService,
|
||||||
) {
|
) {
|
||||||
private val defaultAccountProvider = (enterpriseService.defaultHomeserverList().firstOrNull() ?: AuthenticationConfig.MATRIX_ORG_URL)
|
private val defaultAccountProvider =
|
||||||
.let { url ->
|
(enterpriseService.defaultHomeserverList().firstOrNull { it != EnterpriseService.ANY_ACCOUNT_PROVIDER } ?: AuthenticationConfig.MATRIX_ORG_URL)
|
||||||
AccountProvider(
|
.let { url ->
|
||||||
url = url,
|
AccountProvider(
|
||||||
subtitle = null,
|
url = url,
|
||||||
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
|
subtitle = null,
|
||||||
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
|
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
)
|
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private val accountProvider: MutableStateFlow<AccountProvider> = MutableStateFlow(
|
private val accountProvider: MutableStateFlow<AccountProvider> = MutableStateFlow(
|
||||||
defaultAccountProvider
|
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(
|
fun anAccountProvider(
|
||||||
url: String = AuthenticationConfig.MATRIX_ORG_URL,
|
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(
|
) = AccountProvider(
|
||||||
url = url,
|
url = url,
|
||||||
subtitle = "Matrix.org is an open network for secure, decentralized communication.",
|
subtitle = subtitle,
|
||||||
isPublic = true,
|
isPublic = isPublic,
|
||||||
isMatrixOrg = true,
|
isMatrixOrg = isMatrixOrg,
|
||||||
isValid = true,
|
isValid = isValid,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ fun AccountProviderView(
|
||||||
item: AccountProvider,
|
item: AccountProvider,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
selected: Boolean = false,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|
@ -66,7 +67,7 @@ fun AccountProviderView(
|
||||||
} else {
|
} else {
|
||||||
RoundedIconAtom(
|
RoundedIconAtom(
|
||||||
size = RoundedIconAtomSize.Medium,
|
size = RoundedIconAtomSize.Medium,
|
||||||
imageVector = CompoundIcons.Search(),
|
imageVector = CompoundIcons.Host(),
|
||||||
tint = ElementTheme.colors.iconPrimary,
|
tint = ElementTheme.colors.iconPrimary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -88,6 +89,15 @@ fun AccountProviderView(
|
||||||
tint = ElementTheme.colors.iconSecondary,
|
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) {
|
if (item.subtitle != null) {
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||||
import io.element.android.features.login.impl.error.ChangeServerError
|
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.confirmaccountprovider.ConfirmAccountProviderPresenter
|
||||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||||
import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter
|
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
|
* This class is responsible for managing the login flow, including handling OIDC actions and
|
||||||
* submitting login requests.
|
* 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(
|
class LoginHelper @Inject constructor(
|
||||||
private val oidcActionFlow: OidcActionFlow,
|
private val oidcActionFlow: OidcActionFlow,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import io.element.android.appconfig.AuthenticationConfig
|
import io.element.android.appconfig.AuthenticationConfig
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
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.accountprovider.AccountProvider
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerState
|
import io.element.android.features.login.impl.changeserver.ChangeServerState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -25,6 +26,7 @@ class ChangeAccountProviderPresenter @Inject constructor(
|
||||||
override fun present(): ChangeAccountProviderState {
|
override fun present(): ChangeAccountProviderState {
|
||||||
val staticAccountProviderList = remember {
|
val staticAccountProviderList = remember {
|
||||||
enterpriseService.defaultHomeserverList()
|
enterpriseService.defaultHomeserverList()
|
||||||
|
.filter { it != EnterpriseService.ANY_ACCOUNT_PROVIDER }
|
||||||
.map { it.ensureProtocol() }
|
.map { it.ensureProtocol() }
|
||||||
.ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
|
.ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
|
||||||
.map { url ->
|
.map { url ->
|
||||||
|
|
@ -38,9 +40,14 @@ class ChangeAccountProviderPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val canSearchForAccountProviders = remember {
|
||||||
|
enterpriseService.canConnectToAnyHomeserver()
|
||||||
|
}
|
||||||
|
|
||||||
val changeServerState = changeServerPresenter.present()
|
val changeServerState = changeServerPresenter.present()
|
||||||
return ChangeAccountProviderState(
|
return ChangeAccountProviderState(
|
||||||
accountProviders = staticAccountProviderList,
|
accountProviders = staticAccountProviderList,
|
||||||
|
canSearchForAccountProviders = canSearchForAccountProviders,
|
||||||
changeServerState = changeServerState,
|
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.
|
// Do not use default value, so no member get forgotten in the presenters.
|
||||||
data class ChangeAccountProviderState(
|
data class ChangeAccountProviderState(
|
||||||
val accountProviders: List<AccountProvider>,
|
val accountProviders: List<AccountProvider>,
|
||||||
|
val canSearchForAccountProviders: Boolean,
|
||||||
val changeServerState: ChangeServerState,
|
val changeServerState: ChangeServerState,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,28 @@
|
||||||
package io.element.android.features.login.impl.screens.changeaccountprovider
|
package io.element.android.features.login.impl.screens.changeaccountprovider
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
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.accountprovider.anAccountProvider
|
||||||
|
import io.element.android.features.login.impl.changeserver.ChangeServerState
|
||||||
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
||||||
|
|
||||||
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
|
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
|
||||||
override val values: Sequence<ChangeAccountProviderState>
|
override val values: Sequence<ChangeAccountProviderState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aChangeAccountProviderState(),
|
aChangeAccountProviderState(),
|
||||||
|
aChangeAccountProviderState(canSearchForAccountProviders = false),
|
||||||
// Add other state here
|
// Add other state here
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aChangeAccountProviderState() = ChangeAccountProviderState(
|
fun aChangeAccountProviderState(
|
||||||
accountProviders = listOf(
|
accountProviders: List<AccountProvider> = listOf(
|
||||||
anAccountProvider()
|
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 androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.login.impl.R
|
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.accountprovider.AccountProviderView
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
|
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerView
|
import io.element.android.features.login.impl.changeserver.ChangeServerView
|
||||||
|
|
@ -95,13 +95,11 @@ fun ChangeAccountProviderView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Other
|
// Other
|
||||||
AccountProviderView(
|
if (state.canSearchForAccountProviders) {
|
||||||
item = AccountProvider(
|
AccountProviderOtherView(
|
||||||
url = "",
|
onClick = onOtherProviderClick
|
||||||
title = stringResource(id = R.string.screen_change_account_provider_other),
|
)
|
||||||
),
|
}
|
||||||
onClick = onOtherProviderClick
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(32.dp))
|
Spacer(Modifier.height(32.dp))
|
||||||
}
|
}
|
||||||
ChangeServerView(
|
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 {
|
interface Callback : Plugin {
|
||||||
fun onSignUp()
|
fun onSignUp()
|
||||||
fun onSignIn()
|
fun onSignIn(mustChooseAccountProvider: Boolean)
|
||||||
fun onSignInWithQrCode()
|
fun onSignInWithQrCode()
|
||||||
fun onReportProblem()
|
fun onReportProblem()
|
||||||
fun onLoginPasswordNeeded()
|
fun onLoginPasswordNeeded()
|
||||||
|
|
@ -53,8 +53,8 @@ class OnBoardingNode @AssistedInject constructor(
|
||||||
params = params,
|
params = params,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun onSignIn() {
|
private fun onSignIn(mustChooseAccountProvider: Boolean) {
|
||||||
plugins<Callback>().forEach { it.onSignIn() }
|
plugins<Callback>().forEach { it.onSignIn(mustChooseAccountProvider) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSignUp() {
|
private fun onSignUp() {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.appconfig.OnBoardingConfig
|
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.login.impl.login.LoginHelper
|
||||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -27,6 +29,7 @@ class OnBoardingPresenter @AssistedInject constructor(
|
||||||
@Assisted private val params: OnBoardingNode.Params,
|
@Assisted private val params: OnBoardingNode.Params,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val enterpriseService: EnterpriseService,
|
||||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||||
private val loginHelper: LoginHelper,
|
private val loginHelper: LoginHelper,
|
||||||
) : Presenter<OnBoardingState> {
|
) : Presenter<OnBoardingState> {
|
||||||
|
|
@ -37,15 +40,33 @@ class OnBoardingPresenter @AssistedInject constructor(
|
||||||
): OnBoardingPresenter
|
): OnBoardingPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
private val defaultAccountProvider = params.accountProvider
|
|
||||||
private val loginHint = params.loginHint
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): OnBoardingState {
|
override fun present(): OnBoardingState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
val forcedAccountProvider = remember {
|
||||||
val canLoginWithQrCode by produceState(initialValue = false) {
|
// If defaultHomeserverList() returns a singleton list, this is the default account provider.
|
||||||
value = defaultAccountProvider == null &&
|
// 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)
|
featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin)
|
||||||
}
|
}
|
||||||
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
|
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
|
||||||
|
|
@ -58,7 +79,7 @@ class OnBoardingPresenter @AssistedInject constructor(
|
||||||
coroutineScope = localCoroutineScope,
|
coroutineScope = localCoroutineScope,
|
||||||
isAccountCreation = false,
|
isAccountCreation = false,
|
||||||
homeserverUrl = event.defaultAccountProvider,
|
homeserverUrl = event.defaultAccountProvider,
|
||||||
loginHint = loginHint,
|
loginHint = params.loginHint?.takeIf { forcedAccountProvider == null },
|
||||||
)
|
)
|
||||||
OnBoardingEvents.ClearError -> loginHelper.clearError()
|
OnBoardingEvents.ClearError -> loginHelper.clearError()
|
||||||
}
|
}
|
||||||
|
|
@ -67,8 +88,9 @@ class OnBoardingPresenter @AssistedInject constructor(
|
||||||
return OnBoardingState(
|
return OnBoardingState(
|
||||||
productionApplicationName = buildMeta.productionApplicationName,
|
productionApplicationName = buildMeta.productionApplicationName,
|
||||||
defaultAccountProvider = defaultAccountProvider,
|
defaultAccountProvider = defaultAccountProvider,
|
||||||
|
mustChooseAccountProvider = mustChooseAccountProvider,
|
||||||
canLoginWithQrCode = canLoginWithQrCode,
|
canLoginWithQrCode = canLoginWithQrCode,
|
||||||
canCreateAccount = defaultAccountProvider == null && OnBoardingConfig.CAN_CREATE_ACCOUNT,
|
canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT,
|
||||||
canReportBug = canReportBug,
|
canReportBug = canReportBug,
|
||||||
loginMode = loginMode,
|
loginMode = loginMode,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import io.element.android.libraries.architecture.AsyncData
|
||||||
data class OnBoardingState(
|
data class OnBoardingState(
|
||||||
val productionApplicationName: String,
|
val productionApplicationName: String,
|
||||||
val defaultAccountProvider: String?,
|
val defaultAccountProvider: String?,
|
||||||
|
val mustChooseAccountProvider: Boolean,
|
||||||
val canLoginWithQrCode: Boolean,
|
val canLoginWithQrCode: Boolean,
|
||||||
val canCreateAccount: Boolean,
|
val canCreateAccount: Boolean,
|
||||||
val canReportBug: Boolean,
|
val canReportBug: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
|
||||||
fun anOnBoardingState(
|
fun anOnBoardingState(
|
||||||
productionApplicationName: String = "Element",
|
productionApplicationName: String = "Element",
|
||||||
defaultAccountProvider: String? = null,
|
defaultAccountProvider: String? = null,
|
||||||
|
mustChooseAccountProvider: Boolean = false,
|
||||||
canLoginWithQrCode: Boolean = false,
|
canLoginWithQrCode: Boolean = false,
|
||||||
canCreateAccount: Boolean = false,
|
canCreateAccount: Boolean = false,
|
||||||
canReportBug: Boolean = false,
|
canReportBug: Boolean = false,
|
||||||
|
|
@ -34,6 +35,7 @@ fun anOnBoardingState(
|
||||||
) = OnBoardingState(
|
) = OnBoardingState(
|
||||||
productionApplicationName = productionApplicationName,
|
productionApplicationName = productionApplicationName,
|
||||||
defaultAccountProvider = defaultAccountProvider,
|
defaultAccountProvider = defaultAccountProvider,
|
||||||
|
mustChooseAccountProvider = mustChooseAccountProvider,
|
||||||
canLoginWithQrCode = canLoginWithQrCode,
|
canLoginWithQrCode = canLoginWithQrCode,
|
||||||
canCreateAccount = canCreateAccount,
|
canCreateAccount = canCreateAccount,
|
||||||
canReportBug = canReportBug,
|
canReportBug = canReportBug,
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
fun OnBoardingView(
|
fun OnBoardingView(
|
||||||
state: OnBoardingState,
|
state: OnBoardingState,
|
||||||
onSignInWithQrCode: () -> Unit,
|
onSignInWithQrCode: () -> Unit,
|
||||||
onSignIn: () -> Unit,
|
onSignIn: (mustChooseAccountProvider: Boolean) -> Unit,
|
||||||
onCreateAccount: () -> Unit,
|
onCreateAccount: () -> Unit,
|
||||||
onOidcDetails: (OidcDetails) -> Unit,
|
onOidcDetails: (OidcDetails) -> Unit,
|
||||||
onNeedLoginPassword: () -> Unit,
|
onNeedLoginPassword: () -> Unit,
|
||||||
|
|
@ -143,7 +143,7 @@ private fun OnBoardingContent(state: OnBoardingState) {
|
||||||
private fun OnBoardingButtons(
|
private fun OnBoardingButtons(
|
||||||
state: OnBoardingState,
|
state: OnBoardingState,
|
||||||
onSignInWithQrCode: () -> Unit,
|
onSignInWithQrCode: () -> Unit,
|
||||||
onSignIn: () -> Unit,
|
onSignIn: (mustChooseAccountProvider: Boolean) -> Unit,
|
||||||
onCreateAccount: () -> Unit,
|
onCreateAccount: () -> Unit,
|
||||||
onReportProblem: () -> Unit,
|
onReportProblem: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -171,7 +171,9 @@ private fun OnBoardingButtons(
|
||||||
if (defaultAccountProvider == null) {
|
if (defaultAccountProvider == null) {
|
||||||
Button(
|
Button(
|
||||||
text = stringResource(id = signInButtonStringRes),
|
text = stringResource(id = signInButtonStringRes),
|
||||||
onClick = onSignIn,
|
onClick = {
|
||||||
|
onSignIn(state.mustChooseAccountProvider)
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.testTag(TestTags.onBoardingSignIn)
|
.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_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_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_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>
|
<string name="screen_server_confirmation_title_register">"You’re about to create an account on %1$s"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ package io.element.android.features.login.impl.accountprovider
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.appconfig.AuthenticationConfig
|
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.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.coroutines.test.runTest
|
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
|
@Test
|
||||||
fun `present - user change and reset`() = runTest {
|
fun `present - user change and reset`() = runTest {
|
||||||
val sut = AccountProviderDataSource(FakeEnterpriseService())
|
val sut = AccountProviderDataSource(FakeEnterpriseService())
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,12 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
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 io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -27,7 +30,9 @@ class ChangeAccountProviderPresenterTest {
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = ChangeAccountProviderPresenter(
|
val presenter = ChangeAccountProviderPresenter(
|
||||||
changeServerPresenter = { aChangeServerState() },
|
changeServerPresenter = { aChangeServerState() },
|
||||||
enterpriseService = FakeEnterpriseService(),
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
defaultHomeserverListResult = { emptyList() }
|
||||||
|
),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
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 com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.appconfig.OnBoardingConfig
|
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.DefaultLoginUserStory
|
||||||
import io.element.android.features.login.impl.login.LoginHelper
|
import io.element.android.features.login.impl.login.LoginHelper
|
||||||
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
|
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.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
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_HOMESERVER_URL
|
||||||
import io.element.android.libraries.matrix.test.A_LOGIN_HINT
|
import io.element.android.libraries.matrix.test.A_LOGIN_HINT
|
||||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
|
|
@ -36,6 +41,23 @@ class OnBoardingPresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
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
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val buildMeta = aBuildMeta(
|
val buildMeta = aBuildMeta(
|
||||||
|
|
@ -50,10 +72,14 @@ class OnBoardingPresenterTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) },
|
||||||
|
),
|
||||||
rageshakeFeatureAvailability = { true },
|
rageshakeFeatureAvailability = { true },
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.defaultAccountProvider).isNull()
|
||||||
assertThat(initialState.canLoginWithQrCode).isFalse()
|
assertThat(initialState.canLoginWithQrCode).isFalse()
|
||||||
assertThat(initialState.productionApplicationName).isEqualTo("B")
|
assertThat(initialState.productionApplicationName).isEqualTo("B")
|
||||||
assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT)
|
assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT)
|
||||||
|
|
@ -74,22 +100,79 @@ class OnBoardingPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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(
|
val presenter = createPresenter(
|
||||||
params = OnBoardingNode.Params(
|
params = OnBoardingNode.Params(
|
||||||
accountProvider = A_HOMESERVER_URL,
|
accountProvider = ACCOUNT_PROVIDER_FROM_LINK,
|
||||||
loginHint = null,
|
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 {
|
presenter.test {
|
||||||
|
skipItems(3)
|
||||||
awaitItem().also {
|
awaitItem().also {
|
||||||
assertThat(it.defaultAccountProvider).isEqualTo(A_HOMESERVER_URL)
|
assertThat(it.defaultAccountProvider).isEqualTo(ACCOUNT_PROVIDER_FROM_LINK)
|
||||||
assertThat(it.canLoginWithQrCode).isFalse()
|
assertThat(it.canLoginWithQrCode).isFalse()
|
||||||
assertThat(it.canCreateAccount).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
|
@Test
|
||||||
fun `present - default account provider - login and clear error`() = runTest {
|
fun `present - default account provider - login and clear error`() = runTest {
|
||||||
val authenticationService = FakeMatrixAuthenticationService()
|
val authenticationService = FakeMatrixAuthenticationService()
|
||||||
|
|
@ -98,11 +181,15 @@ class OnBoardingPresenterTest {
|
||||||
accountProvider = A_HOMESERVER_URL,
|
accountProvider = A_HOMESERVER_URL,
|
||||||
loginHint = A_LOGIN_HINT,
|
loginHint = A_LOGIN_HINT,
|
||||||
),
|
),
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
isAllowedToConnectToHomeserverResult = { true },
|
||||||
|
),
|
||||||
loginHelper = createLoginHelper(
|
loginHelper = createLoginHelper(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
skipItems(3)
|
||||||
awaitItem().also {
|
awaitItem().also {
|
||||||
assertThat(it.defaultAccountProvider).isEqualTo(A_HOMESERVER_URL)
|
assertThat(it.defaultAccountProvider).isEqualTo(A_HOMESERVER_URL)
|
||||||
authenticationService.givenChangeServerError(A_THROWABLE)
|
authenticationService.givenChangeServerError(A_THROWABLE)
|
||||||
|
|
@ -126,12 +213,14 @@ private fun createPresenter(
|
||||||
params: OnBoardingNode.Params = OnBoardingNode.Params(null, null),
|
params: OnBoardingNode.Params = OnBoardingNode.Params(null, null),
|
||||||
buildMeta: BuildMeta = aBuildMeta(),
|
buildMeta: BuildMeta = aBuildMeta(),
|
||||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||||
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
rageshakeFeatureAvailability: () -> Boolean = { true },
|
rageshakeFeatureAvailability: () -> Boolean = { true },
|
||||||
loginHelper: LoginHelper = createLoginHelper(),
|
loginHelper: LoginHelper = createLoginHelper(),
|
||||||
) = OnBoardingPresenter(
|
) = OnBoardingPresenter(
|
||||||
params = params,
|
params = params,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
enterpriseService = enterpriseService,
|
||||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||||
loginHelper = loginHelper,
|
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.EventsRecorder
|
||||||
import io.element.android.tests.testutils.clickOn
|
import io.element.android.tests.testutils.clickOn
|
||||||
import io.element.android.tests.testutils.ensureCalledOnce
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.rules.TestRule
|
import org.junit.rules.TestRule
|
||||||
|
|
@ -56,10 +57,28 @@ class OnboardingViewTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when can login with QR code - clicking on sign in manually calls the expected callback`() {
|
fun `when can login with QR code - clicking on sign in manually calls the expected callback - can search account provider`() {
|
||||||
ensureCalledOnce { callback ->
|
`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(
|
rule.setOnboardingView(
|
||||||
state = anOnBoardingState(canLoginWithQrCode = true),
|
state = anOnBoardingState(
|
||||||
|
canLoginWithQrCode = true,
|
||||||
|
mustChooseAccountProvider = mustChooseAccountProvider,
|
||||||
|
),
|
||||||
onSignIn = callback,
|
onSignIn = callback,
|
||||||
)
|
)
|
||||||
rule.clickOn(R.string.screen_onboarding_sign_in_manually)
|
rule.clickOn(R.string.screen_onboarding_sign_in_manually)
|
||||||
|
|
@ -67,12 +86,28 @@ class OnboardingViewTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback`() {
|
fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback - can search account provider`() {
|
||||||
ensureCalledOnce { callback ->
|
`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(
|
rule.setOnboardingView(
|
||||||
state = anOnBoardingState(
|
state = anOnBoardingState(
|
||||||
canLoginWithQrCode = false,
|
canLoginWithQrCode = false,
|
||||||
canCreateAccount = false,
|
canCreateAccount = false,
|
||||||
|
mustChooseAccountProvider = mustChooseAccountProvider,
|
||||||
),
|
),
|
||||||
onSignIn = callback,
|
onSignIn = callback,
|
||||||
)
|
)
|
||||||
|
|
@ -137,7 +172,7 @@ class OnboardingViewTest {
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setOnboardingView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setOnboardingView(
|
||||||
state: OnBoardingState,
|
state: OnBoardingState,
|
||||||
onSignInWithQrCode: () -> Unit = EnsureNeverCalled(),
|
onSignInWithQrCode: () -> Unit = EnsureNeverCalled(),
|
||||||
onSignIn: () -> Unit = EnsureNeverCalled(),
|
onSignIn: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onCreateAccount: () -> Unit = EnsureNeverCalled(),
|
onCreateAccount: () -> Unit = EnsureNeverCalled(),
|
||||||
onReportProblem: () -> Unit = EnsureNeverCalled(),
|
onReportProblem: () -> Unit = EnsureNeverCalled(),
|
||||||
onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(),
|
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 = "matrix.org"
|
||||||
const val A_HOMESERVER_URL_2 = "matrix-client.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 = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidcLogin = false)
|
||||||
val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true)
|
val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true)
|
||||||
val A_ROOM_NOTIFICATION_MODE = RoomNotificationMode.MUTE
|
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
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6d742af6422e7f5ce0070831e921180006a5b48d52c042fdc52030435597f242
|
oid sha256:ef744b07b6e40d1017713b3e44c2e6d358495ea9e1ff88c183c963994e216738
|
||||||
size 5835
|
size 5554
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b4731443df36855c8785072a8085fc9968ec231ff46ff24012b0ae72cd084e46
|
oid sha256:04a8e2e856805b0888f901278537a217e233445b9f09060e1f4b764db87146c8
|
||||||
size 5867
|
size 5589
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c5d38292c3927910a4ac9df5d2ad63dddf41818611096db2ba4a2101b880c8c0
|
oid sha256:4406e5c57eea02b3ac315b303e0995e6ad0ae1b25f751cb8a154220e08a2bf23
|
||||||
size 47316
|
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
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4a954fc1d3091b50581dcb4cdd9ca89acb0c9c92e35fc9ffd6bfe240685d37b0
|
oid sha256:35820145f9f9fe836035774390808d5778cdca064730a3b4ebe62f66a26fafae
|
||||||
size 46333
|
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
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c5ac88572a7fbcacbf571b86586a887ec4931c9e92c15f2ac3633e0adc4088e4
|
oid sha256:cb99a065e901322c590b5331880e8035b6ab6101d4e2f4603ceb3abce2344147
|
||||||
size 51364
|
size 50804
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:02f1214c9ced09fcb178d04ec0e8683ecb1f31c169056db9a3849e6680d060fe
|
oid sha256:ecc79b3e57881971e5bfc7d8e2937b4e9ccb18536851be3fc79154b544aa6895
|
||||||
size 50318
|
size 49800
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue