diff --git a/enterprise b/enterprise index 0299b8ec4f..c754703e72 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit 0299b8ec4f4233a39230d4c35b97f89728c35fd1 +Subproject commit c754703e720bcb20f5dfa1ea0dcd32976825f71c diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt index 9c1cb8b981..b5ade48e01 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt @@ -14,6 +14,7 @@ interface EnterpriseService { val isEnterpriseBuild: Boolean suspend fun isEnterpriseUser(sessionId: SessionId): Boolean fun defaultHomeserver(): String? + suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean fun semanticColorsLight(): SemanticColors fun semanticColorsDark(): SemanticColors diff --git a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt b/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt index 2d66bb3968..97ab03361c 100644 --- a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt +++ b/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt @@ -23,6 +23,7 @@ class DefaultEnterpriseService @Inject constructor() : EnterpriseService { override suspend fun isEnterpriseUser(sessionId: SessionId) = false override fun defaultHomeserver() = null + override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true override fun semanticColorsLight(): SemanticColors = compoundColorsLight diff --git a/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt b/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt index 0d5593dab9..0d9dc1cafa 100644 --- a/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt +++ b/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt @@ -8,6 +8,7 @@ package io.element.android.features.enterprise.impl import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_SESSION_ID import kotlinx.coroutines.test.runTest import org.junit.Test @@ -25,6 +26,12 @@ class DefaultEnterpriseServiceTest { assertThat(defaultEnterpriseService.defaultHomeserver()).isNull() } + @Test + fun `isAllowedToConnectToHomeserver is true for all homeserver urls`() = runTest { + val defaultEnterpriseService = DefaultEnterpriseService() + assertThat(defaultEnterpriseService.isAllowedToConnectToHomeserver(A_HOMESERVER_URL)).isTrue() + } + @Test fun `isEnterpriseUser always return false`() = runTest { val defaultEnterpriseService = DefaultEnterpriseService() diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt index ae71bf6b76..52b49f4716 100644 --- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt @@ -17,6 +17,7 @@ class FakeEnterpriseService( override val isEnterpriseBuild: Boolean = false, private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() }, private val defaultHomeserverResult: () -> String? = { A_FAKE_HOMESERVER }, + private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() }, private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() }, private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() }, private val firebasePushGatewayResult: () -> String? = { lambdaError() }, @@ -30,6 +31,10 @@ class FakeEnterpriseService( return defaultHomeserverResult() } + override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean = simulateLongTask { + isAllowedToConnectToHomeserverResult(homeserverUrl) + } + override fun semanticColorsLight(): SemanticColors { return semanticColorsLightResult() } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt index 9806c792ad..497bf96271 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +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.AccountProviderDataSource import io.element.android.features.login.impl.error.ChangeServerError @@ -26,6 +27,7 @@ import javax.inject.Inject class ChangeServerPresenter @Inject constructor( private val authenticationService: MatrixAuthenticationService, private val accountProviderDataSource: AccountProviderDataSource, + private val enterpriseService: EnterpriseService, ) : Presenter { @Composable override fun present(): ChangeServerState { @@ -53,6 +55,9 @@ class ChangeServerPresenter @Inject constructor( changeServerAction: MutableState>, ) = launch { suspend { + if (enterpriseService.isAllowedToConnectToHomeserver(data.url).not()) { + throw UnauthorizedAccountProviderException(data) + } authenticationService.setHomeserver(data.url).map { authenticationService.getHomeserverDetails().value!! // Valid, remember user choice diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt index 8ed40ccedc..ae9ef58252 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.features.login.impl.changeserver import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.ui.strings.CommonStrings @@ -18,6 +19,7 @@ open class ChangeServerStateProvider : PreviewParameterProvider { - when (val error = state.changeServerAction.error) { + when (val error = state.changeServerAction.error as? ChangeServerError) { is ChangeServerError.Error -> { ErrorDialog( modifier = modifier, @@ -53,6 +56,20 @@ fun ChangeServerView( } ) } + is ChangeServerError.UnauthorizedAccountProvider -> { + ErrorDialog( + modifier = modifier, + content = stringResource( + id = R.string.screen_change_server_error_unauthorized_homeserver, + LocalBuildMeta.current.applicationName, + error.accountProvider.title, + ), + onSubmit = { + eventSink.invoke(ChangeServerEvents.ClearError) + } + ) + } + null -> Unit } } is AsyncData.Loading -> ProgressDialog() diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/UnauthorizedAccountProviderException.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/UnauthorizedAccountProviderException.kt new file mode 100644 index 0000000000..9575031b91 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/UnauthorizedAccountProviderException.kt @@ -0,0 +1,14 @@ +/* + * 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.changeserver + +import io.element.android.features.login.impl.accountprovider.AccountProvider + +class UnauthorizedAccountProviderException( + val accountProvider: AccountProvider, +) : Exception() diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt index 618f4f81f6..69c0121c15 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt @@ -11,6 +11,8 @@ import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import io.element.android.features.login.impl.R +import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException import io.element.android.libraries.matrix.api.auth.AuthenticationException import io.element.android.libraries.ui.strings.CommonStrings @@ -23,12 +25,17 @@ sealed class ChangeServerError : Throwable() { fun message(): String = messageStr ?: stringResource(messageId ?: CommonStrings.error_unknown) } + data class UnauthorizedAccountProvider( + val accountProvider: AccountProvider, + ) : ChangeServerError() + data object SlidingSyncAlert : ChangeServerError() companion object { fun from(error: Throwable): ChangeServerError = when (error) { is AuthenticationException.SlidingSyncVersion -> SlidingSyncAlert is AuthenticationException.Oidc -> Error(messageStr = error.message) + is UnauthorizedAccountProviderException -> UnauthorizedAccountProvider(error.accountProvider) else -> Error(messageId = R.string.screen_change_server_error_invalid_homeserver) } } diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index 3c5702ba94..d257ac1b11 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -14,9 +14,10 @@ "Use a different account provider, such as your own private server or a work account." "Change account provider" "We couldn\'t reach this homeserver. Please check that you have entered the homeserver URL correctly. If the URL is correct, contact your homeserver administrator for further help." - "Server isn\'t available due to an issue in the well-known file: + "Server isn\'t available due to an issue in the .well-known file: %1$s" "The selected account provider does not support sliding sync. An upgrade to the server is needed to use %1$s." + "%1$s is not allowed to connect to %2$s." "Homeserver URL" "Enter a domain address." "What is the address of your server?" diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt index 15b8856969..20b70028d9 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt @@ -8,14 +8,18 @@ package io.element.android.features.login.impl.changeserver import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -38,6 +42,9 @@ class ChangeServerPresenterTest { val authenticationService = FakeMatrixAuthenticationService() createPresenter( authenticationService = authenticationService, + enterpriseService = FakeEnterpriseService( + isAllowedToConnectToHomeserverResult = { true }, + ), ).test { val initialState = awaitItem() assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) @@ -52,7 +59,11 @@ class ChangeServerPresenterTest { @Test fun `present - change server error`() = runTest { - createPresenter().test { + createPresenter( + enterpriseService = FakeEnterpriseService( + isAllowedToConnectToHomeserverResult = { true }, + ), + ).test { val initialState = awaitItem() assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) @@ -67,11 +78,37 @@ class ChangeServerPresenterTest { } } + @Test + fun `present - change server not allowed error`() = runTest { + val isAllowedToConnectToHomeserverResult = lambdaRecorder { false } + createPresenter( + enterpriseService = FakeEnterpriseService( + isAllowedToConnectToHomeserverResult = isAllowedToConnectToHomeserverResult, + ), + ).test { + val initialState = awaitItem() + assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) + val anAccountProvider = AccountProvider(url = A_HOMESERVER_URL) + initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(anAccountProvider)) + val loadingState = awaitItem() + assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java) + val failureState = awaitItem() + assertThat( + (failureState.changeServerAction.errorOrNull() as ChangeServerError.UnauthorizedAccountProvider).accountProvider + ).isEqualTo(anAccountProvider) + isAllowedToConnectToHomeserverResult.assertions() + .isCalledOnce() + .with(value(A_HOMESERVER_URL)) + } + } + private fun createPresenter( authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), + enterpriseService: EnterpriseService = FakeEnterpriseService(), ) = ChangeServerPresenter( authenticationService = authenticationService, - accountProviderDataSource = accountProviderDataSource + accountProviderDataSource = accountProviderDataSource, + enterpriseService = enterpriseService, ) } diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_3_en.png new file mode 100644 index 0000000000..2e5917d7de --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c1257fbdf388933be8fb2ae211c8abacc4cdb01c2a924a7ba3729337c9b6707 +size 15434 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_3_en.png new file mode 100644 index 0000000000..8a1571970c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb9a2d2145791122cc2ab7d903485de361fb6098216bf578dca7cc350e9f94a0 +size 13650