From 6f0788fd3410344ba4a998716a1e265eb97f1b6f Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 7 Jul 2026 21:37:58 +0200 Subject: [PATCH] Introduce LoginModePresenter with local network permission gate --- .../features/login/impl/di/LoginModule.kt | 5 + .../features/login/impl/login/LoginHelper.kt | 128 ------------------ .../login/impl/login/LoginModeEvent.kt | 23 ++++ .../login/impl/login/LoginModePresenter.kt | 117 ++++++++++++++++ .../login/{LoginMode.kt => LoginModeState.kt} | 11 +- .../impl/login/LoginModeStateProvider.kt | 21 +++ .../login/impl/resolver/HomeserverResolver.kt | 13 ++ .../ChooseAccountProviderPresenter.kt | 35 +++-- .../ChooseAccountProviderState.kt | 7 +- .../ChooseAccountProviderStateProvider.kt | 9 +- .../ChooseAccountProviderView.kt | 17 ++- .../LoginWithClassicPresenter.kt | 23 ++-- .../loginwithclassic/LoginWithClassicState.kt | 5 +- .../LoginWithClassicStateProvider.kt | 8 +- .../loginwithclassic/LoginWithClassicView.kt | 17 ++- .../ConfirmAccountProviderPresenter.kt | 21 ++- .../ConfirmAccountProviderState.kt | 8 +- .../ConfirmAccountProviderStateProvider.kt | 9 +- .../ConfirmAccountProviderView.kt | 18 ++- .../screens/onboarding/OnBoardingPresenter.kt | 23 ++-- .../screens/onboarding/OnBoardingState.kt | 7 +- .../onboarding/OnBoardingStateProvider.kt | 8 +- .../impl/screens/onboarding/OnBoardingView.kt | 17 ++- .../ChooseAccountProviderPresenterTest.kt | 20 +-- .../ChooseAccountProviderStateTest.kt | 5 +- .../ChooseAccountProviderViewTest.kt | 3 +- .../LoginWithClassicPresenterTest.kt | 18 +-- .../ConfirmAccountProviderPresenterTest.kt | 76 +++++------ .../onboarding/OnBoardingPresenterTest.kt | 25 ++-- .../screens/onboarding/OnboardingViewTest.kt | 9 +- .../SearchAccountProviderPresenterTest.kt | 11 +- 31 files changed, 420 insertions(+), 297 deletions(-) delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeEvent.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/{LoginMode.kt => LoginModeState.kt} (55%) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt index 4523e6f45e..037acf859b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt @@ -14,6 +14,8 @@ import dev.zacsweers.metro.Binds import dev.zacsweers.metro.ContributesTo import io.element.android.features.login.impl.changeserver.ChangeServerPresenter import io.element.android.features.login.impl.changeserver.ChangeServerState +import io.element.android.features.login.impl.login.LoginModePresenter +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.Presenter @ContributesTo(AppScope::class) @@ -21,4 +23,7 @@ import io.element.android.libraries.architecture.Presenter interface LoginModule { @Binds fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter + + @Binds + fun bindLoginModePresenter(presenter: LoginModePresenter): Presenter } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt deleted file mode 100644 index 3c871a8a1d..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * 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.login - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import dev.zacsweers.metro.Inject -import io.element.android.features.login.impl.error.ChangeServerError -import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter -import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter -import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported -import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter -import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.architecture.runCatchingUpdatingState -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.auth.OAuthPrompt -import io.element.android.libraries.oauth.api.OAuthAction -import io.element.android.libraries.oauth.api.OAuthActionFlow - -/** - * This class is responsible for managing the login flow, including handling OIDC actions and - * submitting login requests. - * It's a helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter] - * and [ChooseAccountProviderPresenter]. - */ -@Inject -class LoginHelper( - private val oAuthActionFlow: OAuthActionFlow, - private val authenticationService: MatrixAuthenticationService, - private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever, -) { - private val loginModeState: MutableState> = mutableStateOf(AsyncData.Uninitialized) - - @Composable - fun collectLoginMode(): State> { - LaunchedEffect(Unit) { - oAuthActionFlow.collect { oAuthAction -> - if (oAuthAction != null) { - onOAuthAction(oAuthAction) - } - } - } - return loginModeState - } - - fun clearError() { - loginModeState.value = AsyncData.Uninitialized - } - - suspend fun submit( - isAccountCreation: Boolean, - homeserverUrl: String, - resolvedHomeserverUrl: String?, - loginHint: String?, - ) { - suspend { - authenticationService.setHomeserver(homeserverUrl).recoverCatching { - // No .well-known file? - // If the homeserver is not reachable, try using resolvedHomeserverUrl. - if (resolvedHomeserverUrl != null && resolvedHomeserverUrl != homeserverUrl) { - authenticationService.setHomeserver(resolvedHomeserverUrl).getOrThrow() - } else { - throw it - } - }.map { matrixHomeServerDetails -> - if (matrixHomeServerDetails.supportsOAuthLogin) { - // Retrieve the details right now - val oAuthPrompt = if (isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login - LoginMode.OAuth( - authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = loginHint).getOrThrow() - ) - } else if (isAccountCreation) { - val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl) - LoginMode.AccountCreation(url) - } else if (matrixHomeServerDetails.supportsPasswordLogin) { - LoginMode.PasswordLogin - } else { - error("Unsupported login flow") - } - }.getOrThrow() - }.runCatchingUpdatingState( - state = loginModeState, - errorTransform = { - when (it) { - is AccountCreationNotSupported -> it - else -> ChangeServerError.from(it) - } - } - ) - } - - private suspend fun onOAuthAction(oAuthAction: OAuthAction) { - if (oAuthAction is OAuthAction.GoBack && oAuthAction.toUnblock && loginModeState.value !is AsyncData.Loading) { - // Ignore GoBack action if the current state is not Loading. This GoBack action is coming from LoginFlowNode. - // This can happen if there is an error, for instance attempt to login again on the same account. - return - } - loginModeState.value = AsyncData.Loading() - when (oAuthAction) { - is OAuthAction.GoBack -> { - authenticationService.cancelOAuthLogin() - .onSuccess { - loginModeState.value = AsyncData.Uninitialized - } - .onFailure { failure -> - loginModeState.value = AsyncData.Failure(failure) - } - } - is OAuthAction.Success -> { - authenticationService.loginWithOAuth(oAuthAction.url) - .onFailure { failure -> - loginModeState.value = AsyncData.Failure(failure) - } - } - } - oAuthActionFlow.reset() - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeEvent.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeEvent.kt new file mode 100644 index 0000000000..73400890d5 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeEvent.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2026 Element Creations 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.login + +sealed interface LoginModeEvent { + data class Submit( + val isAccountCreation: Boolean, + val homeserverUrl: String, + val resolvedHomeserverUrl: String?, + val loginHint: String?, + ) : LoginModeEvent + + data object ClearError : LoginModeEvent + + data object RequestLocalNetworkPermission : LoginModeEvent + + data object DismissLocalNetworkPermission : LoginModeEvent +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt new file mode 100644 index 0000000000..43c03bfbe3 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2026 Element Creations 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.login + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import dev.zacsweers.metro.Inject +import io.element.android.features.login.impl.error.ChangeServerError +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate +import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported +import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.auth.OAuthPrompt +import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.libraries.oauth.api.OAuthActionFlow + +@Inject +class LoginModePresenter( + private val oAuthActionFlow: OAuthActionFlow, + private val authenticationService: MatrixAuthenticationService, + private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever, + private val localNetworkPermissionGate: LocalNetworkPermissionGate, +) : Presenter { + @Composable + override fun present(): LoginModeState { + val loginMode: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } + + val gateState = localNetworkPermissionGate.present( + urlOf = { it.homeserverUrl }, + onProceed = { request -> performSubmit(request, loginMode) }, + ) + + // Forward OAuth navigation events to the presenter's own loginMode state. + LaunchedEffect(Unit) { + oAuthActionFlow.collect { action -> + if (action != null) handleOAuthAction(action, loginMode) + } + } + + fun handleEvent(event: LoginModeEvent) { + when (event) { + LoginModeEvent.ClearError -> loginMode.value = AsyncData.Uninitialized + is LoginModeEvent.Submit -> gateState.submit(event) + LoginModeEvent.DismissLocalNetworkPermission -> gateState.abort() + LoginModeEvent.RequestLocalNetworkPermission -> gateState.requestPermission() + } + } + return LoginModeState( + loginMode = loginMode.value, + localNetworkPermissionDialog = gateState.dialog, + eventSink = ::handleEvent, + ) + } + + private suspend fun performSubmit(request: LoginModeEvent.Submit, loginMode: MutableState>) { + suspend { + authenticationService.setHomeserver(request.homeserverUrl).recoverCatching { + // Fallback to the well-known-resolved URL if the primary URL failed and the caller supplied one. + if (request.resolvedHomeserverUrl != null && request.resolvedHomeserverUrl != request.homeserverUrl) { + authenticationService.setHomeserver(request.resolvedHomeserverUrl).getOrThrow() + } else { + throw it + } + }.map { matrixHomeServerDetails -> + when { + matrixHomeServerDetails.supportsOAuthLogin -> { + val oAuthPrompt = if (request.isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login + LoginMode.OAuth( + authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = request.loginHint).getOrThrow() + ) + } + request.isAccountCreation -> LoginMode.AccountCreation( + webClientUrlForAuthenticationRetriever.retrieve(request.homeserverUrl) + ) + matrixHomeServerDetails.supportsPasswordLogin -> LoginMode.PasswordLogin + else -> error("Unsupported login flow") + } + }.getOrThrow() + }.runCatchingUpdatingState( + state = loginMode, + errorTransform = { + when (it) { + is AccountCreationNotSupported -> it + else -> ChangeServerError.from(it) + } + } + ) + } + + private suspend fun handleOAuthAction(action: OAuthAction, loginMode: MutableState>) { + if (action is OAuthAction.GoBack && action.toUnblock && loginMode.value !is AsyncData.Loading) { + // Ignore GoBack that isn't tied to an in-flight login — it comes from LoginFlowNode + // when the user backs out after a completed login attempt. + return + } + loginMode.value = AsyncData.Loading() + when (action) { + is OAuthAction.GoBack -> authenticationService.cancelOAuthLogin() + .onSuccess { loginMode.value = AsyncData.Uninitialized } + .onFailure { loginMode.value = AsyncData.Failure(it) } + is OAuthAction.Success -> authenticationService.loginWithOAuth(action.url) + .onFailure { loginMode.value = AsyncData.Failure(it) } + } + oAuthActionFlow.reset() + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt similarity index 55% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt index 5ea52e0ebd..3ca3a42a5d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt @@ -1,6 +1,5 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * Copyright (c) 2026 Element Creations Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * Please see LICENSE files in the repository root for full details. @@ -8,8 +7,16 @@ package io.element.android.features.login.impl.login +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.auth.OAuthDetails +data class LoginModeState( + val loginMode: AsyncData, + val localNetworkPermissionDialog: LocalNetworkPermissionDialog, + val eventSink: (LoginModeEvent) -> Unit, +) + sealed interface LoginMode { data object PasswordLogin : LoginMode data class OAuth(val oAuthDetails: OAuthDetails) : LoginMode diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt new file mode 100644 index 0000000000..a0adc327fe --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026 Element Creations 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.login + +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog +import io.element.android.libraries.architecture.AsyncData + +fun aLoginModeState( + loginMode: AsyncData = AsyncData.Uninitialized, + localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None, + eventSink: (LoginModeEvent) -> Unit = {}, +) = LoginModeState( + loginMode = loginMode, + localNetworkPermissionDialog = localNetworkPermissionDialog, + eventSink = eventSink, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt index 1c4ef8cd09..b6eb26c87f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.core.coroutine.parallelMap import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.isValidUrl import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker +import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -28,6 +29,7 @@ import java.util.Collections class HomeserverResolver( private val dispatchers: CoroutineDispatchers, private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker, + private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor, ) { fun resolve(userInput: String): Flow> = flow { val flowContext = currentCoroutineContext() @@ -39,6 +41,17 @@ class HomeserverResolver( // Run all the requests in parallel withContext(dispatchers.io) { list.parallelMap { url -> + // Skip the compatibility probe if we'd need ACCESS_LOCAL_NETWORK first — + // otherwise the probe hangs on the TCP timeout for ~30s. Emit the URL as a + // candidate directly; the actual sign-in flow triggers the permission prompt. + if (localNetworkPermissionAdvisor.shouldRequestPermissionFor(url)) { + currentList.add(HomeserverData(homeserverUrl = url)) + withContext(flowContext) { + emit(currentList.toList()) + } + return@parallelMap + } + val isValid = homeServerLoginCompatibilityChecker.check(url) .onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") } .getOrNull() diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt index c6d6d76486..a9983a19d1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt @@ -12,50 +12,49 @@ 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 dev.zacsweers.metro.Inject 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.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 import io.element.android.libraries.core.uri.ensureProtocol import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.launch @Inject class ChooseAccountProviderPresenter( private val enterpriseService: EnterpriseService, - private val loginHelper: LoginHelper, + private val loginModePresenter: Presenter, ) : Presenter { @Composable override fun present(): ChooseAccountProviderState { - val localCoroutineScope = rememberCoroutineScope() - val loginMode by loginHelper.collectLoginMode() - + val loginModeState = loginModePresenter.present() var selectedAccountProvider: AccountProvider? by remember { mutableStateOf(null) } fun handleEvent(event: ChooseAccountProviderEvents) { when (event) { - ChooseAccountProviderEvents.Continue -> localCoroutineScope.launch { - selectedAccountProvider?.let { - loginHelper.submit( - isAccountCreation = false, - homeserverUrl = it.url, - resolvedHomeserverUrl = null, - loginHint = null, + ChooseAccountProviderEvents.Continue -> { + selectedAccountProvider?.let { provider -> + loginModeState.eventSink( + LoginModeEvent.Submit( + isAccountCreation = false, + homeserverUrl = provider.url, + resolvedHomeserverUrl = null, + loginHint = null, + ) ) } } is ChooseAccountProviderEvents.SelectAccountProvider -> { - // Ensure that the user do not change the server during processing - if (loginMode is AsyncData.Uninitialized) { + // Ensure that the user does not change the server during processing. + if (loginModeState.loginMode is AsyncData.Uninitialized) { selectedAccountProvider = event.accountProvider } } - ChooseAccountProviderEvents.ClearError -> loginHelper.clearError() + ChooseAccountProviderEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError) } } @@ -77,7 +76,7 @@ class ChooseAccountProviderPresenter( return ChooseAccountProviderState( accountProviders = staticAccountProviderList, selectedAccountProvider = selectedAccountProvider, - loginMode = loginMode, + loginModeState = loginModeState, eventSink = ::handleEvent, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt index e34fbccb41..a31e5c464f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt @@ -9,16 +9,17 @@ 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.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncData import kotlinx.collections.immutable.ImmutableList data class ChooseAccountProviderState( val accountProviders: ImmutableList, val selectedAccountProvider: AccountProvider?, - val loginMode: AsyncData, + val loginModeState: LoginModeState, val eventSink: (ChooseAccountProviderEvents) -> Unit, ) { val submitEnabled: Boolean - get() = selectedAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading) + get() = selectedAccountProvider != null && + (loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt index 93bd665011..3fea548fca 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt @@ -11,7 +11,8 @@ 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.features.login.impl.login.LoginModeState +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.architecture.AsyncData import kotlinx.collections.immutable.toImmutableList @@ -58,7 +59,7 @@ open class ChooseAccountProviderStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, + loginModeState: LoginModeState = aLoginModeState(), eventSink: (ChooseAccountProviderEvents) -> Unit = {}, ) = ChooseAccountProviderState( accountProviders = accountProviders.toImmutableList(), selectedAccountProvider = selectedAccountProvider, - loginMode = loginMode, + loginModeState = loginModeState, eventSink = eventSink, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt index f05606dbc3..be37775965 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt @@ -33,6 +33,8 @@ 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.localnetwork.LocalNetworkPermissionDialogView +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.IconTitleSubtitleMolecule @@ -56,9 +58,9 @@ fun ChooseAccountProviderView( onCreateAccountContinue: (url: String) -> Unit, modifier: Modifier = Modifier, ) { - val isLoading by remember(state.loginMode) { + val isLoading by remember(state.loginModeState.loginMode) { derivedStateOf { - state.loginMode is AsyncData.Loading + state.loginModeState.loginMode is AsyncData.Loading } } @@ -124,7 +126,7 @@ fun ChooseAccountProviderView( Spacer(modifier = Modifier.height(48.dp)) } LoginModeView( - loginMode = state.loginMode, + loginMode = state.loginModeState.loginMode, onClearError = { state.eventSink(ChooseAccountProviderEvents.ClearError) }, @@ -135,6 +137,15 @@ fun ChooseAccountProviderView( ) } } + LocalNetworkPermissionDialogView( + dialog = state.loginModeState.localNetworkPermissionDialog, + onSubmit = { + state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission) + }, + onDismiss = { + state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission) + } + ) } @PreviewsDayNight diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt index 90a528c3ae..765dfaadd1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt @@ -20,7 +20,8 @@ import dev.zacsweers.metro.AssistedInject 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.classic.ElementClassicConnectionState -import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.login.LoginModeEvent +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta @@ -32,7 +33,7 @@ import kotlinx.coroutines.launch class LoginWithClassicPresenter( @Assisted private val userId: UserId, @Assisted private val navigator: LoginWithClassicNavigator, - private val loginHelper: LoginHelper, + private val loginModePresenter: Presenter, private val elementClassicConnection: ElementClassicConnection, private val accountProviderDataSource: AccountProviderDataSource, private val buildMeta: BuildMeta, @@ -51,7 +52,7 @@ class LoginWithClassicPresenter( var loginWithClassicAction by remember { mutableStateOf>(AsyncAction.Uninitialized) } - val loginMode by loginHelper.collectLoginMode() + val loginModeState = loginModePresenter.present() val elementClassicConnectionState by elementClassicConnection.stateFlow.collectAsState() fun handleEvent(event: LoginWithClassicEvent) { @@ -69,11 +70,13 @@ class LoginWithClassicPresenter( val elementClassicUserId = currentState.elementClassicSession.userId val accountProvider = elementClassicUserId.domainName.orEmpty().ensureProtocol() accountProviderDataSource.setUrl(accountProvider) - loginHelper.submit( - isAccountCreation = false, - homeserverUrl = accountProvider, - resolvedHomeserverUrl = currentState.elementClassicSession.homeserverUrl, - loginHint = "mxid:" + elementClassicUserId.value, + loginModeState.eventSink( + LoginModeEvent.Submit( + isAccountCreation = false, + homeserverUrl = accountProvider, + resolvedHomeserverUrl = currentState.elementClassicSession.homeserverUrl, + loginHint = "mxid:" + elementClassicUserId.value, + ) ) } } @@ -83,7 +86,7 @@ class LoginWithClassicPresenter( } LoginWithClassicEvent.ClearError -> { loginWithClassicAction = AsyncAction.Uninitialized - loginHelper.clearError() + loginModeState.eventSink(LoginModeEvent.ClearError) } } } @@ -94,7 +97,7 @@ class LoginWithClassicPresenter( userId = userId, displayName = elementClassicReady?.displayName, avatar = elementClassicReady?.avatar, - loginMode = loginMode, + loginModeState = loginModeState, loginWithClassicAction = loginWithClassicAction, eventSink = ::handleEvent, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt index 275a444768..8e93298e76 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt @@ -9,9 +9,8 @@ package io.element.android.features.login.impl.screens.classic.loginwithclassic import android.graphics.Bitmap import androidx.compose.runtime.Stable -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.UserId @Stable @@ -21,6 +20,6 @@ data class LoginWithClassicState( val displayName: String?, val avatar: Bitmap?, val loginWithClassicAction: AsyncAction, - val loginMode: AsyncData, + val loginModeState: LoginModeState, val eventSink: (LoginWithClassicEvent) -> Unit, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt index 31b1770f63..6dfc292f6f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt @@ -9,9 +9,9 @@ package io.element.android.features.login.impl.screens.classic.loginwithclassic import android.graphics.Bitmap import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.matrix.api.core.UserId @@ -29,7 +29,7 @@ fun aLoginWithClassicState( displayName: String? = null, avatar: Bitmap? = null, loginWithClassicAction: AsyncAction = AsyncAction.Uninitialized, - loginMode: AsyncData = AsyncData.Uninitialized, + loginModeState: LoginModeState = aLoginModeState(), eventSink: (LoginWithClassicEvent) -> Unit = {}, ) = LoginWithClassicState( isElementPro = isElementPro, @@ -37,6 +37,6 @@ fun aLoginWithClassicState( displayName = displayName, avatar = avatar, loginWithClassicAction = loginWithClassicAction, - loginMode = loginMode, + loginModeState = loginModeState, eventSink = eventSink, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt index b1ca50fe61..389d07ea1a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt @@ -35,6 +35,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.features.login.impl.R +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView +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 @@ -65,9 +67,9 @@ fun LoginWithClassicView( onCreateAccountContinue: (url: String) -> Unit, modifier: Modifier = Modifier, ) { - val isLoading by remember(state.loginMode) { + val isLoading by remember(state.loginModeState.loginMode) { derivedStateOf { - state.loginMode is AsyncData.Loading + state.loginModeState.loginMode is AsyncData.Loading } } @@ -195,7 +197,7 @@ fun LoginWithClassicView( } ) LoginModeView( - loginMode = state.loginMode, + loginMode = state.loginModeState.loginMode, onClearError = { state.eventSink(LoginWithClassicEvent.ClearError) }, @@ -204,6 +206,15 @@ fun LoginWithClassicView( onNeedLoginPassword = onNeedLoginPassword, onCreateAccountContinue = onCreateAccountContinue, ) + LocalNetworkPermissionDialogView( + dialog = state.loginModeState.localNetworkPermissionDialog, + onSubmit = { + state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission) + }, + onDismiss = { + state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission) + } + ) } @PreviewsDayNight diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt index bf06613830..0111b1e7d7 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt @@ -11,20 +11,19 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.login.LoginModeEvent +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.Presenter -import kotlinx.coroutines.launch @AssistedInject class ConfirmAccountProviderPresenter( @Assisted private val params: Params, private val accountProviderDataSource: AccountProviderDataSource, - private val loginHelper: LoginHelper, + private val loginModePresenter: Presenter, ) : Presenter { data class Params( val isAccountCreation: Boolean, @@ -38,28 +37,26 @@ class ConfirmAccountProviderPresenter( @Composable override fun present(): ConfirmAccountProviderState { val accountProvider by accountProviderDataSource.flow.collectAsState() - val localCoroutineScope = rememberCoroutineScope() - - val loginMode by loginHelper.collectLoginMode() + val loginModeState = loginModePresenter.present() fun handleEvent(event: ConfirmAccountProviderEvents) { when (event) { - ConfirmAccountProviderEvents.Continue -> localCoroutineScope.launch { - loginHelper.submit( + ConfirmAccountProviderEvents.Continue -> loginModeState.eventSink( + LoginModeEvent.Submit( isAccountCreation = params.isAccountCreation, homeserverUrl = accountProvider.url, resolvedHomeserverUrl = null, loginHint = null, ) - } - ConfirmAccountProviderEvents.ClearError -> loginHelper.clearError() + ) + ConfirmAccountProviderEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError) } } return ConfirmAccountProviderState( accountProvider = accountProvider, isAccountCreation = params.isAccountCreation, - loginMode = loginMode, + loginModeState = loginModeState, eventSink = ::handleEvent, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt index b29b610b3e..6e6deb8c2c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt @@ -9,14 +9,16 @@ 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.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncData data class ConfirmAccountProviderState( val accountProvider: AccountProvider, val isAccountCreation: Boolean, - val loginMode: AsyncData, + val loginModeState: LoginModeState, val eventSink: (ConfirmAccountProviderEvents) -> Unit ) { - val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading) + val submitEnabled: Boolean + get() = accountProvider.url.isNotEmpty() && + (loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt index f3a48a86b4..f597ad6b54 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt @@ -11,7 +11,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.login.LoginMode +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 import io.element.android.libraries.architecture.AsyncData @@ -24,7 +25,7 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, + loginModeState: LoginModeState = aLoginModeState(), eventSink: (ConfirmAccountProviderEvents) -> Unit = {}, ) = ConfirmAccountProviderState( accountProvider = accountProvider, isAccountCreation = isAccountCreation, - loginMode = loginMode, + loginModeState = loginModeState, eventSink = eventSink ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt index c2525f3756..3db0748a04 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt @@ -20,6 +20,8 @@ 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.localnetwork.LocalNetworkPermissionDialogView +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 @@ -45,9 +47,9 @@ fun ConfirmAccountProviderView( onChange: () -> Unit, modifier: Modifier = Modifier, ) { - val isLoading by remember(state.loginMode) { + val isLoading by remember(state.loginModeState.loginMode) { derivedStateOf { - state.loginMode is AsyncData.Loading + state.loginModeState.loginMode is AsyncData.Loading } } val eventSink = state.eventSink @@ -98,7 +100,7 @@ fun ConfirmAccountProviderView( } ) { LoginModeView( - loginMode = state.loginMode, + loginMode = state.loginModeState.loginMode, onClearError = { eventSink(ConfirmAccountProviderEvents.ClearError) }, @@ -108,6 +110,16 @@ fun ConfirmAccountProviderView( onCreateAccountContinue = onCreateAccountContinue, ) } + + LocalNetworkPermissionDialogView( + dialog = state.loginModeState.localNetworkPermissionDialog, + onSubmit = { + state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission) + }, + onDismiss = { + state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission) + } + ) } @PreviewsDayNight diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt index 306549d11b..29e4b1afe5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt @@ -25,7 +25,8 @@ import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.canConnectToAnyHomeserver 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.login.LoginHelper +import io.element.android.features.login.impl.login.LoginModeEvent +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta @@ -41,7 +42,7 @@ class OnBoardingPresenter( private val enterpriseService: EnterpriseService, private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, - private val loginHelper: LoginHelper, + private val loginModePresenter: Presenter, private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider, private val sessionStore: SessionStore, private val accountProviderDataSource: AccountProviderDataSource, @@ -98,21 +99,23 @@ class OnBoardingPresenter( value = sessionStore.numberOfSessions() > 0 } - val loginMode by loginHelper.collectLoginMode() + val loginModeState = loginModePresenter.present() fun handleEvent(event: OnBoardingEvents) { when (event) { is OnBoardingEvents.OnSignIn -> localCoroutineScope.launch { // Ensure that the current account provider is set accountProviderDataSource.setUrl(event.defaultAccountProvider) - loginHelper.submit( - isAccountCreation = false, - homeserverUrl = event.defaultAccountProvider, - resolvedHomeserverUrl = null, - loginHint = params.loginHint?.takeIf { forcedAccountProvider == null }, + loginModeState.eventSink( + LoginModeEvent.Submit( + isAccountCreation = false, + homeserverUrl = event.defaultAccountProvider, + resolvedHomeserverUrl = null, + loginHint = params.loginHint?.takeIf { forcedAccountProvider == null }, + ) ) } - OnBoardingEvents.ClearError -> loginHelper.clearError() + OnBoardingEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError) OnBoardingEvents.OnVersionClick -> { if (canReportBug) { if (multipleTapToUnlock.unlock(localCoroutineScope)) { @@ -133,7 +136,7 @@ class OnBoardingPresenter( canLoginWithQrCode = canLoginWithQrCode, canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT, canReportBug = canReportBug && showReportBug, - loginMode = loginMode, + loginModeState = loginModeState, version = buildMeta.versionName, onBoardingLogoResId = onBoardingLogoResId, eventSink = ::handleEvent, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt index 316efb03ef..4fb4b8090e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt @@ -9,7 +9,7 @@ package io.element.android.features.login.impl.screens.onboarding import androidx.annotation.DrawableRes -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncData data class OnBoardingState( @@ -25,9 +25,10 @@ data class OnBoardingState( val version: String, @DrawableRes val onBoardingLogoResId: Int?, - val loginMode: AsyncData, + val loginModeState: LoginModeState, val eventSink: (OnBoardingEvents) -> Unit, ) { val submitEnabled: Boolean - get() = defaultAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading) + get() = defaultAccountProvider != null && + (loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt index 249a904dc3..41e4a81818 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt @@ -10,8 +10,8 @@ package io.element.android.features.login.impl.screens.onboarding import androidx.annotation.DrawableRes import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.login.LoginMode -import io.element.android.libraries.architecture.AsyncData +import io.element.android.features.login.impl.login.LoginModeState +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.designsystem.R open class OnBoardingStateProvider : PreviewParameterProvider { @@ -49,7 +49,7 @@ fun anOnBoardingState( version: String = "1.0.0", @DrawableRes customLogoResId: Int? = null, - loginMode: AsyncData = AsyncData.Uninitialized, + loginModeState: LoginModeState = aLoginModeState(), eventSink: (OnBoardingEvents) -> Unit = {}, ) = OnBoardingState( isAddingAccount = isAddingAccount, @@ -62,7 +62,7 @@ fun anOnBoardingState( canCreateAccount = canCreateAccount, canReportBug = canReportBug, version = version, - loginMode = loginMode, + loginModeState = loginModeState, onBoardingLogoResId = customLogoResId, eventSink = eventSink, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt index 420bb61475..74fdbffab1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt @@ -33,6 +33,8 @@ 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.features.login.impl.localnetwork.LocalNetworkPermissionDialogView +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.atoms.ElementLogoAtom @@ -74,7 +76,7 @@ fun OnBoardingView( ) { val loginView = @Composable { LoginModeView( - loginMode = state.loginMode, + loginMode = state.loginModeState.loginMode, onClearError = { state.eventSink(OnBoardingEvents.ClearError) }, @@ -83,6 +85,15 @@ fun OnBoardingView( onNeedLoginPassword = onNeedLoginPassword, onCreateAccountContinue = onCreateAccountContinue, ) + LocalNetworkPermissionDialogView( + dialog = state.loginModeState.localNetworkPermissionDialog, + onSubmit = { + state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission) + }, + onDismiss = { + state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission) + } + ) } val buttons = @Composable { OnBoardingButtons( @@ -260,9 +271,9 @@ private fun OnBoardingButtons( onCreateAccount: () -> Unit, onReportProblem: () -> Unit, ) { - val isLoading by remember(state.loginMode) { + val isLoading by remember(state.loginModeState.loginMode) { derivedStateOf { - state.loginMode is AsyncData.Loading + state.loginModeState.loginMode is AsyncData.Loading } } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt index 7461a7db55..4f00bb002b 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt @@ -12,8 +12,8 @@ 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.features.login.impl.login.LoginModePresenter +import io.element.android.features.login.impl.screens.onboarding.createLoginModePresenter 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 @@ -81,7 +81,7 @@ class ChooseAccountProviderPresenterTest { enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, ), - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), ) @@ -105,7 +105,7 @@ class ChooseAccountProviderPresenterTest { enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, ), - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), ) @@ -121,12 +121,12 @@ class ChooseAccountProviderPresenterTest { // Check an error was returned val submittedState = awaitItem() - assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(submittedState.loginModeState.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) + assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } } @@ -138,7 +138,7 @@ class ChooseAccountProviderPresenterTest { enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, ), - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), ) @@ -152,7 +152,7 @@ class ChooseAccountProviderPresenterTest { it.eventSink(ChooseAccountProviderEvents.Continue) } awaitItem().also { - assertThat(it.loginMode.isLoading()).isTrue() + assertThat(it.loginModeState.loginMode.isLoading()).isTrue() it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider2)) } expectNoEvents() @@ -162,8 +162,8 @@ class ChooseAccountProviderPresenterTest { private fun createPresenter( enterpriseService: EnterpriseService = FakeEnterpriseService(), - loginHelper: LoginHelper = createLoginHelper(), + loginModePresenter: LoginModePresenter = createLoginModePresenter(), ) = ChooseAccountProviderPresenter( enterpriseService = enterpriseService, - loginHelper = loginHelper, + loginModePresenter = loginModePresenter, ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateTest.kt index ba1eff39e8..a5cb54e7e1 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateTest.kt @@ -11,6 +11,7 @@ package io.element.android.features.login.impl.screens.chooseaccountprovider import com.google.common.truth.Truth.assertThat import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.architecture.AsyncData import org.junit.Test @@ -35,7 +36,7 @@ class ChooseAccountProviderStateTest { fun `submitEnabled returns false when there is a selectedAccountProvider but there is an error`() { val sut = aChooseAccountProviderState( selectedAccountProvider = anAccountProvider(), - loginMode = AsyncData.Failure(Throwable("Error")), + loginModeState = aLoginModeState(loginMode = AsyncData.Failure(Throwable("Error"))), ) assertThat(sut.submitEnabled).isFalse() } @@ -44,7 +45,7 @@ class ChooseAccountProviderStateTest { fun `submitEnabled returns false when there is a selectedAccountProvider but the result is successful`() { val sut = aChooseAccountProviderState( selectedAccountProvider = anAccountProvider(), - loginMode = AsyncData.Success(LoginMode.PasswordLogin), + loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.PasswordLogin)), ) assertThat(sut.submitEnabled).isFalse() } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt index b036e07e92..f1ce703d1c 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.login.impl.accountprovider.anAccountProvider +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.matrix.test.AN_EXCEPTION @@ -69,7 +70,7 @@ class ChooseAccountProviderViewTest : RobolectricTest() { val eventSink = EventsRecorder() setChooseAccountProviderView( state = aChooseAccountProviderState( - loginMode = AsyncData.Failure(AN_EXCEPTION), + loginModeState = aLoginModeState(loginMode = AsyncData.Failure(AN_EXCEPTION)), eventSink = eventSink, ), ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt index 6b2a4fb0e1..8d56386fa4 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt @@ -16,8 +16,8 @@ import io.element.android.features.login.impl.classic.FakeElementClassicConnecti import io.element.android.features.login.impl.classic.ROOM_KEYS_VERSION import io.element.android.features.login.impl.classic.anElementClassicReady import io.element.android.features.login.impl.classic.anElementClassicSession -import io.element.android.features.login.impl.login.LoginHelper -import io.element.android.features.login.impl.screens.onboarding.createLoginHelper +import io.element.android.features.login.impl.login.LoginModePresenter +import io.element.android.features.login.impl.screens.onboarding.createLoginModePresenter import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_FAILURE_REASON @@ -42,7 +42,7 @@ class LoginWithClassicPresenterTest { assertThat(initialState.displayName).isNull() assertThat(initialState.avatar).isNull() assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue() - assertThat(initialState.loginMode.isUninitialized()).isTrue() + assertThat(initialState.loginModeState.loginMode.isUninitialized()).isTrue() } } @@ -69,7 +69,7 @@ class LoginWithClassicPresenterTest { ) val presenter = createPresenter( elementClassicConnection = elementClassicConnection, - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), ) @@ -108,7 +108,7 @@ class LoginWithClassicPresenterTest { ) val presenter = createPresenter( elementClassicConnection = elementClassicConnection, - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), ) @@ -147,7 +147,7 @@ class LoginWithClassicPresenterTest { val navigateToMissingKeyBackupResult = lambdaRecorder { } val presenter = createPresenter( elementClassicConnection = elementClassicConnection, - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), navigator = FakeLoginWithClassicNavigator( @@ -188,7 +188,7 @@ class LoginWithClassicPresenterTest { val navigateToMissingKeyBackupResult = lambdaRecorder { } val presenter = createPresenter( elementClassicConnection = elementClassicConnection, - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), navigator = FakeLoginWithClassicNavigator( @@ -247,14 +247,14 @@ class LoginWithClassicPresenterTest { private fun createPresenter( userId: UserId = A_USER_ID, navigator: LoginWithClassicNavigator = FakeLoginWithClassicNavigator(), - loginHelper: LoginHelper = createLoginHelper(), + loginModePresenter: LoginModePresenter = createLoginModePresenter(), elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), isEnterpriseBuild: Boolean = false, ) = LoginWithClassicPresenter( userId = userId, navigator = navigator, - loginHelper = loginHelper, + loginModePresenter = loginModePresenter, elementClassicConnection = elementClassicConnection, accountProviderDataSource = accountProviderDataSource, buildMeta = aBuildMeta( diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt index a9045ab152..c20896870b 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt @@ -14,7 +14,7 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService 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 -import io.element.android.features.login.impl.screens.onboarding.createLoginHelper +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.libraries.architecture.AsyncData @@ -43,7 +43,7 @@ class ConfirmAccountProviderPresenterTest { assertThat(initialState.isAccountCreation).isFalse() assertThat(initialState.submitEnabled).isTrue() assertThat(initialState.accountProvider.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL) - assertThat(initialState.loginMode).isEqualTo(AsyncData.Uninitialized) + assertThat(initialState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } @@ -62,11 +62,11 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isEqualTo(LoginMode.PasswordLogin) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isEqualTo(LoginMode.PasswordLogin) } } @@ -85,11 +85,11 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) } } @@ -110,15 +110,15 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) authenticationService.givenOAuthCancelError(AN_EXCEPTION) defaultOAuthActionFlow.post(OAuthAction.GoBack()) val cancelFailureState = awaitItem() - assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(cancelFailureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) } } @@ -139,14 +139,14 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) defaultOAuthActionFlow.post(OAuthAction.GoBack()) val cancelFinalState = awaitItem() - assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) + assertThat(cancelFinalState.loginModeState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) } } @@ -167,10 +167,10 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) defaultOAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true)) val cancelFinalState = awaitItem() - assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) + assertThat(cancelFinalState.loginModeState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) } } @@ -191,17 +191,17 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) authenticationService.givenLoginError(AN_EXCEPTION) defaultOAuthActionFlow.post(OAuthAction.Success("aUrl")) val cancelLoadingState = awaitItem() - assertThat(cancelLoadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(cancelLoadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val cancelFailureState = awaitItem() - assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(cancelFailureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) } } @@ -222,14 +222,14 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) defaultOidcActionFlow.post(OAuthAction.Success("aUrl")) val successSuccessState = awaitItem() - assertThat(successSuccessState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(successSuccessState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) } } @@ -249,7 +249,7 @@ class ConfirmAccountProviderPresenterTest { skipItems(1) // Loading val failureState = awaitItem() assertThat(failureState.submitEnabled).isFalse() - assertThat(failureState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(failureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) } } @@ -273,12 +273,12 @@ class ConfirmAccountProviderPresenterTest { // Check an error was returned val submittedState = awaitItem() - assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) // Assert the error is then cleared submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) val clearedState = awaitItem() - assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) + assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } @@ -302,11 +302,11 @@ class ConfirmAccountProviderPresenterTest { skipItems(1) // Loading // Check an error was returned val submittedState = awaitItem() - assertThat(submittedState.loginMode.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java) + assertThat(submittedState.loginModeState.loginMode.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java) // Assert the error is then cleared submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) val clearedState = awaitItem() - assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) + assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } @@ -326,8 +326,8 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink(ConfirmAccountProviderEvents.Continue) skipItems(1) // Loading val submittedState = awaitItem() - assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) } } @@ -349,8 +349,8 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink(ConfirmAccountProviderEvents.Continue) skipItems(1) // Loading val submittedState = awaitItem() - assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) } } @@ -372,7 +372,7 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink(ConfirmAccountProviderEvents.Continue) skipItems(1) // Loading val submittedState = awaitItem() - assertThat(submittedState.loginMode.dataOrNull()).isEqualTo(LoginMode.AccountCreation(aUrl)) + assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isEqualTo(LoginMode.AccountCreation(aUrl)) } } @@ -385,7 +385,7 @@ class ConfirmAccountProviderPresenterTest { ) = ConfirmAccountProviderPresenter( params = params, accountProviderDataSource = accountProviderDataSource, - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = matrixAuthenticationService, oAuthActionFlow = defaultOAuthActionFlow, webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever, diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt index 8249694278..56ccc230b0 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt @@ -15,7 +15,8 @@ import io.element.android.features.enterprise.api.EnterpriseService 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.login.LoginHelper +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate +import io.element.android.features.login.impl.login.LoginModePresenter 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 @@ -255,7 +256,7 @@ class OnBoardingPresenterTest { enterpriseService = FakeEnterpriseService( isAllowedToConnectToHomeserverResult = { true }, ), - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), accountProviderDataSource = accountProviderDataSource, @@ -271,12 +272,12 @@ class OnBoardingPresenterTest { assertThat(accountProviderDataSource.flow.first().url).isEqualTo(A_HOMESERVER_URL_2) // Check an error was returned val submittedState = awaitItem() - assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) // Assert the error is then cleared submittedState.eventSink(OnBoardingEvents.ClearError) val clearedState = awaitItem() - assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) + assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } } @@ -292,7 +293,7 @@ private fun createPresenter( enterpriseService: EnterpriseService = FakeEnterpriseService(), wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(), rageshakeFeatureAvailability: () -> Flow = { flowOf(true) }, - loginHelper: LoginHelper = createLoginHelper(), + loginModePresenter: LoginModePresenter = createLoginModePresenter(), onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null }, sessionStore: SessionStore = InMemorySessionStore(), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), @@ -305,18 +306,26 @@ private fun createPresenter( wellknownRetriever = wellknownRetriever, ), rageshakeFeatureAvailability = rageshakeFeatureAvailability, - loginHelper = loginHelper, + loginModePresenter = loginModePresenter, onBoardingLogoResIdProvider = onBoardingLogoResIdProvider, sessionStore = sessionStore, accountProviderDataSource = accountProviderDataSource, ) -fun createLoginHelper( +fun createLoginModePresenter( oAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(), authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), -): LoginHelper = LoginHelper( + localNetworkPermissionAdvisor: io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor = + io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor(), + permissionsPresenterFactory: io.element.android.libraries.permissions.api.PermissionsPresenter.Factory = + io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory(), +): LoginModePresenter = LoginModePresenter( oAuthActionFlow = oAuthActionFlow, authenticationService = authenticationService, webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever, + localNetworkPermissionGate = LocalNetworkPermissionGate( + advisor = localNetworkPermissionAdvisor, + permissionsPresenterFactory = permissionsPresenterFactory, + ), ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt index 52d1667687..1bdcb9bda1 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt @@ -21,6 +21,7 @@ import com.google.testing.junit.testparameterinjector.KotlinTestParameters.named import com.google.testing.junit.testparameterinjector.TestParameter import io.element.android.features.login.impl.R import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.matrix.test.AN_EXCEPTION @@ -148,7 +149,7 @@ class OnboardingViewTest : RobolectricTestParameter() { setOnboardingView( state = anOnBoardingState( defaultAccountProvider = "element.io", - loginMode = AsyncData.Failure(AN_EXCEPTION), + loginModeState = aLoginModeState(loginMode = AsyncData.Failure(AN_EXCEPTION)), eventSink = eventSink, ), ) @@ -208,7 +209,7 @@ class OnboardingViewTest : RobolectricTestParameter() { ensureCalledOnce { callback -> setOnboardingView( state = anOnBoardingState( - loginMode = AsyncData.Success(LoginMode.PasswordLogin), + loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.PasswordLogin)), eventSink = eventSink, ), onNeedLoginPassword = callback, @@ -224,7 +225,7 @@ class OnboardingViewTest : RobolectricTestParameter() { ensureCalledOnceWithParam(oAuthDetails) { callback -> setOnboardingView( state = anOnBoardingState( - loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails)), + loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails))), eventSink = eventSink, ), onOAuthDetails = callback, @@ -240,7 +241,7 @@ class OnboardingViewTest : RobolectricTestParameter() { ensureCalledOnceWithParam(oAuthDetails.url) { callback -> setOnboardingView( state = anOnBoardingState( - loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl")), + loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl"))), eventSink = eventSink, ), onCreateAccountContinue = callback, diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt index 87afb77474..29c5d4a86c 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt @@ -13,6 +13,7 @@ import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.features.login.impl.resolver.HomeserverResolver import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatibilityChecker +import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -30,7 +31,7 @@ class SearchAccountProviderPresenterTest { fun `present - initial state`() = runTest { val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(true) }) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()), changeServerPresenter = { aChangeServerState() } ) presenter.test { @@ -44,7 +45,7 @@ class SearchAccountProviderPresenterTest { fun `present - error while checking login compatibility`() = runTest { val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.failure(IllegalStateException("Oops")) }) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()), changeServerPresenter = { aChangeServerState() } ) presenter.test { @@ -68,7 +69,7 @@ class SearchAccountProviderPresenterTest { fun `present - enter text no result`() = runTest { val fakeWellknownRetriever = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(false) }) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever, FakeLocalNetworkPermissionAdvisor()), changeServerPresenter = { aChangeServerState() } ) presenter.test { @@ -101,7 +102,7 @@ class SearchAccountProviderPresenterTest { } val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()), changeServerPresenter = { aChangeServerState() } ) presenter.test { @@ -141,7 +142,7 @@ class SearchAccountProviderPresenterTest { } val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()), changeServerPresenter = { aChangeServerState() } ) presenter.test {