Skip to content

Commit b926f5f

Browse files
bmartyElementBot
andauthored
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 <[email protected]>
1 parent 0e5fc8f commit b926f5f

File tree

46 files changed

+1164
-64
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1164
-64
lines changed

features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,14 @@ interface EnterpriseService {
2121

2222
fun firebasePushGateway(): String?
2323
fun unifiedPushDefaultPushGateway(): String?
24+
25+
companion object {
26+
const val ANY_ACCOUNT_PROVIDER = "*"
27+
}
28+
}
29+
30+
fun EnterpriseService.canConnectToAnyHomeserver(): Boolean {
31+
return defaultHomeserverList().let {
32+
it.isEmpty() || it.contains(EnterpriseService.ANY_ACCOUNT_PROVIDER)
33+
}
2434
}

features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import io.element.android.features.login.api.LoginEntryPoint
3030
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
3131
import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode
3232
import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode
33+
import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderNode
3334
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode
3435
import io.element.android.features.login.impl.screens.createaccount.CreateAccountNode
3536
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
@@ -107,6 +108,9 @@ class LoginFlowNode @AssistedInject constructor(
107108
val isAccountCreation: Boolean,
108109
) : NavTarget
109110

111+
@Parcelize
112+
data object ChooseAccountProvider : NavTarget
113+
110114
@Parcelize
111115
data object ChangeAccountProvider : NavTarget
112116

@@ -133,9 +137,13 @@ class LoginFlowNode @AssistedInject constructor(
133137
)
134138
}
135139

136-
override fun onSignIn() {
140+
override fun onSignIn(mustChooseAccountProvider: Boolean) {
137141
backstack.push(
138-
NavTarget.ConfirmAccountProvider(isAccountCreation = false)
142+
if (mustChooseAccountProvider) {
143+
NavTarget.ChooseAccountProvider
144+
} else {
145+
NavTarget.ConfirmAccountProvider(isAccountCreation = false)
146+
}
139147
)
140148
}
141149

@@ -166,6 +174,22 @@ class LoginFlowNode @AssistedInject constructor(
166174
)
167175
createNode<OnBoardingNode>(buildContext, listOf(callback, inputs))
168176
}
177+
NavTarget.ChooseAccountProvider -> {
178+
val callback = object : ChooseAccountProviderNode.Callback {
179+
override fun onOidcDetails(oidcDetails: OidcDetails) {
180+
navigateToMas(oidcDetails)
181+
}
182+
183+
override fun onCreateAccountContinue(url: String) {
184+
backstack.push(NavTarget.CreateAccount(url))
185+
}
186+
187+
override fun onLoginPasswordNeeded() {
188+
backstack.push(NavTarget.LoginPassword)
189+
}
190+
}
191+
createNode<ChooseAccountProviderNode>(buildContext, listOf(callback))
192+
}
169193
NavTarget.QrCode -> {
170194
createNode<QrCodeLoginFlowNode>(buildContext)
171195
}

