Merge pull request #5237 from element-hq/feature/bma/removeLoginUserStory

Remove LoginUserStory.
This commit is contained in:
Benoit Marty
2025-08-27 15:44:21 +02:00
committed by GitHub
13 changed files with 6 additions and 119 deletions

View File

@@ -10,12 +10,10 @@ package io.element.android.appnav.root
import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.core.state.SavedStateMap
import io.element.android.appnav.di.MatrixSessionCache
import io.element.android.features.login.api.LoginUserStory
import io.element.android.features.preferences.api.CacheService
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
import io.element.android.libraries.sessionstorage.api.LoggedInState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
@@ -33,7 +31,6 @@ class RootNavStateFlowFactory @Inject constructor(
private val cacheService: CacheService,
private val matrixSessionCache: MatrixSessionCache,
private val imageLoaderHolder: ImageLoaderHolder,
private val loginUserStory: LoginUserStory,
private val sessionPreferencesStoreFactory: SessionPreferencesStoreFactory,
) {
private var currentCacheIndex = 0
@@ -42,13 +39,11 @@ class RootNavStateFlowFactory @Inject constructor(
return combine(
cacheIndexFlow(savedStateMap),
authenticationService.loggedInStateFlow(),
loginUserStory.loginFlowIsDone,
) { cacheIndex, loggedInState, loginFlowIsDone ->
if (loginFlowIsDone) {
RootNavState(cacheIndex = cacheIndex, loggedInState = loggedInState)
} else {
RootNavState(cacheIndex = cacheIndex, loggedInState = LoggedInState.NotLoggedIn)
}
) { cacheIndex, loggedInState ->
RootNavState(
cacheIndex = cacheIndex,
loggedInState = loggedInState,
)
}
}

View File

@@ -1,14 +0,0 @@
/*
* Copyright 2023, 2024 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.api
import kotlinx.coroutines.flow.StateFlow
interface LoginUserStory {
val loginFlowIsDone: StateFlow<Boolean>
}

View File

@@ -1,26 +0,0 @@
/*
* Copyright 2023, 2024 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
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.login.api.LoginUserStory
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultLoginUserStory @Inject constructor() : LoginUserStory {
// True by default, will be set to false when the login user story is started, and set to true again once it's done.
override val loginFlowIsDone: MutableStateFlow<Boolean> = MutableStateFlow(true)
fun setLoginFlowIsDone(value: Boolean) {
loginFlowIsDone.value = value
}
}

View File

@@ -55,7 +55,6 @@ class LoginFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val accountProviderDataSource: AccountProviderDataSource,
private val defaultLoginUserStory: DefaultLoginUserStory,
private val oidcActionFlow: OidcActionFlow,
) : BaseFlowNode<LoginFlowNode.NavTarget>(
backstack = BackStack(
@@ -77,7 +76,6 @@ class LoginFlowNode @AssistedInject constructor(
override fun onBuilt() {
super.onBuilt()
defaultLoginUserStory.setLoginFlowIsDone(false)
lifecycle.subscribe(
onResume = {
if (externalAppStarted) {

View File

@@ -12,7 +12,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter
@@ -38,7 +37,6 @@ import javax.inject.Inject
class LoginHelper @Inject constructor(
private val oidcActionFlow: OidcActionFlow,
private val authenticationService: MatrixAuthenticationService,
private val defaultLoginUserStory: DefaultLoginUserStory,
private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever,
) {
private val loginModeState: MutableState<AsyncData<LoginMode>> = mutableStateOf(AsyncData.Uninitialized)
@@ -108,9 +106,6 @@ class LoginHelper @Inject constructor(
}
is OidcAction.Success -> {
authenticationService.loginWithOidc(oidcAction.url)
.onSuccess { _ ->
defaultLoginUserStory.setLoginFlowIsDone(true)
}
.onFailure { failure ->
loginModeState.value = AsyncData.Failure(failure)
}

View File

@@ -24,7 +24,6 @@ import com.bumble.appyx.navmodel.backstack.operation.replace
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.di.QrCodeLoginBindings
import io.element.android.features.login.impl.di.QrCodeLoginComponent
import io.element.android.features.login.impl.screens.qrcode.confirmation.QrCodeConfirmationNode
@@ -54,7 +53,6 @@ class QrCodeLoginFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
qrCodeLoginComponentBuilder: QrCodeLoginComponent.Builder,
private val defaultLoginUserStory: DefaultLoginUserStory,
private val coroutineDispatchers: CoroutineDispatchers,
) : BaseFlowNode<QrCodeLoginFlowNode.NavTarget>(
backstack = BackStack(
@@ -198,7 +196,6 @@ class QrCodeLoginFlowNode @AssistedInject constructor(
authenticationJob = launch(coroutineDispatchers.main) {
qrCodeLoginManager.authenticate(qrCodeLoginData)
.onSuccess {
defaultLoginUserStory.setLoginFlowIsDone(true)
authenticationJob = null
}
.onFailure { throwable ->

View File

@@ -16,7 +16,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.data.tryOrNull
@@ -36,7 +35,6 @@ class CreateAccountPresenter @AssistedInject constructor(
@Assisted private val url: String,
private val authenticationService: MatrixAuthenticationService,
private val clientProvider: MatrixClientProvider,
private val defaultLoginUserStory: DefaultLoginUserStory,
private val messageParser: MessageParser,
private val buildMeta: BuildMeta,
) : Presenter<CreateAccountState> {
@@ -86,8 +84,6 @@ class CreateAccountPresenter @AssistedInject constructor(
val sessionVerificationService = client.sessionVerificationService()
withTimeout(10.seconds) { sessionVerificationService.sessionVerifiedStatus.first { it.isVerified() } }
}
// We will not navigate to the WaitList screen, so the login user story is done
defaultLoginUserStory.setLoginFlowIsDone(true)
loggedInState.value = AsyncAction.Success(sessionId)
}.onFailure { failure ->
loggedInState.value = AsyncAction.Failure(failure)

View File

@@ -15,7 +15,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
@@ -28,7 +27,6 @@ import javax.inject.Inject
class LoginPasswordPresenter @Inject constructor(
private val authenticationService: MatrixAuthenticationService,
private val accountProviderDataSource: AccountProviderDataSource,
private val defaultLoginUserStory: DefaultLoginUserStory,
) : Presenter<LoginPasswordState> {
@Composable
override fun present(): LoginPasswordState {
@@ -69,8 +67,6 @@ class LoginPasswordPresenter @Inject constructor(
loggedInState.value = AsyncData.Loading()
authenticationService.login(formState.login.trim(), formState.password)
.onSuccess { sessionId ->
// We will not navigate to the WaitList screen, so the login user story is done
defaultLoginUserStory.setLoginFlowIsDone(true)
loggedInState.value = AsyncData.Success(sessionId)
}
.onFailure { failure ->

View File

@@ -12,7 +12,6 @@ import com.bumble.appyx.core.modality.AncestryInfo
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.di.FakeMergedQrCodeLoginComponent
import io.element.android.features.login.impl.screens.qrcode.confirmation.QrCodeConfirmationStep
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@@ -98,7 +97,7 @@ class QrCodeLoginFlowNodeTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `startAuthentication - success marks the login flow as done`() = runTest {
fun `startAuthentication - success`() = runTest {
val fakeAuthenticationService = FakeMatrixAuthenticationService(
loginWithQrCodeResult = { _, progress ->
progress(QrCodeLoginStep.Finished)
@@ -107,12 +106,8 @@ class QrCodeLoginFlowNodeTest {
)
// Test with a real manager to ensure the flow is correctly done
val qrCodeLoginManager = DefaultQrCodeLoginManager(fakeAuthenticationService)
val defaultLoginUserStory = DefaultLoginUserStory().apply {
loginFlowIsDone.value = false
}
val flowNode = createLoginFlowNode(
qrCodeLoginManager = qrCodeLoginManager,
defaultLoginUserStory = defaultLoginUserStory,
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
)
@@ -122,7 +117,6 @@ class QrCodeLoginFlowNodeTest {
advanceUntilIdle()
assertThat(qrCodeLoginManager.currentLoginStep.value).isEqualTo(QrCodeLoginStep.Finished)
assertThat(defaultLoginUserStory.loginFlowIsDone.value).isTrue()
assertThat(flowNode.isLoginInProgress()).isFalse()
}
@@ -137,12 +131,8 @@ class QrCodeLoginFlowNodeTest {
)
// Test with a real manager to ensure the flow is correctly done
val qrCodeLoginManager = DefaultQrCodeLoginManager(fakeAuthenticationService)
val defaultLoginUserStory = DefaultLoginUserStory().apply {
loginFlowIsDone.value = false
}
val flowNode = createLoginFlowNode(
qrCodeLoginManager = qrCodeLoginManager,
defaultLoginUserStory = defaultLoginUserStory,
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
)
@@ -152,7 +142,6 @@ class QrCodeLoginFlowNodeTest {
advanceUntilIdle()
assertThat(qrCodeLoginManager.currentLoginStep.value).isEqualTo(QrCodeLoginStep.Failed(QrLoginException.Unknown))
assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse()
assertThat(flowNode.isLoginInProgress()).isFalse()
}
@@ -167,12 +156,8 @@ class QrCodeLoginFlowNodeTest {
)
// Test with a real manager to ensure the flow is correctly done
val qrCodeLoginManager = DefaultQrCodeLoginManager(fakeAuthenticationService)
val defaultLoginUserStory = DefaultLoginUserStory().apply {
loginFlowIsDone.value = false
}
val flowNode = createLoginFlowNode(
qrCodeLoginManager = qrCodeLoginManager,
defaultLoginUserStory = defaultLoginUserStory,
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
)
@@ -183,13 +168,11 @@ class QrCodeLoginFlowNodeTest {
advanceUntilIdle()
assertThat(qrCodeLoginManager.currentLoginStep.value).isEqualTo(QrCodeLoginStep.Uninitialized)
assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse()
assertThat(flowNode.isLoginInProgress()).isFalse()
}
private fun TestScope.createLoginFlowNode(
qrCodeLoginManager: QrCodeLoginManager = FakeQrCodeLoginManager(),
defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(),
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers()
): QrCodeLoginFlowNode {
val buildContext = BuildContext(
@@ -201,7 +184,6 @@ class QrCodeLoginFlowNodeTest {
buildContext = buildContext,
plugins = emptyList(),
qrCodeLoginComponentBuilder = FakeMergedQrCodeLoginComponent.Builder(qrCodeLoginManager),
defaultLoginUserStory = defaultLoginUserStory,
coroutineDispatchers = coroutineDispatchers,
)
}

View File

@@ -13,7 +13,6 @@ import app.cash.turbine.test
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.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.login.LoginMode
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
@@ -30,7 +29,6 @@ import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow
import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.waitForPredicate
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@@ -186,13 +184,9 @@ class ConfirmAccountProviderPresenterTest {
fun `present - oidc - success with success`() = runTest {
val authenticationService = FakeMatrixAuthenticationService()
val defaultOidcActionFlow = FakeOidcActionFlow()
val defaultLoginUserStory = DefaultLoginUserStory().apply {
setLoginFlowIsDone(false)
}
val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService,
defaultOidcActionFlow = defaultOidcActionFlow,
defaultLoginUserStory = defaultLoginUserStory,
)
authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
moleculeFlow(RecompositionMode.Immediate) {
@@ -207,11 +201,9 @@ class ConfirmAccountProviderPresenterTest {
assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse()
defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
val successSuccessState = awaitItem()
assertThat(successSuccessState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
waitForPredicate { defaultLoginUserStory.loginFlowIsDone.value }
}
}
@@ -357,7 +349,6 @@ class ConfirmAccountProviderPresenterTest {
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
defaultOidcActionFlow: OidcActionFlow = FakeOidcActionFlow(),
defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(),
webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(),
) = ConfirmAccountProviderPresenter(
params = params,
@@ -365,7 +356,6 @@ class ConfirmAccountProviderPresenterTest {
loginHelper = createLoginHelper(
authenticationService = matrixAuthenticationService,
oidcActionFlow = defaultOidcActionFlow,
defaultLoginUserStory = defaultLoginUserStory,
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever,
),
)

View File

@@ -11,7 +11,6 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@@ -88,9 +87,6 @@ class CreateAccountPresenterTest {
@Test
fun `present - receiving a message able to be parsed change the state to success`() = runTest {
val defaultLoginUserStory = DefaultLoginUserStory()
defaultLoginUserStory.setLoginFlowIsDone(false)
assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse()
val lambda = lambdaRecorder<String, ExternalSession> { _ -> anExternalSession() }
val sessionVerificationService = FakeSessionVerificationService()
val client = FakeMatrixClient(sessionVerificationService = sessionVerificationService)
@@ -100,7 +96,6 @@ class CreateAccountPresenterTest {
importCreatedSessionLambda = { Result.success(A_SESSION_ID) }
),
messageParser = FakeMessageParser(lambda),
defaultLoginUserStory = defaultLoginUserStory,
clientProvider = clientProvider,
)
moleculeFlow(RecompositionMode.Immediate) {
@@ -113,14 +108,10 @@ class CreateAccountPresenterTest {
assertThat(awaitItem().createAction.dataOrNull()).isEqualTo(A_SESSION_ID)
}
lambda.assertions().isCalledOnce().with(value("aMessage"))
assertThat(defaultLoginUserStory.loginFlowIsDone.value).isTrue()
}
@Test
fun `present - receiving a message able to be parsed but error in importing change the state to error`() = runTest {
val defaultLoginUserStory = DefaultLoginUserStory()
defaultLoginUserStory.setLoginFlowIsDone(false)
assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse()
val presenter = createPresenter(
authenticationService = FakeMatrixAuthenticationService(
importCreatedSessionLambda = { Result.failure(AN_EXCEPTION) }
@@ -135,20 +126,17 @@ class CreateAccountPresenterTest {
assertThat(awaitItem().createAction.isLoading()).isTrue()
assertThat(awaitItem().createAction.errorOrNull()).isNotNull()
}
assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse()
}
private fun createPresenter(
url: String = "aUrl",
authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(),
messageParser: MessageParser = FakeMessageParser(),
buildMeta: BuildMeta = aBuildMeta(),
clientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
) = CreateAccountPresenter(
url = url,
authenticationService = authenticationService,
defaultLoginUserStory = defaultLoginUserStory,
messageParser = messageParser,
buildMeta = buildMeta,
clientProvider = clientProvider,

View File

@@ -10,7 +10,6 @@ package io.element.android.features.login.impl.screens.loginpassword
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.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.SessionId
@@ -64,12 +63,9 @@ class LoginPasswordPresenterTest {
fun `present - submit`() = runTest {
val authenticationService = FakeMatrixAuthenticationService()
authenticationService.givenHomeserver(A_HOMESERVER)
val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) }
createLoginPasswordPresenter(
authenticationService = authenticationService,
defaultLoginUserStory = loginUserStory,
).test {
assertThat(loginUserStory.loginFlowIsDone.value).isFalse()
val initialState = awaitItem()
initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME))
initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD))
@@ -80,7 +76,6 @@ class LoginPasswordPresenterTest {
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
val loggedInState = awaitItem()
assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Success(A_SESSION_ID))
assertThat(loginUserStory.loginFlowIsDone.value).isTrue()
}
}
@@ -134,10 +129,8 @@ class LoginPasswordPresenterTest {
private fun createLoginPasswordPresenter(
authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(),
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory()
): LoginPasswordPresenter = LoginPasswordPresenter(
authenticationService = authenticationService,
accountProviderDataSource = accountProviderDataSource,
defaultLoginUserStory = defaultLoginUserStory,
)
}

View File

@@ -11,7 +11,6 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.OnBoardingConfig
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
import io.element.android.features.login.impl.login.LoginHelper
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
@@ -253,11 +252,9 @@ private fun createPresenter(
fun createLoginHelper(
oidcActionFlow: OidcActionFlow = FakeOidcActionFlow(),
authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(),
webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(),
): LoginHelper = LoginHelper(
oidcActionFlow = oidcActionFlow,
authenticationService = authenticationService,
defaultLoginUserStory = defaultLoginUserStory,
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever,
)