Check homeserver when login using qr code (#4708)

* Login with Qr code: check homeserver validity

* QrCode login, unauthorized homeserver: update copy.

* Update screenshots

* Add unit test on SdkQrCodeLoginData

* Remove default param value.

* Remember imageAnalysis

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Benoit Marty 2025-05-15 14:08:05 +02:00 committed by GitHub
parent 9825469912
commit f1ca70fb9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 208 additions and 30 deletions

View file

@ -21,8 +21,10 @@ open class AccountProviderProvider : PreviewParameterProvider<AccountProvider> {
)
}
fun anAccountProvider() = AccountProvider(
url = AuthenticationConfig.MATRIX_ORG_URL,
fun anAccountProvider(
url: String = AuthenticationConfig.MATRIX_ORG_URL,
) = AccountProvider(
url = url,
subtitle = "Matrix.org is an open network for secure, decentralized communication.",
isPublic = true,
isMatrixOrg = true,

View file

@ -56,7 +56,10 @@ class ChangeServerPresenter @Inject constructor(
) = launch {
suspend {
if (enterpriseService.isAllowedToConnectToHomeserver(data.url).not()) {
throw UnauthorizedAccountProviderException(data)
throw UnauthorizedAccountProviderException(
unauthorisedAccountProviderTitle = data.title,
authorisedAccountProviderTitles = listOfNotNull(enterpriseService.defaultHomeserver())
)
}
authenticationService.setHomeserver(data.url).map {
authenticationService.getHomeserverDetails().value!!

View file

@ -8,7 +8,6 @@
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
@ -19,7 +18,14 @@ open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerStat
aChangeServerState(),
aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.Error(CommonStrings.error_unknown))),
aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.SlidingSyncAlert)),
aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.UnauthorizedAccountProvider(anAccountProvider()))),
aChangeServerState(
changeServerAction = AsyncData.Failure(
ChangeServerError.UnauthorizedAccountProvider(
unauthorisedAccountProviderTitle = "example.com",
authorisedAccountProviderTitles = listOf("element.io", "element.org"),
)
)
),
)
}

View file

@ -62,7 +62,7 @@ fun ChangeServerView(
content = stringResource(
id = R.string.screen_change_server_error_unauthorized_homeserver,
LocalBuildMeta.current.applicationName,
error.accountProvider.title,
error.unauthorisedAccountProviderTitle,
),
onSubmit = {
eventSink.invoke(ChangeServerEvents.ClearError)

View file

@ -7,8 +7,7 @@
package io.element.android.features.login.impl.changeserver
import io.element.android.features.login.impl.accountprovider.AccountProvider
class UnauthorizedAccountProviderException(
val accountProvider: AccountProvider,
val unauthorisedAccountProviderTitle: String,
val authorisedAccountProviderTitles: List<String>,
) : Exception()

View file

@ -11,7 +11,6 @@ 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
@ -26,7 +25,8 @@ sealed class ChangeServerError : Throwable() {
}
data class UnauthorizedAccountProvider(
val accountProvider: AccountProvider,
val unauthorisedAccountProviderTitle: String,
val authorisedAccountProviderTitles: List<String>,
) : ChangeServerError()
data object SlidingSyncAlert : ChangeServerError()
@ -35,7 +35,10 @@ sealed class ChangeServerError : Throwable() {
fun from(error: Throwable): ChangeServerError = when (error) {
is AuthenticationException.SlidingSyncVersion -> SlidingSyncAlert
is AuthenticationException.Oidc -> Error(messageStr = error.message)
is UnauthorizedAccountProviderException -> UnauthorizedAccountProvider(error.accountProvider)
is UnauthorizedAccountProviderException -> UnauthorizedAccountProvider(
unauthorisedAccountProviderTitle = error.unauthorisedAccountProviderTitle,
authorisedAccountProviderTitles = error.authorisedAccountProviderTitles,
)
else -> Error(messageId = R.string.screen_change_server_error_invalid_homeserver)
}
}

View file

@ -15,6 +15,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException
import io.element.android.features.login.impl.qrcode.QrCodeLoginManager
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
@ -36,6 +38,7 @@ class QrCodeScanPresenter @Inject constructor(
private val qrCodeLoginDataFactory: MatrixQrCodeLoginDataFactory,
private val qrCodeLoginManager: QrCodeLoginManager,
private val coroutineDispatchers: CoroutineDispatchers,
private val enterpriseService: EnterpriseService,
) : Presenter<QrCodeScanState> {
private var isScanning by mutableStateOf(true)
@ -90,9 +93,17 @@ class QrCodeScanPresenter @Inject constructor(
launch(coroutineDispatchers.computation) {
suspend {
qrCodeLoginDataFactory.parseQrCodeData(code).onFailure {
val data = qrCodeLoginDataFactory.parseQrCodeData(code).onFailure {
Timber.e(it, "Error parsing QR code data")
}.getOrThrow()
val serverName = data.serverName()
if (serverName != null && enterpriseService.isAllowedToConnectToHomeserver(serverName).not()) {
throw UnauthorizedAccountProviderException(
unauthorisedAccountProviderTitle = serverName,
authorisedAccountProviderTitles = listOfNotNull(enterpriseService.defaultHomeserver())
)
}
data
}.runCatchingUpdatingState(codeScannedAction)
}.invokeOnCompletion {
isProcessingCode.set(false)

View file

@ -8,6 +8,7 @@
package io.element.android.features.login.impl.screens.qrcode.scan
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
@ -19,6 +20,15 @@ open class QrCodeScanStateProvider : PreviewParameterProvider<QrCodeScanState> {
aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Loading),
aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Failure(Exception("Error"))),
aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Failure(QrLoginException.OtherDeviceNotSignedIn)),
aQrCodeScanState(
isScanning = false,
authenticationAction = AsyncAction.Failure(
UnauthorizedAccountProviderException(
unauthorisedAccountProviderTitle = "example.com",
authorisedAccountProviderTitles = listOf("element.io", "element.org"),
)
)
),
// Add other state here
)
}

View file

@ -35,6 +35,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.components.BigIcon
@ -144,6 +145,12 @@ private fun ColumnScope.Buttons(
Spacer(modifier = Modifier.width(4.dp))
Text(
text = when (error) {
is UnauthorizedAccountProviderException -> {
stringResource(
id = R.string.screen_change_server_error_unauthorized_homeserver_title,
error.unauthorisedAccountProviderTitle,
)
}
is QrLoginException.OtherDeviceNotSignedIn -> {
stringResource(R.string.screen_qr_code_login_device_not_signed_in_scan_state_subtitle)
}
@ -156,6 +163,12 @@ private fun ColumnScope.Buttons(
}
Text(
text = when (error) {
is UnauthorizedAccountProviderException -> {
stringResource(
id = R.string.screen_change_server_error_unauthorized_homeserver_content,
error.authorisedAccountProviderTitles.joinToString(),
)
}
is QrLoginException.OtherDeviceNotSignedIn -> {
stringResource(R.string.screen_qr_code_login_device_not_signed_in_scan_state_description)
}

View file

@ -18,6 +18,8 @@
%1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"The selected account provider does not support sliding sync. An upgrade to the server is needed to use %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s is not allowed to connect to %2$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"This app has been configured to allow: %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"Account provider %1$s not allowed."</string>
<string name="screen_change_server_form_header">"Homeserver URL"</string>
<string name="screen_change_server_form_notice">"Enter a domain address."</string>
<string name="screen_change_server_subtitle">"What is the address of your server?"</string>