Introduce LoginModePresenter with local network permission gate

This commit is contained in:
ganfra
2026-07-07 21:37:58 +02:00
parent e92923a77d
commit 6f0788fd34
31 changed files with 420 additions and 297 deletions
@@ -14,6 +14,8 @@ import dev.zacsweers.metro.Binds
import dev.zacsweers.metro.ContributesTo import dev.zacsweers.metro.ContributesTo
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter 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.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 import io.element.android.libraries.architecture.Presenter
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
@@ -21,4 +23,7 @@ import io.element.android.libraries.architecture.Presenter
interface LoginModule { interface LoginModule {
@Binds @Binds
fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter<ChangeServerState> fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter<ChangeServerState>
@Binds
fun bindLoginModePresenter(presenter: LoginModePresenter): Presenter<LoginModeState>
} }
@@ -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<AsyncData<LoginMode>> = mutableStateOf(AsyncData.Uninitialized)
@Composable
fun collectLoginMode(): State<AsyncData<LoginMode>> {
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()
}
}
@@ -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
}
@@ -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<LoginModeState> {
@Composable
override fun present(): LoginModeState {
val loginMode: MutableState<AsyncData<LoginMode>> = remember { mutableStateOf(AsyncData.Uninitialized) }
val gateState = localNetworkPermissionGate.present<LoginModeEvent.Submit>(
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<AsyncData<LoginMode>>) {
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<AsyncData<LoginMode>>) {
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()
}
}
@@ -1,6 +1,5 @@
/* /*
* Copyright (c) 2025 Element Creations Ltd. * Copyright (c) 2026 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
@@ -8,8 +7,16 @@
package io.element.android.features.login.impl.login 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 import io.element.android.libraries.matrix.api.auth.OAuthDetails
data class LoginModeState(
val loginMode: AsyncData<LoginMode>,
val localNetworkPermissionDialog: LocalNetworkPermissionDialog,
val eventSink: (LoginModeEvent) -> Unit,
)
sealed interface LoginMode { sealed interface LoginMode {
data object PasswordLogin : LoginMode data object PasswordLogin : LoginMode
data class OAuth(val oAuthDetails: OAuthDetails) : LoginMode data class OAuth(val oAuthDetails: OAuthDetails) : LoginMode
@@ -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<LoginMode> = AsyncData.Uninitialized,
localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None,
eventSink: (LoginModeEvent) -> Unit = {},
) = LoginModeState(
loginMode = loginMode,
localNetworkPermissionDialog = localNetworkPermissionDialog,
eventSink = eventSink,
)
@@ -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.ensureProtocol
import io.element.android.libraries.core.uri.isValidUrl import io.element.android.libraries.core.uri.isValidUrl
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
@@ -28,6 +29,7 @@ import java.util.Collections
class HomeserverResolver( class HomeserverResolver(
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker, private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker,
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
) { ) {
fun resolve(userInput: String): Flow<List<HomeserverData>> = flow { fun resolve(userInput: String): Flow<List<HomeserverData>> = flow {
val flowContext = currentCoroutineContext() val flowContext = currentCoroutineContext()
@@ -39,6 +41,17 @@ class HomeserverResolver(
// Run all the requests in parallel // Run all the requests in parallel
withContext(dispatchers.io) { withContext(dispatchers.io) {
list.parallelMap { url -> 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) val isValid = homeServerLoginCompatibilityChecker.check(url)
.onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") } .onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") }
.getOrNull() .getOrNull()
@@ -12,50 +12,49 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.appconfig.AuthenticationConfig import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.login.impl.accountprovider.AccountProvider 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.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.ensureProtocol
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
@Inject @Inject
class ChooseAccountProviderPresenter( class ChooseAccountProviderPresenter(
private val enterpriseService: EnterpriseService, private val enterpriseService: EnterpriseService,
private val loginHelper: LoginHelper, private val loginModePresenter: Presenter<LoginModeState>,
) : Presenter<ChooseAccountProviderState> { ) : Presenter<ChooseAccountProviderState> {
@Composable @Composable
override fun present(): ChooseAccountProviderState { override fun present(): ChooseAccountProviderState {
val localCoroutineScope = rememberCoroutineScope() val loginModeState = loginModePresenter.present()
val loginMode by loginHelper.collectLoginMode()
var selectedAccountProvider: AccountProvider? by remember { mutableStateOf(null) } var selectedAccountProvider: AccountProvider? by remember { mutableStateOf(null) }
fun handleEvent(event: ChooseAccountProviderEvents) { fun handleEvent(event: ChooseAccountProviderEvents) {
when (event) { when (event) {
ChooseAccountProviderEvents.Continue -> localCoroutineScope.launch { ChooseAccountProviderEvents.Continue -> {
selectedAccountProvider?.let { selectedAccountProvider?.let { provider ->
loginHelper.submit( loginModeState.eventSink(
isAccountCreation = false, LoginModeEvent.Submit(
homeserverUrl = it.url, isAccountCreation = false,
resolvedHomeserverUrl = null, homeserverUrl = provider.url,
loginHint = null, resolvedHomeserverUrl = null,
loginHint = null,
)
) )
} }
} }
is ChooseAccountProviderEvents.SelectAccountProvider -> { is ChooseAccountProviderEvents.SelectAccountProvider -> {
// Ensure that the user do not change the server during processing // Ensure that the user does not change the server during processing.
if (loginMode is AsyncData.Uninitialized) { if (loginModeState.loginMode is AsyncData.Uninitialized) {
selectedAccountProvider = event.accountProvider selectedAccountProvider = event.accountProvider
} }
} }
ChooseAccountProviderEvents.ClearError -> loginHelper.clearError() ChooseAccountProviderEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError)
} }
} }
@@ -77,7 +76,7 @@ class ChooseAccountProviderPresenter(
return ChooseAccountProviderState( return ChooseAccountProviderState(
accountProviders = staticAccountProviderList, accountProviders = staticAccountProviderList,
selectedAccountProvider = selectedAccountProvider, selectedAccountProvider = selectedAccountProvider,
loginMode = loginMode, loginModeState = loginModeState,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }
@@ -9,16 +9,17 @@
package io.element.android.features.login.impl.screens.chooseaccountprovider 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.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 io.element.android.libraries.architecture.AsyncData
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class ChooseAccountProviderState( data class ChooseAccountProviderState(
val accountProviders: ImmutableList<AccountProvider>, val accountProviders: ImmutableList<AccountProvider>,
val selectedAccountProvider: AccountProvider?, val selectedAccountProvider: AccountProvider?,
val loginMode: AsyncData<LoginMode>, val loginModeState: LoginModeState,
val eventSink: (ChooseAccountProviderEvents) -> Unit, val eventSink: (ChooseAccountProviderEvents) -> Unit,
) { ) {
val submitEnabled: Boolean 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)
} }
@@ -11,7 +11,8 @@ package io.element.android.features.login.impl.screens.chooseaccountprovider
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.features.login.impl.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 io.element.android.libraries.architecture.AsyncData
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -58,7 +59,7 @@ open class ChooseAccountProviderStateProvider : PreviewParameterProvider<ChooseA
server3, server3,
), ),
selectedAccountProvider = server2, selectedAccountProvider = server2,
loginMode = AsyncData.Loading(), loginModeState = aLoginModeState(loginMode = AsyncData.Loading()),
), ),
// Add other state here // Add other state here
) )
@@ -69,11 +70,11 @@ fun aChooseAccountProviderState(
anAccountProvider() anAccountProvider()
), ),
selectedAccountProvider: AccountProvider? = null, selectedAccountProvider: AccountProvider? = null,
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized, loginModeState: LoginModeState = aLoginModeState(),
eventSink: (ChooseAccountProviderEvents) -> Unit = {}, eventSink: (ChooseAccountProviderEvents) -> Unit = {},
) = ChooseAccountProviderState( ) = ChooseAccountProviderState(
accountProviders = accountProviders.toImmutableList(), accountProviders = accountProviders.toImmutableList(),
selectedAccountProvider = selectedAccountProvider, selectedAccountProvider = selectedAccountProvider,
loginMode = loginMode, loginModeState = loginModeState,
eventSink = eventSink, eventSink = eventSink,
) )
@@ -33,6 +33,8 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.accountprovider.AccountProviderView 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.features.login.impl.login.LoginModeView
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
@@ -56,9 +58,9 @@ fun ChooseAccountProviderView(
onCreateAccountContinue: (url: String) -> Unit, onCreateAccountContinue: (url: String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val isLoading by remember(state.loginMode) { val isLoading by remember(state.loginModeState.loginMode) {
derivedStateOf { derivedStateOf {
state.loginMode is AsyncData.Loading state.loginModeState.loginMode is AsyncData.Loading
} }
} }
@@ -124,7 +126,7 @@ fun ChooseAccountProviderView(
Spacer(modifier = Modifier.height(48.dp)) Spacer(modifier = Modifier.height(48.dp))
} }
LoginModeView( LoginModeView(
loginMode = state.loginMode, loginMode = state.loginModeState.loginMode,
onClearError = { onClearError = {
state.eventSink(ChooseAccountProviderEvents.ClearError) 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 @PreviewsDayNight
@@ -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.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.classic.ElementClassicConnection 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.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.AsyncAction
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
@@ -32,7 +33,7 @@ import kotlinx.coroutines.launch
class LoginWithClassicPresenter( class LoginWithClassicPresenter(
@Assisted private val userId: UserId, @Assisted private val userId: UserId,
@Assisted private val navigator: LoginWithClassicNavigator, @Assisted private val navigator: LoginWithClassicNavigator,
private val loginHelper: LoginHelper, private val loginModePresenter: Presenter<LoginModeState>,
private val elementClassicConnection: ElementClassicConnection, private val elementClassicConnection: ElementClassicConnection,
private val accountProviderDataSource: AccountProviderDataSource, private val accountProviderDataSource: AccountProviderDataSource,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
@@ -51,7 +52,7 @@ class LoginWithClassicPresenter(
var loginWithClassicAction by remember { var loginWithClassicAction by remember {
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
} }
val loginMode by loginHelper.collectLoginMode() val loginModeState = loginModePresenter.present()
val elementClassicConnectionState by elementClassicConnection.stateFlow.collectAsState() val elementClassicConnectionState by elementClassicConnection.stateFlow.collectAsState()
fun handleEvent(event: LoginWithClassicEvent) { fun handleEvent(event: LoginWithClassicEvent) {
@@ -69,11 +70,13 @@ class LoginWithClassicPresenter(
val elementClassicUserId = currentState.elementClassicSession.userId val elementClassicUserId = currentState.elementClassicSession.userId
val accountProvider = elementClassicUserId.domainName.orEmpty().ensureProtocol() val accountProvider = elementClassicUserId.domainName.orEmpty().ensureProtocol()
accountProviderDataSource.setUrl(accountProvider) accountProviderDataSource.setUrl(accountProvider)
loginHelper.submit( loginModeState.eventSink(
isAccountCreation = false, LoginModeEvent.Submit(
homeserverUrl = accountProvider, isAccountCreation = false,
resolvedHomeserverUrl = currentState.elementClassicSession.homeserverUrl, homeserverUrl = accountProvider,
loginHint = "mxid:" + elementClassicUserId.value, resolvedHomeserverUrl = currentState.elementClassicSession.homeserverUrl,
loginHint = "mxid:" + elementClassicUserId.value,
)
) )
} }
} }
@@ -83,7 +86,7 @@ class LoginWithClassicPresenter(
} }
LoginWithClassicEvent.ClearError -> { LoginWithClassicEvent.ClearError -> {
loginWithClassicAction = AsyncAction.Uninitialized loginWithClassicAction = AsyncAction.Uninitialized
loginHelper.clearError() loginModeState.eventSink(LoginModeEvent.ClearError)
} }
} }
} }
@@ -94,7 +97,7 @@ class LoginWithClassicPresenter(
userId = userId, userId = userId,
displayName = elementClassicReady?.displayName, displayName = elementClassicReady?.displayName,
avatar = elementClassicReady?.avatar, avatar = elementClassicReady?.avatar,
loginMode = loginMode, loginModeState = loginModeState,
loginWithClassicAction = loginWithClassicAction, loginWithClassicAction = loginWithClassicAction,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
@@ -9,9 +9,8 @@ package io.element.android.features.login.impl.screens.classic.loginwithclassic
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.compose.runtime.Stable 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.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@Stable @Stable
@@ -21,6 +20,6 @@ data class LoginWithClassicState(
val displayName: String?, val displayName: String?,
val avatar: Bitmap?, val avatar: Bitmap?,
val loginWithClassicAction: AsyncAction<Unit>, val loginWithClassicAction: AsyncAction<Unit>,
val loginMode: AsyncData<LoginMode>, val loginModeState: LoginModeState,
val eventSink: (LoginWithClassicEvent) -> Unit, val eventSink: (LoginWithClassicEvent) -> Unit,
) )
@@ -9,9 +9,9 @@ package io.element.android.features.login.impl.screens.classic.loginwithclassic
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.compose.ui.tooling.preview.PreviewParameterProvider 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.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@@ -29,7 +29,7 @@ fun aLoginWithClassicState(
displayName: String? = null, displayName: String? = null,
avatar: Bitmap? = null, avatar: Bitmap? = null,
loginWithClassicAction: AsyncAction<Unit> = AsyncAction.Uninitialized, loginWithClassicAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized, loginModeState: LoginModeState = aLoginModeState(),
eventSink: (LoginWithClassicEvent) -> Unit = {}, eventSink: (LoginWithClassicEvent) -> Unit = {},
) = LoginWithClassicState( ) = LoginWithClassicState(
isElementPro = isElementPro, isElementPro = isElementPro,
@@ -37,6 +37,6 @@ fun aLoginWithClassicState(
displayName = displayName, displayName = displayName,
avatar = avatar, avatar = avatar,
loginWithClassicAction = loginWithClassicAction, loginWithClassicAction = loginWithClassicAction,
loginMode = loginMode, loginModeState = loginModeState,
eventSink = eventSink, eventSink = eventSink,
) )
@@ -35,6 +35,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.login.impl.R 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.features.login.impl.login.LoginModeView
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
@@ -65,9 +67,9 @@ fun LoginWithClassicView(
onCreateAccountContinue: (url: String) -> Unit, onCreateAccountContinue: (url: String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val isLoading by remember(state.loginMode) { val isLoading by remember(state.loginModeState.loginMode) {
derivedStateOf { derivedStateOf {
state.loginMode is AsyncData.Loading state.loginModeState.loginMode is AsyncData.Loading
} }
} }
@@ -195,7 +197,7 @@ fun LoginWithClassicView(
} }
) )
LoginModeView( LoginModeView(
loginMode = state.loginMode, loginMode = state.loginModeState.loginMode,
onClearError = { onClearError = {
state.eventSink(LoginWithClassicEvent.ClearError) state.eventSink(LoginWithClassicEvent.ClearError)
}, },
@@ -204,6 +206,15 @@ fun LoginWithClassicView(
onNeedLoginPassword = onNeedLoginPassword, onNeedLoginPassword = onNeedLoginPassword,
onCreateAccountContinue = onCreateAccountContinue, onCreateAccountContinue = onCreateAccountContinue,
) )
LocalNetworkPermissionDialogView(
dialog = state.loginModeState.localNetworkPermissionDialog,
onSubmit = {
state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission)
},
onDismiss = {
state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission)
}
)
} }
@PreviewsDayNight @PreviewsDayNight
@@ -11,20 +11,19 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.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 io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.launch
@AssistedInject @AssistedInject
class ConfirmAccountProviderPresenter( class ConfirmAccountProviderPresenter(
@Assisted private val params: Params, @Assisted private val params: Params,
private val accountProviderDataSource: AccountProviderDataSource, private val accountProviderDataSource: AccountProviderDataSource,
private val loginHelper: LoginHelper, private val loginModePresenter: Presenter<LoginModeState>,
) : Presenter<ConfirmAccountProviderState> { ) : Presenter<ConfirmAccountProviderState> {
data class Params( data class Params(
val isAccountCreation: Boolean, val isAccountCreation: Boolean,
@@ -38,28 +37,26 @@ class ConfirmAccountProviderPresenter(
@Composable @Composable
override fun present(): ConfirmAccountProviderState { override fun present(): ConfirmAccountProviderState {
val accountProvider by accountProviderDataSource.flow.collectAsState() val accountProvider by accountProviderDataSource.flow.collectAsState()
val localCoroutineScope = rememberCoroutineScope() val loginModeState = loginModePresenter.present()
val loginMode by loginHelper.collectLoginMode()
fun handleEvent(event: ConfirmAccountProviderEvents) { fun handleEvent(event: ConfirmAccountProviderEvents) {
when (event) { when (event) {
ConfirmAccountProviderEvents.Continue -> localCoroutineScope.launch { ConfirmAccountProviderEvents.Continue -> loginModeState.eventSink(
loginHelper.submit( LoginModeEvent.Submit(
isAccountCreation = params.isAccountCreation, isAccountCreation = params.isAccountCreation,
homeserverUrl = accountProvider.url, homeserverUrl = accountProvider.url,
resolvedHomeserverUrl = null, resolvedHomeserverUrl = null,
loginHint = null, loginHint = null,
) )
} )
ConfirmAccountProviderEvents.ClearError -> loginHelper.clearError() ConfirmAccountProviderEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError)
} }
} }
return ConfirmAccountProviderState( return ConfirmAccountProviderState(
accountProvider = accountProvider, accountProvider = accountProvider,
isAccountCreation = params.isAccountCreation, isAccountCreation = params.isAccountCreation,
loginMode = loginMode, loginModeState = loginModeState,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }
@@ -9,14 +9,16 @@
package io.element.android.features.login.impl.screens.confirmaccountprovider 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.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 io.element.android.libraries.architecture.AsyncData
data class ConfirmAccountProviderState( data class ConfirmAccountProviderState(
val accountProvider: AccountProvider, val accountProvider: AccountProvider,
val isAccountCreation: Boolean, val isAccountCreation: Boolean,
val loginMode: AsyncData<LoginMode>, val loginModeState: LoginModeState,
val eventSink: (ConfirmAccountProviderEvents) -> Unit 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)
} }
@@ -11,7 +11,8 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.features.login.impl.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.features.login.impl.screens.createaccount.AccountCreationNotSupported
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@@ -24,7 +25,7 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<Confir
), ),
aConfirmAccountProviderState( aConfirmAccountProviderState(
isAccountCreation = true, isAccountCreation = true,
loginMode = AsyncData.Failure(AccountCreationNotSupported()) loginModeState = aLoginModeState(loginMode = AsyncData.Failure(AccountCreationNotSupported())),
), ),
) )
} }
@@ -32,11 +33,11 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<Confir
private fun aConfirmAccountProviderState( private fun aConfirmAccountProviderState(
accountProvider: AccountProvider = anAccountProvider(), accountProvider: AccountProvider = anAccountProvider(),
isAccountCreation: Boolean = false, isAccountCreation: Boolean = false,
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized, loginModeState: LoginModeState = aLoginModeState(),
eventSink: (ConfirmAccountProviderEvents) -> Unit = {}, eventSink: (ConfirmAccountProviderEvents) -> Unit = {},
) = ConfirmAccountProviderState( ) = ConfirmAccountProviderState(
accountProvider = accountProvider, accountProvider = accountProvider,
isAccountCreation = isAccountCreation, isAccountCreation = isAccountCreation,
loginMode = loginMode, loginModeState = loginModeState,
eventSink = eventSink eventSink = eventSink
) )
@@ -20,6 +20,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView
import io.element.android.features.login.impl.login.LoginModeEvent
import io.element.android.features.login.impl.login.LoginModeView import io.element.android.features.login.impl.login.LoginModeView
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
@@ -45,9 +47,9 @@ fun ConfirmAccountProviderView(
onChange: () -> Unit, onChange: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val isLoading by remember(state.loginMode) { val isLoading by remember(state.loginModeState.loginMode) {
derivedStateOf { derivedStateOf {
state.loginMode is AsyncData.Loading state.loginModeState.loginMode is AsyncData.Loading
} }
} }
val eventSink = state.eventSink val eventSink = state.eventSink
@@ -98,7 +100,7 @@ fun ConfirmAccountProviderView(
} }
) { ) {
LoginModeView( LoginModeView(
loginMode = state.loginMode, loginMode = state.loginModeState.loginMode,
onClearError = { onClearError = {
eventSink(ConfirmAccountProviderEvents.ClearError) eventSink(ConfirmAccountProviderEvents.ClearError)
}, },
@@ -108,6 +110,16 @@ fun ConfirmAccountProviderView(
onCreateAccountContinue = onCreateAccountContinue, onCreateAccountContinue = onCreateAccountContinue,
) )
} }
LocalNetworkPermissionDialogView(
dialog = state.loginModeState.localNetworkPermissionDialog,
onSubmit = {
state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission)
},
onDismiss = {
state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission)
}
)
} }
@PreviewsDayNight @PreviewsDayNight
@@ -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.enterprise.api.canConnectToAnyHomeserver
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.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.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
@@ -41,7 +42,7 @@ class OnBoardingPresenter(
private val enterpriseService: EnterpriseService, private val enterpriseService: EnterpriseService,
private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl, private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val loginHelper: LoginHelper, private val loginModePresenter: Presenter<LoginModeState>,
private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider, private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider,
private val sessionStore: SessionStore, private val sessionStore: SessionStore,
private val accountProviderDataSource: AccountProviderDataSource, private val accountProviderDataSource: AccountProviderDataSource,
@@ -98,21 +99,23 @@ class OnBoardingPresenter(
value = sessionStore.numberOfSessions() > 0 value = sessionStore.numberOfSessions() > 0
} }
val loginMode by loginHelper.collectLoginMode() val loginModeState = loginModePresenter.present()
fun handleEvent(event: OnBoardingEvents) { fun handleEvent(event: OnBoardingEvents) {
when (event) { when (event) {
is OnBoardingEvents.OnSignIn -> localCoroutineScope.launch { is OnBoardingEvents.OnSignIn -> localCoroutineScope.launch {
// Ensure that the current account provider is set // Ensure that the current account provider is set
accountProviderDataSource.setUrl(event.defaultAccountProvider) accountProviderDataSource.setUrl(event.defaultAccountProvider)
loginHelper.submit( loginModeState.eventSink(
isAccountCreation = false, LoginModeEvent.Submit(
homeserverUrl = event.defaultAccountProvider, isAccountCreation = false,
resolvedHomeserverUrl = null, homeserverUrl = event.defaultAccountProvider,
loginHint = params.loginHint?.takeIf { forcedAccountProvider == null }, resolvedHomeserverUrl = null,
loginHint = params.loginHint?.takeIf { forcedAccountProvider == null },
)
) )
} }
OnBoardingEvents.ClearError -> loginHelper.clearError() OnBoardingEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError)
OnBoardingEvents.OnVersionClick -> { OnBoardingEvents.OnVersionClick -> {
if (canReportBug) { if (canReportBug) {
if (multipleTapToUnlock.unlock(localCoroutineScope)) { if (multipleTapToUnlock.unlock(localCoroutineScope)) {
@@ -133,7 +136,7 @@ class OnBoardingPresenter(
canLoginWithQrCode = canLoginWithQrCode, canLoginWithQrCode = canLoginWithQrCode,
canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT, canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT,
canReportBug = canReportBug && showReportBug, canReportBug = canReportBug && showReportBug,
loginMode = loginMode, loginModeState = loginModeState,
version = buildMeta.versionName, version = buildMeta.versionName,
onBoardingLogoResId = onBoardingLogoResId, onBoardingLogoResId = onBoardingLogoResId,
eventSink = ::handleEvent, eventSink = ::handleEvent,
@@ -9,7 +9,7 @@
package io.element.android.features.login.impl.screens.onboarding package io.element.android.features.login.impl.screens.onboarding
import androidx.annotation.DrawableRes 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 import io.element.android.libraries.architecture.AsyncData
data class OnBoardingState( data class OnBoardingState(
@@ -25,9 +25,10 @@ data class OnBoardingState(
val version: String, val version: String,
@DrawableRes @DrawableRes
val onBoardingLogoResId: Int?, val onBoardingLogoResId: Int?,
val loginMode: AsyncData<LoginMode>, val loginModeState: LoginModeState,
val eventSink: (OnBoardingEvents) -> Unit, val eventSink: (OnBoardingEvents) -> Unit,
) { ) {
val submitEnabled: Boolean 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)
} }
@@ -10,8 +10,8 @@ package io.element.android.features.login.impl.screens.onboarding
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.compose.ui.tooling.preview.PreviewParameterProvider 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.libraries.architecture.AsyncData import io.element.android.features.login.impl.login.aLoginModeState
import io.element.android.libraries.designsystem.R import io.element.android.libraries.designsystem.R
open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> { open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
@@ -49,7 +49,7 @@ fun anOnBoardingState(
version: String = "1.0.0", version: String = "1.0.0",
@DrawableRes @DrawableRes
customLogoResId: Int? = null, customLogoResId: Int? = null,
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized, loginModeState: LoginModeState = aLoginModeState(),
eventSink: (OnBoardingEvents) -> Unit = {}, eventSink: (OnBoardingEvents) -> Unit = {},
) = OnBoardingState( ) = OnBoardingState(
isAddingAccount = isAddingAccount, isAddingAccount = isAddingAccount,
@@ -62,7 +62,7 @@ fun anOnBoardingState(
canCreateAccount = canCreateAccount, canCreateAccount = canCreateAccount,
canReportBug = canReportBug, canReportBug = canReportBug,
version = version, version = version,
loginMode = loginMode, loginModeState = loginModeState,
onBoardingLogoResId = customLogoResId, onBoardingLogoResId = customLogoResId,
eventSink = eventSink, eventSink = eventSink,
) )
@@ -33,6 +33,8 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView
import io.element.android.features.login.impl.login.LoginModeEvent
import io.element.android.features.login.impl.login.LoginModeView import io.element.android.features.login.impl.login.LoginModeView
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom
@@ -74,7 +76,7 @@ fun OnBoardingView(
) { ) {
val loginView = @Composable { val loginView = @Composable {
LoginModeView( LoginModeView(
loginMode = state.loginMode, loginMode = state.loginModeState.loginMode,
onClearError = { onClearError = {
state.eventSink(OnBoardingEvents.ClearError) state.eventSink(OnBoardingEvents.ClearError)
}, },
@@ -83,6 +85,15 @@ fun OnBoardingView(
onNeedLoginPassword = onNeedLoginPassword, onNeedLoginPassword = onNeedLoginPassword,
onCreateAccountContinue = onCreateAccountContinue, onCreateAccountContinue = onCreateAccountContinue,
) )
LocalNetworkPermissionDialogView(
dialog = state.loginModeState.localNetworkPermissionDialog,
onSubmit = {
state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission)
},
onDismiss = {
state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission)
}
)
} }
val buttons = @Composable { val buttons = @Composable {
OnBoardingButtons( OnBoardingButtons(
@@ -260,9 +271,9 @@ private fun OnBoardingButtons(
onCreateAccount: () -> Unit, onCreateAccount: () -> Unit,
onReportProblem: () -> Unit, onReportProblem: () -> Unit,
) { ) {
val isLoading by remember(state.loginMode) { val isLoading by remember(state.loginModeState.loginMode) {
derivedStateOf { derivedStateOf {
state.loginMode is AsyncData.Loading state.loginModeState.loginMode is AsyncData.Loading
} }
} }
@@ -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.api.EnterpriseService
import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.login.LoginHelper import io.element.android.features.login.impl.login.LoginModePresenter
import io.element.android.features.login.impl.screens.onboarding.createLoginHelper import io.element.android.features.login.impl.screens.onboarding.createLoginModePresenter
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2 import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
@@ -81,7 +81,7 @@ class ChooseAccountProviderPresenterTest {
enterpriseService = FakeEnterpriseService( enterpriseService = FakeEnterpriseService(
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) },
), ),
loginHelper = createLoginHelper( loginModePresenter = createLoginModePresenter(
authenticationService = authenticationService, authenticationService = authenticationService,
), ),
) )
@@ -105,7 +105,7 @@ class ChooseAccountProviderPresenterTest {
enterpriseService = FakeEnterpriseService( enterpriseService = FakeEnterpriseService(
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) },
), ),
loginHelper = createLoginHelper( loginModePresenter = createLoginModePresenter(
authenticationService = authenticationService, authenticationService = authenticationService,
), ),
) )
@@ -121,12 +121,12 @@ class ChooseAccountProviderPresenterTest {
// Check an error was returned // Check an error was returned
val submittedState = awaitItem() 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 // Assert the error is then cleared
submittedState.eventSink(ChooseAccountProviderEvents.ClearError) submittedState.eventSink(ChooseAccountProviderEvents.ClearError)
val clearedState = awaitItem() val clearedState = awaitItem()
assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized)
} }
} }
} }
@@ -138,7 +138,7 @@ class ChooseAccountProviderPresenterTest {
enterpriseService = FakeEnterpriseService( enterpriseService = FakeEnterpriseService(
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) },
), ),
loginHelper = createLoginHelper( loginModePresenter = createLoginModePresenter(
authenticationService = authenticationService, authenticationService = authenticationService,
), ),
) )
@@ -152,7 +152,7 @@ class ChooseAccountProviderPresenterTest {
it.eventSink(ChooseAccountProviderEvents.Continue) it.eventSink(ChooseAccountProviderEvents.Continue)
} }
awaitItem().also { awaitItem().also {
assertThat(it.loginMode.isLoading()).isTrue() assertThat(it.loginModeState.loginMode.isLoading()).isTrue()
it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider2)) it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider2))
} }
expectNoEvents() expectNoEvents()
@@ -162,8 +162,8 @@ class ChooseAccountProviderPresenterTest {
private fun createPresenter( private fun createPresenter(
enterpriseService: EnterpriseService = FakeEnterpriseService(), enterpriseService: EnterpriseService = FakeEnterpriseService(),
loginHelper: LoginHelper = createLoginHelper(), loginModePresenter: LoginModePresenter = createLoginModePresenter(),
) = ChooseAccountProviderPresenter( ) = ChooseAccountProviderPresenter(
enterpriseService = enterpriseService, enterpriseService = enterpriseService,
loginHelper = loginHelper, loginModePresenter = loginModePresenter,
) )
@@ -11,6 +11,7 @@ package io.element.android.features.login.impl.screens.chooseaccountprovider
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.features.login.impl.login.LoginMode 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.architecture.AsyncData
import org.junit.Test import org.junit.Test
@@ -35,7 +36,7 @@ class ChooseAccountProviderStateTest {
fun `submitEnabled returns false when there is a selectedAccountProvider but there is an error`() { fun `submitEnabled returns false when there is a selectedAccountProvider but there is an error`() {
val sut = aChooseAccountProviderState( val sut = aChooseAccountProviderState(
selectedAccountProvider = anAccountProvider(), selectedAccountProvider = anAccountProvider(),
loginMode = AsyncData.Failure(Throwable("Error")), loginModeState = aLoginModeState(loginMode = AsyncData.Failure(Throwable("Error"))),
) )
assertThat(sut.submitEnabled).isFalse() assertThat(sut.submitEnabled).isFalse()
} }
@@ -44,7 +45,7 @@ class ChooseAccountProviderStateTest {
fun `submitEnabled returns false when there is a selectedAccountProvider but the result is successful`() { fun `submitEnabled returns false when there is a selectedAccountProvider but the result is successful`() {
val sut = aChooseAccountProviderState( val sut = aChooseAccountProviderState(
selectedAccountProvider = anAccountProvider(), selectedAccountProvider = anAccountProvider(),
loginMode = AsyncData.Success(LoginMode.PasswordLogin), loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.PasswordLogin)),
) )
assertThat(sut.submitEnabled).isFalse() assertThat(sut.submitEnabled).isFalse()
} }
@@ -17,6 +17,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.features.login.impl.login.aLoginModeState
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
@@ -69,7 +70,7 @@ class ChooseAccountProviderViewTest : RobolectricTest() {
val eventSink = EventsRecorder<ChooseAccountProviderEvents>() val eventSink = EventsRecorder<ChooseAccountProviderEvents>()
setChooseAccountProviderView( setChooseAccountProviderView(
state = aChooseAccountProviderState( state = aChooseAccountProviderState(
loginMode = AsyncData.Failure(AN_EXCEPTION), loginModeState = aLoginModeState(loginMode = AsyncData.Failure(AN_EXCEPTION)),
eventSink = eventSink, eventSink = eventSink,
), ),
) )
@@ -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.ROOM_KEYS_VERSION
import io.element.android.features.login.impl.classic.anElementClassicReady 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.classic.anElementClassicSession
import io.element.android.features.login.impl.login.LoginHelper import io.element.android.features.login.impl.login.LoginModePresenter
import io.element.android.features.login.impl.screens.onboarding.createLoginHelper 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.api.core.UserId
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_FAILURE_REASON import io.element.android.libraries.matrix.test.A_FAILURE_REASON
@@ -42,7 +42,7 @@ class LoginWithClassicPresenterTest {
assertThat(initialState.displayName).isNull() assertThat(initialState.displayName).isNull()
assertThat(initialState.avatar).isNull() assertThat(initialState.avatar).isNull()
assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue() 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( val presenter = createPresenter(
elementClassicConnection = elementClassicConnection, elementClassicConnection = elementClassicConnection,
loginHelper = createLoginHelper( loginModePresenter = createLoginModePresenter(
authenticationService = authenticationService, authenticationService = authenticationService,
), ),
) )
@@ -108,7 +108,7 @@ class LoginWithClassicPresenterTest {
) )
val presenter = createPresenter( val presenter = createPresenter(
elementClassicConnection = elementClassicConnection, elementClassicConnection = elementClassicConnection,
loginHelper = createLoginHelper( loginModePresenter = createLoginModePresenter(
authenticationService = authenticationService, authenticationService = authenticationService,
), ),
) )
@@ -147,7 +147,7 @@ class LoginWithClassicPresenterTest {
val navigateToMissingKeyBackupResult = lambdaRecorder<Unit> { } val navigateToMissingKeyBackupResult = lambdaRecorder<Unit> { }
val presenter = createPresenter( val presenter = createPresenter(
elementClassicConnection = elementClassicConnection, elementClassicConnection = elementClassicConnection,
loginHelper = createLoginHelper( loginModePresenter = createLoginModePresenter(
authenticationService = authenticationService, authenticationService = authenticationService,
), ),
navigator = FakeLoginWithClassicNavigator( navigator = FakeLoginWithClassicNavigator(
@@ -188,7 +188,7 @@ class LoginWithClassicPresenterTest {
val navigateToMissingKeyBackupResult = lambdaRecorder<Unit> { } val navigateToMissingKeyBackupResult = lambdaRecorder<Unit> { }
val presenter = createPresenter( val presenter = createPresenter(
elementClassicConnection = elementClassicConnection, elementClassicConnection = elementClassicConnection,
loginHelper = createLoginHelper( loginModePresenter = createLoginModePresenter(
authenticationService = authenticationService, authenticationService = authenticationService,
), ),
navigator = FakeLoginWithClassicNavigator( navigator = FakeLoginWithClassicNavigator(
@@ -247,14 +247,14 @@ class LoginWithClassicPresenterTest {
private fun createPresenter( private fun createPresenter(
userId: UserId = A_USER_ID, userId: UserId = A_USER_ID,
navigator: LoginWithClassicNavigator = FakeLoginWithClassicNavigator(), navigator: LoginWithClassicNavigator = FakeLoginWithClassicNavigator(),
loginHelper: LoginHelper = createLoginHelper(), loginModePresenter: LoginModePresenter = createLoginModePresenter(),
elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(), elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(),
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
isEnterpriseBuild: Boolean = false, isEnterpriseBuild: Boolean = false,
) = LoginWithClassicPresenter( ) = LoginWithClassicPresenter(
userId = userId, userId = userId,
navigator = navigator, navigator = navigator,
loginHelper = loginHelper, loginModePresenter = loginModePresenter,
elementClassicConnection = elementClassicConnection, elementClassicConnection = elementClassicConnection,
accountProviderDataSource = accountProviderDataSource, accountProviderDataSource = accountProviderDataSource,
buildMeta = aBuildMeta( buildMeta = aBuildMeta(
@@ -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.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.login.LoginMode 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.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.FakeWebClientUrlForAuthenticationRetriever
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@@ -43,7 +43,7 @@ class ConfirmAccountProviderPresenterTest {
assertThat(initialState.isAccountCreation).isFalse() assertThat(initialState.isAccountCreation).isFalse()
assertThat(initialState.submitEnabled).isTrue() assertThat(initialState.submitEnabled).isTrue()
assertThat(initialState.accountProvider.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL) 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) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginMode.dataOrNull()).isEqualTo(LoginMode.PasswordLogin) assertThat(successState.loginModeState.loginMode.dataOrNull()).isEqualTo(LoginMode.PasswordLogin)
} }
} }
@@ -85,11 +85,11 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
} }
} }
@@ -110,15 +110,15 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
authenticationService.givenOAuthCancelError(AN_EXCEPTION) authenticationService.givenOAuthCancelError(AN_EXCEPTION)
defaultOAuthActionFlow.post(OAuthAction.GoBack()) defaultOAuthActionFlow.post(OAuthAction.GoBack())
val cancelFailureState = awaitItem() 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) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
defaultOAuthActionFlow.post(OAuthAction.GoBack()) defaultOAuthActionFlow.post(OAuthAction.GoBack())
val cancelFinalState = awaitItem() 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) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() 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)) defaultOAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true))
val cancelFinalState = awaitItem() 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) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
authenticationService.givenLoginError(AN_EXCEPTION) authenticationService.givenLoginError(AN_EXCEPTION)
defaultOAuthActionFlow.post(OAuthAction.Success("aUrl")) defaultOAuthActionFlow.post(OAuthAction.Success("aUrl"))
val cancelLoadingState = awaitItem() val cancelLoadingState = awaitItem()
assertThat(cancelLoadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) assertThat(cancelLoadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
val cancelFailureState = awaitItem() 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) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
defaultOidcActionFlow.post(OAuthAction.Success("aUrl")) defaultOidcActionFlow.post(OAuthAction.Success("aUrl"))
val successSuccessState = awaitItem() 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 skipItems(1) // Loading
val failureState = awaitItem() val failureState = awaitItem()
assertThat(failureState.submitEnabled).isFalse() 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 // Check an error was returned
val submittedState = awaitItem() 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 // Assert the error is then cleared
submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) submittedState.eventSink(ConfirmAccountProviderEvents.ClearError)
val clearedState = awaitItem() 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 skipItems(1) // Loading
// Check an error was returned // Check an error was returned
val submittedState = awaitItem() 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 // Assert the error is then cleared
submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) submittedState.eventSink(ConfirmAccountProviderEvents.ClearError)
val clearedState = awaitItem() 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) initialState.eventSink(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading skipItems(1) // Loading
val submittedState = awaitItem() val submittedState = awaitItem()
assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java) assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
} }
} }
@@ -349,8 +349,8 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink(ConfirmAccountProviderEvents.Continue) initialState.eventSink(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading skipItems(1) // Loading
val submittedState = awaitItem() val submittedState = awaitItem()
assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java) assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
} }
} }
@@ -372,7 +372,7 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink(ConfirmAccountProviderEvents.Continue) initialState.eventSink(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading skipItems(1) // Loading
val submittedState = awaitItem() 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( ) = ConfirmAccountProviderPresenter(
params = params, params = params,
accountProviderDataSource = accountProviderDataSource, accountProviderDataSource = accountProviderDataSource,
loginHelper = createLoginHelper( loginModePresenter = createLoginModePresenter(
authenticationService = matrixAuthenticationService, authenticationService = matrixAuthenticationService,
oAuthActionFlow = defaultOAuthActionFlow, oAuthActionFlow = defaultOAuthActionFlow,
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever, webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever,
@@ -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.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.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.FakeWebClientUrlForAuthenticationRetriever
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
import io.element.android.features.wellknown.test.FakeWellknownRetriever import io.element.android.features.wellknown.test.FakeWellknownRetriever
@@ -255,7 +256,7 @@ class OnBoardingPresenterTest {
enterpriseService = FakeEnterpriseService( enterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = { true }, isAllowedToConnectToHomeserverResult = { true },
), ),
loginHelper = createLoginHelper( loginModePresenter = createLoginModePresenter(
authenticationService = authenticationService, authenticationService = authenticationService,
), ),
accountProviderDataSource = accountProviderDataSource, accountProviderDataSource = accountProviderDataSource,
@@ -271,12 +272,12 @@ class OnBoardingPresenterTest {
assertThat(accountProviderDataSource.flow.first().url).isEqualTo(A_HOMESERVER_URL_2) assertThat(accountProviderDataSource.flow.first().url).isEqualTo(A_HOMESERVER_URL_2)
// Check an error was returned // Check an error was returned
val submittedState = awaitItem() 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 // Assert the error is then cleared
submittedState.eventSink(OnBoardingEvents.ClearError) submittedState.eventSink(OnBoardingEvents.ClearError)
val clearedState = awaitItem() 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(), enterpriseService: EnterpriseService = FakeEnterpriseService(),
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(), wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
rageshakeFeatureAvailability: () -> Flow<Boolean> = { flowOf(true) }, rageshakeFeatureAvailability: () -> Flow<Boolean> = { flowOf(true) },
loginHelper: LoginHelper = createLoginHelper(), loginModePresenter: LoginModePresenter = createLoginModePresenter(),
onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null }, onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null },
sessionStore: SessionStore = InMemorySessionStore(), sessionStore: SessionStore = InMemorySessionStore(),
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
@@ -305,18 +306,26 @@ private fun createPresenter(
wellknownRetriever = wellknownRetriever, wellknownRetriever = wellknownRetriever,
), ),
rageshakeFeatureAvailability = rageshakeFeatureAvailability, rageshakeFeatureAvailability = rageshakeFeatureAvailability,
loginHelper = loginHelper, loginModePresenter = loginModePresenter,
onBoardingLogoResIdProvider = onBoardingLogoResIdProvider, onBoardingLogoResIdProvider = onBoardingLogoResIdProvider,
sessionStore = sessionStore, sessionStore = sessionStore,
accountProviderDataSource = accountProviderDataSource, accountProviderDataSource = accountProviderDataSource,
) )
fun createLoginHelper( fun createLoginModePresenter(
oAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(), oAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(),
authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), 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, oAuthActionFlow = oAuthActionFlow,
authenticationService = authenticationService, authenticationService = authenticationService,
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever, webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever,
localNetworkPermissionGate = LocalNetworkPermissionGate(
advisor = localNetworkPermissionAdvisor,
permissionsPresenterFactory = permissionsPresenterFactory,
),
) )
@@ -21,6 +21,7 @@ import com.google.testing.junit.testparameterinjector.KotlinTestParameters.named
import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameter
import io.element.android.features.login.impl.R 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.LoginMode
import io.element.android.features.login.impl.login.aLoginModeState
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
@@ -148,7 +149,7 @@ class OnboardingViewTest : RobolectricTestParameter() {
setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
defaultAccountProvider = "element.io", defaultAccountProvider = "element.io",
loginMode = AsyncData.Failure(AN_EXCEPTION), loginModeState = aLoginModeState(loginMode = AsyncData.Failure(AN_EXCEPTION)),
eventSink = eventSink, eventSink = eventSink,
), ),
) )
@@ -208,7 +209,7 @@ class OnboardingViewTest : RobolectricTestParameter() {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
loginMode = AsyncData.Success(LoginMode.PasswordLogin), loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.PasswordLogin)),
eventSink = eventSink, eventSink = eventSink,
), ),
onNeedLoginPassword = callback, onNeedLoginPassword = callback,
@@ -224,7 +225,7 @@ class OnboardingViewTest : RobolectricTestParameter() {
ensureCalledOnceWithParam(oAuthDetails) { callback -> ensureCalledOnceWithParam(oAuthDetails) { callback ->
setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails)), loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails))),
eventSink = eventSink, eventSink = eventSink,
), ),
onOAuthDetails = callback, onOAuthDetails = callback,
@@ -240,7 +241,7 @@ class OnboardingViewTest : RobolectricTestParameter() {
ensureCalledOnceWithParam(oAuthDetails.url) { callback -> ensureCalledOnceWithParam(oAuthDetails.url) { callback ->
setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl")), loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl"))),
eventSink = eventSink, eventSink = eventSink,
), ),
onCreateAccountContinue = callback, onCreateAccountContinue = callback,
@@ -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.features.login.impl.resolver.HomeserverResolver
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatibilityChecker 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.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
@@ -30,7 +31,7 @@ class SearchAccountProviderPresenterTest {
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(true) }) val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(true) })
val presenter = SearchAccountProviderPresenter( val presenter = SearchAccountProviderPresenter(
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()),
changeServerPresenter = { aChangeServerState() } changeServerPresenter = { aChangeServerState() }
) )
presenter.test { presenter.test {
@@ -44,7 +45,7 @@ class SearchAccountProviderPresenterTest {
fun `present - error while checking login compatibility`() = runTest { fun `present - error while checking login compatibility`() = runTest {
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.failure(IllegalStateException("Oops")) }) val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.failure(IllegalStateException("Oops")) })
val presenter = SearchAccountProviderPresenter( val presenter = SearchAccountProviderPresenter(
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()),
changeServerPresenter = { aChangeServerState() } changeServerPresenter = { aChangeServerState() }
) )
presenter.test { presenter.test {
@@ -68,7 +69,7 @@ class SearchAccountProviderPresenterTest {
fun `present - enter text no result`() = runTest { fun `present - enter text no result`() = runTest {
val fakeWellknownRetriever = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(false) }) val fakeWellknownRetriever = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(false) })
val presenter = SearchAccountProviderPresenter( val presenter = SearchAccountProviderPresenter(
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever), homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever, FakeLocalNetworkPermissionAdvisor()),
changeServerPresenter = { aChangeServerState() } changeServerPresenter = { aChangeServerState() }
) )
presenter.test { presenter.test {
@@ -101,7 +102,7 @@ class SearchAccountProviderPresenterTest {
} }
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult) val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult)
val presenter = SearchAccountProviderPresenter( val presenter = SearchAccountProviderPresenter(
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()),
changeServerPresenter = { aChangeServerState() } changeServerPresenter = { aChangeServerState() }
) )
presenter.test { presenter.test {
@@ -141,7 +142,7 @@ class SearchAccountProviderPresenterTest {
} }
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult) val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult)
val presenter = SearchAccountProviderPresenter( val presenter = SearchAccountProviderPresenter(
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()),
changeServerPresenter = { aChangeServerState() } changeServerPresenter = { aChangeServerState() }
) )
presenter.test { presenter.test {