features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,16 @@ import javax.inject.Inject
2020
class AccountProviderDataSource @Inject constructor(
2121
enterpriseService: EnterpriseService,
2222
) {
23-
private val defaultAccountProvider = (enterpriseService.defaultHomeserverList().firstOrNull() ?: AuthenticationConfig.MATRIX_ORG_URL)
24-
.let { url ->
25-
AccountProvider(
26-
url = url,
27-
subtitle = null,
28-
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
29-
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
30-
)
31-
}
23+
private val defaultAccountProvider =
24+
(enterpriseService.defaultHomeserverList().firstOrNull { it != EnterpriseService.ANY_ACCOUNT_PROVIDER } ?: AuthenticationConfig.MATRIX_ORG_URL)
25+
.let { url ->
26+
AccountProvider(
27+
url = url,
28+
subtitle = null,
29+
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
30+
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
31+
)
32+
}
3233

3334
private val accountProvider: MutableStateFlow<AccountProvider> = MutableStateFlow(
3435
defaultAccountProvider
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.login.impl.accountprovider
9+
10+
import androidx.compose.foundation.clickable
11+
import androidx.compose.foundation.layout.Column
12+
import androidx.compose.foundation.layout.Row
13+
import androidx.compose.foundation.layout.fillMaxWidth
14+
import androidx.compose.foundation.layout.heightIn
15+
import androidx.compose.foundation.layout.padding
16+
import androidx.compose.runtime.Composable
17+
import androidx.compose.ui.Alignment
18+
import androidx.compose.ui.Modifier
19+
import androidx.compose.ui.res.stringResource
20+
import androidx.compose.ui.unit.dp
21+
import io.element.android.compound.theme.ElementTheme
22+
import io.element.android.compound.tokens.generated.CompoundIcons
23+
import io.element.android.features.login.impl.R
24+
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
25+
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
26+
import io.element.android.libraries.designsystem.preview.ElementPreview
27+
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
28+
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
29+
import io.element.android.libraries.designsystem.theme.components.Text
30+
31+
/**
32+
* https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817
33+
*/
34+
@Composable
35+
fun AccountProviderOtherView(
36+
onClick: () -> Unit,
37+
modifier: Modifier = Modifier,
38+
) {
39+
Column(
40+
modifier = modifier
41+
.fillMaxWidth()
42+
.clickable { onClick() }
43+
) {
44+
HorizontalDivider()
45+
Row(
46+
modifier = Modifier
47+
.fillMaxWidth()
48+
.heightIn(min = 44.dp)
49+
.padding(vertical = 4.dp, horizontal = 16.dp),
50+
verticalAlignment = Alignment.CenterVertically
51+
) {
52+
RoundedIconAtom(
53+
size = RoundedIconAtomSize.Medium,
54+
imageVector = CompoundIcons.Search(),
55+
tint = ElementTheme.colors.iconPrimary,
56+
)
57+
Text(
58+
modifier = Modifier
59+
.padding(start = 16.dp)
60+
.weight(1f),
61+
text = stringResource(R.string.screen_change_account_provider_other),
62+
style = ElementTheme.typography.fontBodyLgMedium,
63+
color = ElementTheme.colors.textPrimary,
64+
)
65+
}
66+
}
67+
}
68+
69+
@PreviewsDayNight
70+
@Composable
71+
internal fun AccountProviderOtherViewPreview() = ElementPreview {
72+
AccountProviderOtherView(
73+
onClick = { },
74+
)
75+
}

features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ open class AccountProviderProvider : PreviewParameterProvider<AccountProvider> {
2323

2424
fun anAccountProvider(
2525
url: String = AuthenticationConfig.MATRIX_ORG_URL,
26+
subtitle: String? = "Matrix.org is an open network for secure, decentralized communication.",
27+
isPublic: Boolean = true,
28+
isMatrixOrg: Boolean = true,
29+
isValid: Boolean = true,
2630
) = AccountProvider(
2731
url = url,
28-
subtitle = "Matrix.org is an open network for secure, decentralized communication.",
29-
isPublic = true,
30-
isMatrixOrg = true,
31-
isValid = true,
32+
subtitle = subtitle,
33+
isPublic = isPublic,
34+
isMatrixOrg = isMatrixOrg,
35+
isValid = isValid,
3236
)

features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ fun AccountProviderView(
3939
item: AccountProvider,
4040
onClick: () -> Unit,
4141
modifier: Modifier = Modifier,
42+
selected: Boolean = false,
4243
) {
4344
Column(
4445
modifier = modifier
@@ -66,7 +67,7 @@ fun AccountProviderView(
6667
} else {
6768
RoundedIconAtom(
6869
size = RoundedIconAtomSize.Medium,
69-
imageVector = CompoundIcons.Search(),
70+
imageVector = CompoundIcons.Host(),
7071
tint = ElementTheme.colors.iconPrimary,
7172
)
7273
}
@@ -88,6 +89,15 @@ fun AccountProviderView(
8889
tint = ElementTheme.colors.iconSecondary,
8990
)
9091
}
92+
if (selected) {
93+
Icon(
94+
modifier = Modifier
95+
.padding(start = 10.dp),
96+
imageVector = CompoundIcons.Check(),
97+
contentDescription = null,
98+
tint = ElementTheme.colors.iconAccentPrimary,
99+
)
100+
}
91101
}
92102
if (item.subtitle != null) {
93103
Text(

features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.runtime.State
1414
import androidx.compose.runtime.mutableStateOf
1515
import io.element.android.features.login.impl.DefaultLoginUserStory
1616
import io.element.android.features.login.impl.error.ChangeServerError
17+
import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter
1718
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter
1819
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
1920
import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter
@@ -31,7 +32,8 @@ import javax.inject.Inject
3132
/**
3233
* This class is responsible for managing the login flow, including handling OIDC actions and
3334
* submitting login requests.
34-
* It's an helper to avoid code duplication. It is used by [OnBoardingPresenter] and [ConfirmAccountProviderPresenter].
35+
* It's an helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter]
36+
* and [ChooseAccountProviderPresenter].
3537
*/
3638
class LoginHelper @Inject constructor(
3739
private val oidcActionFlow: OidcActionFlow,

features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
1111
import androidx.compose.runtime.remember
1212
import io.element.android.appconfig.AuthenticationConfig
1313
import io.element.android.features.enterprise.api.EnterpriseService
14+
import io.element.android.features.enterprise.api.canConnectToAnyHomeserver
1415
import io.element.android.features.login.impl.accountprovider.AccountProvider
1516
import io.element.android.features.login.impl.changeserver.ChangeServerState
1617
import io.element.android.libraries.architecture.Presenter
@@ -25,6 +26,7 @@ class ChangeAccountProviderPresenter @Inject constructor(
2526
override fun present(): ChangeAccountProviderState {
2627
val staticAccountProviderList = remember {
2728
enterpriseService.defaultHomeserverList()
29+
.filter { it != EnterpriseService.ANY_ACCOUNT_PROVIDER }
2830
.map { it.ensureProtocol() }
2931
.ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
3032
.map { url ->
@@ -38,9 +40,14 @@ class ChangeAccountProviderPresenter @Inject constructor(
3840
}
3941
}
4042

43+
val canSearchForAccountProviders = remember {
44+
enterpriseService.canConnectToAnyHomeserver()
45+
}
46+
4147
val changeServerState = changeServerPresenter.present()
4248
return ChangeAccountProviderState(
4349
accountProviders = staticAccountProviderList,
50+
canSearchForAccountProviders = canSearchForAccountProviders,
4451
changeServerState = changeServerState,
4552
)
4653
}

features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ import io.element.android.features.login.impl.changeserver.ChangeServerState
1313
// Do not use default value, so no member get forgotten in the presenters.
1414
data class ChangeAccountProviderState(
1515
val accountProviders: List<AccountProvider>,
16+
val canSearchForAccountProviders: Boolean,
1617
val changeServerState: ChangeServerState,
1718
)

features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,28 @@
88
package io.element.android.features.login.impl.screens.changeaccountprovider
99

1010
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
11+
import io.element.android.features.login.impl.accountprovider.AccountProvider
1112
import io.element.android.features.login.impl.accountprovider.anAccountProvider
13+
import io.element.android.features.login.impl.changeserver.ChangeServerState
1214
import io.element.android.features.login.impl.changeserver.aChangeServerState
1315

1416
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
1517
override val values: Sequence<ChangeAccountProviderState>
1618
get() = sequenceOf(
1719
aChangeAccountProviderState(),
20+
aChangeAccountProviderState(canSearchForAccountProviders = false),
1821
// Add other state here
1922
)
2023
}
2124

22-
fun aChangeAccountProviderState() = ChangeAccountProviderState(
23-
accountProviders = listOf(
25+
fun aChangeAccountProviderState(
26+
accountProviders: List<AccountProvider> = listOf(
2427
anAccountProvider()
2528
),
26-
changeServerState = aChangeServerState(),
29+
canSearchForAccountProviders: Boolean = true,
30+
changeServerState: ChangeServerState = aChangeServerState(),
31+
) = ChangeAccountProviderState(
32+
accountProviders = accountProviders,
33+
canSearchForAccountProviders = canSearchForAccountProviders,
34+
changeServerState = changeServerState,
2735
)

features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
2727
import androidx.compose.ui.unit.dp
2828
import io.element.android.compound.tokens.generated.CompoundIcons
2929
import io.element.android.features.login.impl.R
30-
import io.element.android.features.login.impl.accountprovider.AccountProvider
30+
import io.element.android.features.login.impl.accountprovider.AccountProviderOtherView
3131
import io.element.android.features.login.impl.accountprovider.AccountProviderView
3232
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
3333
import io.element.android.features.login.impl.changeserver.ChangeServerView
@@ -95,13 +95,11 @@ fun ChangeAccountProviderView(
9595
)
9696
}
9797
// Other
98-
AccountProviderView(
99-
item = AccountProvider(
100-
url = "",
101-
title = stringResource(id = R.string.screen_change_account_provider_other),
102-
),
103-
onClick = onOtherProviderClick
104-
)
98+
if (state.canSearchForAccountProviders) {
99+
AccountProviderOtherView(
100+
onClick = onOtherProviderClick
101+
)
102+
}
105103
Spacer(Modifier.height(32.dp))
106104
}
107105
ChangeServerView(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.login.impl.screens.chooseaccountprovider
9+
10+
import io.element.android.features.login.impl.accountprovider.AccountProvider
11+
12+
sealed interface ChooseAccountProviderEvents {
13+
data class SelectAccountProvider(val accountProvider: AccountProvider) : ChooseAccountProviderEvents
14+
data object Continue : ChooseAccountProviderEvents
15+
data object ClearError : ChooseAccountProviderEvents
16+
}

0 commit comments

Comments
 (0)