mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Consolidate account provider selection into a single editable screen
This commit is contained in:
-43
@@ -23,7 +23,6 @@ import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
@@ -33,14 +32,12 @@ import io.element.android.features.login.api.LoginEntryPoint
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.classic.ElementClassicConnection
|
||||
import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode
|
||||
import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode
|
||||
import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderNode
|
||||
import io.element.android.features.login.impl.screens.classic.ClassicFlowNode
|
||||
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode
|
||||
import io.element.android.features.login.impl.screens.createaccount.CreateAccountNode
|
||||
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
|
||||
import io.element.android.features.login.impl.screens.onboarding.OnBoardingNode
|
||||
import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode
|
||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
@@ -130,12 +127,6 @@ class LoginFlowNode(
|
||||
@Parcelize
|
||||
data object ChooseAccountProvider : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object ChangeAccountProvider : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object SearchAccountProvider : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class LoginPassword(
|
||||
val initialLogin: String = "",
|
||||
@@ -281,43 +272,9 @@ class LoginFlowNode(
|
||||
override fun navigateToLoginPassword() {
|
||||
backstack.push(NavTarget.LoginPassword())
|
||||
}
|
||||
|
||||
override fun navigateToChangeAccountProvider() {
|
||||
backstack.push(NavTarget.ChangeAccountProvider)
|
||||
}
|
||||
}
|
||||
createNode<ConfirmAccountProviderNode>(buildContext, plugins = listOf(inputs, callback))
|
||||
}
|
||||
NavTarget.ChangeAccountProvider -> {
|
||||
val callback = object : ChangeAccountProviderNode.Callback {
|
||||
override fun onDone() {
|
||||
// Go back to the Account Provider screen
|
||||
val confirmAccountProvider = backstack.elements.value.firstOrNull {
|
||||
it.key.navTarget is NavTarget.ConfirmAccountProvider
|
||||
}?.key?.navTarget ?: NavTarget.ConfirmAccountProvider(isAccountCreation = false)
|
||||
backstack.singleTop(confirmAccountProvider)
|
||||
}
|
||||
|
||||
override fun navigateToSearchAccountProvider() {
|
||||
backstack.push(NavTarget.SearchAccountProvider)
|
||||
}
|
||||
}
|
||||
|
||||
createNode<ChangeAccountProviderNode>(buildContext, plugins = listOf(callback))
|
||||
}
|
||||
NavTarget.SearchAccountProvider -> {
|
||||
val callback = object : SearchAccountProviderNode.Callback {
|
||||
override fun onDone() {
|
||||
// Go back to the Account Provider screen
|
||||
val confirmAccountProvider = backstack.elements.value.firstOrNull {
|
||||
it.key.navTarget is NavTarget.ConfirmAccountProvider
|
||||
}?.key?.navTarget ?: NavTarget.ConfirmAccountProvider(isAccountCreation = false)
|
||||
backstack.singleTop(confirmAccountProvider)
|
||||
}
|
||||
}
|
||||
|
||||
createNode<SearchAccountProviderNode>(buildContext, plugins = listOf(callback))
|
||||
}
|
||||
is NavTarget.LoginPassword -> {
|
||||
val inputs = LoginPasswordNode.Inputs(
|
||||
initialLogin = navTarget.initialLogin,
|
||||
|
||||
+1
@@ -9,6 +9,7 @@
|
||||
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||
|
||||
sealed interface ConfirmAccountProviderEvents {
|
||||
data class UserInputChanged(val accountProvider: String) : ConfirmAccountProviderEvents
|
||||
data object Continue : ConfirmAccountProviderEvents
|
||||
data object ClearError : ConfirmAccountProviderEvents
|
||||
}
|
||||
|
||||
-2
@@ -46,7 +46,6 @@ class ConfirmAccountProviderNode(
|
||||
fun navigateToLoginPassword()
|
||||
fun navigateToOAuth(oAuthDetails: OAuthDetails)
|
||||
fun navigateToCreateAccount(url: String)
|
||||
fun navigateToChangeAccountProvider()
|
||||
}
|
||||
|
||||
private val callback: Callback = callback()
|
||||
@@ -61,7 +60,6 @@ class ConfirmAccountProviderNode(
|
||||
onOAuthDetails = callback::navigateToOAuth,
|
||||
onNeedLoginPassword = callback::navigateToLoginPassword,
|
||||
onCreateAccountContinue = callback::navigateToCreateAccount,
|
||||
onChange = callback::navigateToChangeAccountProvider,
|
||||
onLearnMoreClick = { openLearnMorePage(context) },
|
||||
)
|
||||
}
|
||||
|
||||
+40
-6
@@ -9,14 +9,23 @@
|
||||
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
|
||||
import io.element.android.features.login.impl.changeserver.ChangeServerState
|
||||
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||
import io.element.android.features.login.impl.login.LoginModeState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
||||
@AssistedInject
|
||||
@@ -24,6 +33,7 @@ class ConfirmAccountProviderPresenter(
|
||||
@Assisted private val params: Params,
|
||||
private val accountProviderDataSource: AccountProviderDataSource,
|
||||
private val loginModePresenter: Presenter<LoginModeState>,
|
||||
private val changeServerPresenter: Presenter<ChangeServerState>,
|
||||
) : Presenter<ConfirmAccountProviderState> {
|
||||
data class Params(
|
||||
val isAccountCreation: Boolean,
|
||||
@@ -38,25 +48,49 @@ class ConfirmAccountProviderPresenter(
|
||||
override fun present(): ConfirmAccountProviderState {
|
||||
val accountProvider by accountProviderDataSource.flow.collectAsState()
|
||||
val loginModeState = loginModePresenter.present()
|
||||
val changeServerState = changeServerPresenter.present()
|
||||
|
||||
fun handleEvent(event: ConfirmAccountProviderEvents) {
|
||||
when (event) {
|
||||
ConfirmAccountProviderEvents.Continue -> loginModeState.eventSink(
|
||||
// Editable input, seeded from the current (history-defaulted) account provider until the user edits it.
|
||||
var userInput by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val accountProviderInput = userInput ?: accountProvider.url
|
||||
val latestAccountProviderInput by rememberUpdatedState(accountProviderInput)
|
||||
|
||||
// Once the chosen account provider has been validated and persisted, proceed with the actual sign in / sign up.
|
||||
LaunchedEffect(changeServerState.changeServerAction) {
|
||||
if (changeServerState.changeServerAction is AsyncData.Success) {
|
||||
loginModeState.eventSink(
|
||||
LoginModeEvent.Submit(
|
||||
isAccountCreation = params.isAccountCreation,
|
||||
homeserverUrl = accountProvider.url,
|
||||
homeserverUrl = latestAccountProviderInput.trim(),
|
||||
resolvedHomeserverUrl = null,
|
||||
loginHint = null,
|
||||
)
|
||||
)
|
||||
ConfirmAccountProviderEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvent(event: ConfirmAccountProviderEvents) {
|
||||
when (event) {
|
||||
is ConfirmAccountProviderEvents.UserInputChanged -> {
|
||||
userInput = event.accountProvider
|
||||
}
|
||||
// Validate (and persist) the chosen account provider before proceeding. This also enforces the
|
||||
// enterprise account-provider access control, which the login submit does not do on its own.
|
||||
ConfirmAccountProviderEvents.Continue -> changeServerState.eventSink(
|
||||
ChangeServerEvents.ChangeServer(AccountProvider(url = accountProviderInput.trim()))
|
||||
)
|
||||
ConfirmAccountProviderEvents.ClearError -> {
|
||||
loginModeState.eventSink(LoginModeEvent.ClearError)
|
||||
changeServerState.eventSink(ChangeServerEvents.ClearError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ConfirmAccountProviderState(
|
||||
accountProvider = accountProvider,
|
||||
accountProviderInput = accountProviderInput,
|
||||
isAccountCreation = params.isAccountCreation,
|
||||
loginModeState = loginModeState,
|
||||
changeServerState = changeServerState,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
+10
-4
@@ -8,17 +8,23 @@
|
||||
|
||||
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||
|
||||
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.login.LoginModeState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
data class ConfirmAccountProviderState(
|
||||
val accountProvider: AccountProvider,
|
||||
val accountProviderInput: String,
|
||||
val isAccountCreation: Boolean,
|
||||
val loginModeState: LoginModeState,
|
||||
val changeServerState: ChangeServerState,
|
||||
val eventSink: (ConfirmAccountProviderEvents) -> Unit
|
||||
) {
|
||||
val submitEnabled: Boolean
|
||||
get() = accountProvider.url.isNotEmpty() &&
|
||||
(loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading)
|
||||
get() = accountProviderInput.isNotBlank() &&
|
||||
loginModeState.loginMode is AsyncData.Uninitialized &&
|
||||
changeServerState.changeServerAction is AsyncData.Uninitialized
|
||||
|
||||
val isLoading: Boolean
|
||||
get() = loginModeState.loginMode is AsyncData.Loading ||
|
||||
changeServerState.changeServerAction is AsyncData.Loading
|
||||
}
|
||||
|
||||
+6
-4
@@ -9,8 +9,8 @@
|
||||
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
||||
import io.element.android.features.login.impl.changeserver.ChangeServerState
|
||||
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
||||
import io.element.android.features.login.impl.login.LoginModeState
|
||||
import io.element.android.features.login.impl.login.aLoginModeState
|
||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||
@@ -31,13 +31,15 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<Confir
|
||||
}
|
||||
|
||||
private fun aConfirmAccountProviderState(
|
||||
accountProvider: AccountProvider = anAccountProvider(),
|
||||
accountProviderInput: String = "matrix.org",
|
||||
isAccountCreation: Boolean = false,
|
||||
loginModeState: LoginModeState = aLoginModeState(),
|
||||
changeServerState: ChangeServerState = aChangeServerState(),
|
||||
eventSink: (ConfirmAccountProviderEvents) -> Unit = {},
|
||||
) = ConfirmAccountProviderState(
|
||||
accountProvider = accountProvider,
|
||||
accountProviderInput = accountProviderInput,
|
||||
isAccountCreation = isAccountCreation,
|
||||
loginModeState = loginModeState,
|
||||
changeServerState = changeServerState,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
||||
+86
-42
@@ -8,29 +8,38 @@
|
||||
|
||||
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
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.changeserver.ChangeServerView
|
||||
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||
import io.element.android.features.login.impl.login.LoginModeView
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.form.textFieldState
|
||||
import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext
|
||||
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.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
@@ -44,14 +53,8 @@ fun ConfirmAccountProviderView(
|
||||
onNeedLoginPassword: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onCreateAccountContinue: (url: String) -> Unit,
|
||||
onChange: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isLoading by remember(state.loginModeState.loginMode) {
|
||||
derivedStateOf {
|
||||
state.loginModeState.loginMode is AsyncData.Loading
|
||||
}
|
||||
}
|
||||
val eventSink = state.eventSink
|
||||
|
||||
HeaderFooterPage(
|
||||
@@ -62,55 +65,97 @@ fun ConfirmAccountProviderView(
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.UserProfileSolid()),
|
||||
title = stringResource(
|
||||
id = if (state.isAccountCreation) {
|
||||
R.string.screen_account_provider_signup_title
|
||||
CommonStrings.screen_select_server_title_register
|
||||
} else {
|
||||
R.string.screen_account_provider_signin_title
|
||||
},
|
||||
state.accountProvider.title
|
||||
CommonStrings.screen_select_server_title_login
|
||||
}
|
||||
),
|
||||
subTitle = stringResource(
|
||||
id = if (state.isAccountCreation) {
|
||||
R.string.screen_account_provider_signup_subtitle
|
||||
} else {
|
||||
R.string.screen_account_provider_signin_subtitle
|
||||
},
|
||||
)
|
||||
subTitle = null,
|
||||
)
|
||||
},
|
||||
footer = {
|
||||
ButtonColumnMolecule {
|
||||
Button(
|
||||
text = stringResource(id = CommonStrings.action_continue),
|
||||
showProgress = isLoading,
|
||||
onClick = { eventSink.invoke(ConfirmAccountProviderEvents.Continue) },
|
||||
enabled = state.submitEnabled || isLoading,
|
||||
showProgress = state.isLoading,
|
||||
onClick = { eventSink(ConfirmAccountProviderEvents.Continue) },
|
||||
enabled = state.submitEnabled && !state.isLoading,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.loginContinue)
|
||||
)
|
||||
TextButton(
|
||||
text = stringResource(id = R.string.screen_account_provider_change),
|
||||
onClick = onChange,
|
||||
enabled = !isLoading,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.loginChangeServer)
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
LoginModeView(
|
||||
loginMode = state.loginModeState.loginMode,
|
||||
onClearError = {
|
||||
eventSink(ConfirmAccountProviderEvents.ClearError)
|
||||
var input by textFieldState(stateValue = state.accountProviderInput)
|
||||
val focusManager = LocalFocusManager.current
|
||||
TextField(
|
||||
value = input,
|
||||
onValueChange = {
|
||||
input = it
|
||||
eventSink(ConfirmAccountProviderEvents.UserInputChanged(it))
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 40.dp)
|
||||
.onTabOrEnterKeyFocusNext(focusManager)
|
||||
.testTag(TestTags.changeServerServer),
|
||||
label = stringResource(id = CommonStrings.screen_select_server_textfield_header),
|
||||
supportingText = stringResource(
|
||||
id = if (state.isAccountCreation) {
|
||||
CommonStrings.screen_select_server_textfield_footer_register
|
||||
} else {
|
||||
CommonStrings.screen_select_server_textfield_footer_login
|
||||
}
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Uri,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
eventSink(ConfirmAccountProviderEvents.Continue)
|
||||
}),
|
||||
singleLine = true,
|
||||
trailingIcon = if (input.isNotEmpty()) {
|
||||
{
|
||||
Box(
|
||||
Modifier.clickable(
|
||||
onClickLabel = stringResource(CommonStrings.action_clear),
|
||||
role = Role.Button,
|
||||
) {
|
||||
input = ""
|
||||
eventSink(ConfirmAccountProviderEvents.UserInputChanged(""))
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Close(),
|
||||
contentDescription = stringResource(CommonStrings.action_clear)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onOAuthDetails = onOAuthDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
}
|
||||
|
||||
// Renders the account-provider validation errors, progress and permission dialogs.
|
||||
// The successful validation itself is observed by the presenter, which then proceeds with the login flow.
|
||||
ChangeServerView(
|
||||
state = state.changeServerState,
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onSuccess = {},
|
||||
)
|
||||
|
||||
LoginModeView(
|
||||
loginMode = state.loginModeState.loginMode,
|
||||
onClearError = { eventSink(ConfirmAccountProviderEvents.ClearError) },
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onOAuthDetails = onOAuthDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
|
||||
LocalNetworkPermissionDialogView(
|
||||
dialog = state.loginModeState.localNetworkPermissionDialog,
|
||||
onSubmit = {
|
||||
@@ -133,6 +178,5 @@ internal fun ConfirmAccountProviderViewPreview(
|
||||
onNeedLoginPassword = {},
|
||||
onCreateAccountContinue = {},
|
||||
onLearnMoreClick = {},
|
||||
onChange = {},
|
||||
)
|
||||
}
|
||||
|
||||
+78
-70
@@ -8,15 +8,21 @@
|
||||
|
||||
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||
|
||||
import app.cash.turbine.ReceiveTurbine
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.appconfig.AuthenticationConfig
|
||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource
|
||||
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
|
||||
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
||||
import io.element.android.features.login.impl.login.LoginMode
|
||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||
import io.element.android.features.login.impl.screens.onboarding.createLoginModePresenter
|
||||
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
|
||||
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
@@ -25,6 +31,9 @@ import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
|
||||
import io.element.android.libraries.oauth.api.OAuthAction
|
||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||
import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
|
||||
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -42,7 +51,7 @@ class ConfirmAccountProviderPresenterTest {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isAccountCreation).isFalse()
|
||||
assertThat(initialState.submitEnabled).isTrue()
|
||||
assertThat(initialState.accountProvider.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL)
|
||||
assertThat(initialState.accountProviderInput).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL)
|
||||
assertThat(initialState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
||||
}
|
||||
}
|
||||
@@ -60,13 +69,9 @@ class ConfirmAccountProviderPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.submitEnabled).isTrue()
|
||||
assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
val successState = awaitLoginMode { it is AsyncData.Success }
|
||||
assertThat(successState.loginModeState.loginMode.dataOrNull()).isEqualTo(LoginMode.PasswordLogin)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,13 +88,9 @@ class ConfirmAccountProviderPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.submitEnabled).isTrue()
|
||||
assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
val successState = awaitLoginMode { it is AsyncData.Success }
|
||||
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,17 +109,13 @@ class ConfirmAccountProviderPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.submitEnabled).isTrue()
|
||||
assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
val successState = awaitLoginMode { it is AsyncData.Success }
|
||||
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
authenticationService.givenOAuthCancelError(AN_EXCEPTION)
|
||||
defaultOAuthActionFlow.post(OAuthAction.GoBack())
|
||||
val cancelFailureState = awaitItem()
|
||||
val cancelFailureState = awaitLoginMode { it is AsyncData.Failure }
|
||||
assertThat(cancelFailureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,16 +134,12 @@ class ConfirmAccountProviderPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.submitEnabled).isTrue()
|
||||
assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
val successState = awaitLoginMode { it is AsyncData.Success }
|
||||
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
defaultOAuthActionFlow.post(OAuthAction.GoBack())
|
||||
val cancelFinalState = awaitItem()
|
||||
val cancelFinalState = awaitLoginMode { it is AsyncData.Uninitialized }
|
||||
assertThat(cancelFinalState.loginModeState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,12 +158,11 @@ class ConfirmAccountProviderPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.submitEnabled).isTrue()
|
||||
assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||
awaitLoginMode { it is AsyncData.Loading }
|
||||
defaultOAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true))
|
||||
val cancelFinalState = awaitItem()
|
||||
val cancelFinalState = awaitLoginMode { it is AsyncData.Uninitialized }
|
||||
assertThat(cancelFinalState.loginModeState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,19 +181,13 @@ class ConfirmAccountProviderPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.submitEnabled).isTrue()
|
||||
assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
val successState = awaitLoginMode { it is AsyncData.Success }
|
||||
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
authenticationService.givenLoginError(AN_EXCEPTION)
|
||||
defaultOAuthActionFlow.post(OAuthAction.Success("aUrl"))
|
||||
val cancelLoadingState = awaitItem()
|
||||
assertThat(cancelLoadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val cancelFailureState = awaitItem()
|
||||
val cancelFailureState = awaitLoginMode { it is AsyncData.Failure }
|
||||
assertThat(cancelFailureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,16 +206,12 @@ class ConfirmAccountProviderPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.submitEnabled).isTrue()
|
||||
assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
val successState = awaitLoginMode { it is AsyncData.Success }
|
||||
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
defaultOidcActionFlow.post(OAuthAction.Success("aUrl"))
|
||||
val successSuccessState = awaitItem()
|
||||
val successSuccessState = awaitLoginMode { it is AsyncData.Loading }
|
||||
assertThat(successSuccessState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,10 +228,10 @@ class ConfirmAccountProviderPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||
skipItems(1) // Loading
|
||||
val failureState = awaitItem()
|
||||
assertThat(failureState.submitEnabled).isFalse()
|
||||
assertThat(failureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
||||
// The account provider validation fails, so the login is never attempted.
|
||||
val failureState = awaitState { it.changeServerState.changeServerAction is AsyncData.Failure }
|
||||
assertThat(failureState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,18 +248,15 @@ class ConfirmAccountProviderPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
|
||||
// Submit will return an error
|
||||
// Submit will return an error while validating the account provider
|
||||
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
|
||||
|
||||
skipItems(1) // Loading
|
||||
|
||||
// Check an error was returned
|
||||
val submittedState = awaitItem()
|
||||
assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
||||
val submittedState = awaitState { it.changeServerState.changeServerAction is AsyncData.Failure }
|
||||
|
||||
// Assert the error is then cleared
|
||||
submittedState.eventSink(ConfirmAccountProviderEvents.ClearError)
|
||||
val clearedState = awaitItem()
|
||||
val clearedState = awaitState { it.changeServerState.changeServerAction is AsyncData.Uninitialized }
|
||||
assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
||||
}
|
||||
}
|
||||
@@ -286,7 +265,7 @@ class ConfirmAccountProviderPresenterTest {
|
||||
fun `present - confirm account creation without oidc and without url generates an error`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails())
|
||||
Result.success(aMatrixHomeServerDetails(supportsPasswordLogin = true))
|
||||
},
|
||||
)
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
@@ -299,14 +278,14 @@ class ConfirmAccountProviderPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
|
||||
skipItems(1) // Loading
|
||||
// Check an error was returned
|
||||
val submittedState = awaitItem()
|
||||
val submittedState = awaitLoginMode { it is AsyncData.Failure }
|
||||
assertThat(submittedState.loginModeState.loginMode.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java)
|
||||
// Assert the error is then cleared
|
||||
submittedState.eventSink(ConfirmAccountProviderEvents.ClearError)
|
||||
val clearedState = awaitItem()
|
||||
val clearedState = awaitLoginMode { it is AsyncData.Uninitialized }
|
||||
assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,10 +303,9 @@ class ConfirmAccountProviderPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
|
||||
skipItems(1) // Loading
|
||||
val submittedState = awaitItem()
|
||||
assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
val submittedState = awaitLoginMode { it is AsyncData.Success }
|
||||
assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,10 +325,9 @@ class ConfirmAccountProviderPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
|
||||
skipItems(1) // Loading
|
||||
val submittedState = awaitItem()
|
||||
assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
val submittedState = awaitLoginMode { it is AsyncData.Success }
|
||||
assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,7 +336,7 @@ class ConfirmAccountProviderPresenterTest {
|
||||
val aUrl = "aUrl"
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails())
|
||||
Result.success(aMatrixHomeServerDetails(supportsPasswordLogin = true))
|
||||
},
|
||||
)
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
@@ -370,9 +347,26 @@ class ConfirmAccountProviderPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
|
||||
skipItems(1) // Loading
|
||||
val submittedState = awaitItem()
|
||||
val submittedState = awaitLoginMode { it is AsyncData.Success }
|
||||
assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isEqualTo(LoginMode.AccountCreation(aUrl))
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Awaits until the emitted state's login mode matches [predicate], skipping the intermediate
|
||||
* account-provider validation states, and returns that state.
|
||||
*/
|
||||
private suspend fun ReceiveTurbine<ConfirmAccountProviderState>.awaitLoginMode(
|
||||
predicate: (AsyncData<LoginMode>) -> Boolean,
|
||||
): ConfirmAccountProviderState = awaitState { predicate(it.loginModeState.loginMode) }
|
||||
|
||||
private suspend fun ReceiveTurbine<ConfirmAccountProviderState>.awaitState(
|
||||
predicate: (ConfirmAccountProviderState) -> Boolean,
|
||||
): ConfirmAccountProviderState {
|
||||
while (true) {
|
||||
val item = awaitItem()
|
||||
if (predicate(item)) return item
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,5 +384,19 @@ class ConfirmAccountProviderPresenterTest {
|
||||
oAuthActionFlow = defaultOAuthActionFlow,
|
||||
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever,
|
||||
),
|
||||
changeServerPresenter = ChangeServerPresenter(
|
||||
authenticationService = matrixAuthenticationService,
|
||||
accountProviderDataSource = accountProviderDataSource,
|
||||
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
isAllowedToConnectToHomeserverResult = { true },
|
||||
),
|
||||
wellknownRetriever = FakeWellknownRetriever(),
|
||||
),
|
||||
localNetworkPermissionGate = LocalNetworkPermissionGate(
|
||||
advisor = FakeLocalNetworkPermissionAdvisor(),
|
||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(FakePermissionsPresenter()),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,4 +10,9 @@
|
||||
<string name="a11y_state_collapsed">"Collapsed"</string>
|
||||
<string name="a11y_active">Active</string>
|
||||
<string name="a11y_inactive">Inactive</string>
|
||||
<string name="screen_select_server_title_login">"Enter your account provider"</string>
|
||||
<string name="screen_select_server_title_register">"Choose an account provider"</string>
|
||||
<string name="screen_select_server_textfield_header">"Account provider"</string>
|
||||
<string name="screen_select_server_textfield_footer_login">"If your account is provided by your company or a community, then change the account provider (example: companyname.com)."</string>
|
||||
<string name="screen_select_server_textfield_footer_register">"Create an account on matrix.org for free. If you want to create an account with a different provider, then change it accordingly (example: communityname.org)."</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user