Add forced logout flow when the proxy is no longer available (#3458)

* Add `MatrixClient.isSlidingSyncProxySupported` function

* Update localazy strings

* Modify `ErrorDialog` to have an `onSubmit` call, which will be used for the submit action.

Also make the title text optional and dismissing the dialog by tapping outside/going back configurable.

* Check if a forced migration to SSS is needed because the proxy is no longer available.

In that case, display the non-dismissable dialog and force the user to log out after enabling SSS.

* Enable native/simplified sliding sync by default.

* Refactor the login to make sure we:

1. Always try native/simplified sliding sync login first, if available.
2. Then, if it wasn't available or failed with an sliding sync not supported error, try with the proxy instead (either discovered proxy or forced custom one).

* Move logic to `LoggedInPresenter` and the UI to `LoggedInView`

* Update screenshots

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2024-09-16 11:13:02 +02:00 committed by GitHub
parent da3f5e00dc
commit 663362ac7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
79 changed files with 315 additions and 231 deletions

View file

@ -48,7 +48,7 @@ fun <T> AsyncActionView(
ErrorDialog(
title = errorTitle(async.error),
content = errorMessage(async.error),
onDismiss = onErrorDismiss
onSubmit = onErrorDismiss
)
} else {
RetryDialog(

View file

@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.window.DialogProperties
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
@ -25,17 +26,23 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun ErrorDialog(
content: String,
onDismiss: () -> Unit,
onSubmit: () -> Unit,
modifier: Modifier = Modifier,
title: String = ErrorDialogDefaults.title,
title: String? = ErrorDialogDefaults.title,
submitText: String = ErrorDialogDefaults.submitText,
onDismiss: () -> Unit = onSubmit,
canDismiss: Boolean = true,
) {
BasicAlertDialog(modifier = modifier, onDismissRequest = onDismiss) {
BasicAlertDialog(
modifier = modifier,
onDismissRequest = onDismiss,
properties = DialogProperties(dismissOnClickOutside = canDismiss, dismissOnBackPress = canDismiss)
) {
ErrorDialogContent(
title = title,
content = content,
submitText = submitText,
onSubmitClick = onDismiss,
onSubmitClick = onSubmit,
)
}
}
@ -44,7 +51,7 @@ fun ErrorDialog(
private fun ErrorDialogContent(
content: String,
onSubmitClick: () -> Unit,
title: String = ErrorDialogDefaults.title,
title: String? = ErrorDialogDefaults.title,
submitText: String = ErrorDialogDefaults.submitText,
) {
SimpleAlertDialogContent(
@ -78,6 +85,6 @@ internal fun ErrorDialogContentPreview() {
internal fun ErrorDialogPreview() = ElementPreview {
ErrorDialog(
content = "Content",
onDismiss = {},
onSubmit = {},
)
}

View file

@ -131,6 +131,9 @@ interface MatrixClient : Closeable {
/** Returns `true` if the home server supports native sliding sync. */
suspend fun isNativeSlidingSyncSupported(): Boolean
/** Returns `true` if the current session is using native sliding sync. */
/** Returns `true` if the home server supports sliding sync using a proxy. */
suspend fun isSlidingSyncProxySupported(): Boolean
/** Returns `true` if the current session is using native sliding sync, `false` if it's using a proxy. */
fun isUsingNativeSlidingSync(): Boolean
}

View file

@ -534,6 +534,10 @@ class RustMatrixClient(
return client.availableSlidingSyncVersions().contains(SlidingSyncVersion.Native)
}
override suspend fun isSlidingSyncProxySupported(): Boolean {
return client.availableSlidingSyncVersions().any { it is SlidingSyncVersion.Proxy }
}
override fun isUsingNativeSlidingSync(): Boolean {
return client.session().slidingSyncVersion == SlidingSyncVersion.Native
}

View file

@ -7,7 +7,6 @@
package io.element.android.libraries.matrix.impl
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.featureflag.api.FeatureFlagService
@ -19,12 +18,10 @@ import io.element.android.libraries.matrix.impl.paths.getSessionPaths
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
import io.element.android.libraries.matrix.impl.util.anonymizedTokens
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.ClientBuilder
import org.matrix.rustcomponents.sdk.Session
@ -47,7 +44,6 @@ class RustMatrixClientFactory @Inject constructor(
private val proxyProvider: ProxyProvider,
private val clock: SystemClock,
private val utdTracker: UtdTracker,
private val appPreferencesStore: AppPreferencesStore,
private val featureFlagService: FeatureFlagService,
) {
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
@ -55,7 +51,7 @@ class RustMatrixClientFactory @Inject constructor(
val client = getBaseClientBuilder(
sessionPaths = sessionData.getSessionPaths(),
passphrase = sessionData.passphrase,
restore = true,
slidingSyncType = ClientBuilderSlidingSync.Restored,
)
.homeserverUrl(sessionData.homeserverUrl)
.username(sessionData.userId)
@ -88,16 +84,8 @@ class RustMatrixClientFactory @Inject constructor(
internal suspend fun getBaseClientBuilder(
sessionPaths: SessionPaths,
passphrase: String?,
restore: Boolean,
slidingSyncType: ClientBuilderSlidingSync,
): ClientBuilder {
val slidingSync = when {
// Always check restore first, since otherwise other values could accidentally override the already persisted config
restore -> ClientBuilderSlidingSync.Restored
AuthenticationConfig.SLIDING_SYNC_PROXY_URL != null -> ClientBuilderSlidingSync.CustomProxy(AuthenticationConfig.SLIDING_SYNC_PROXY_URL!!)
appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first() -> ClientBuilderSlidingSync.Simplified
else -> ClientBuilderSlidingSync.Discovered
}
return ClientBuilder()
.sessionPaths(
dataPath = sessionPaths.fileDirectory.absolutePath,
@ -117,9 +105,9 @@ class RustMatrixClientFactory @Inject constructor(
)
.run {
// Apply sliding sync version settings
when (slidingSync) {
when (slidingSyncType) {
ClientBuilderSlidingSync.Restored -> this
is ClientBuilderSlidingSync.CustomProxy -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.Proxy(slidingSync.url))
is ClientBuilderSlidingSync.CustomProxy -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.Proxy(slidingSyncType.url))
ClientBuilderSlidingSync.Discovered -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.DiscoverProxy)
ClientBuilderSlidingSync.Simplified -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.DiscoverNative)
ClientBuilderSlidingSync.ForcedSimplified -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.Native)

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.impl.auth
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.di.AppScope
@ -19,6 +20,7 @@ import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper
import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData
@ -28,6 +30,7 @@ import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator
import io.element.android.libraries.matrix.impl.mapper.toSessionData
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.sessionstorage.api.LoggedInState
import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionStore
@ -35,9 +38,13 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientBuilder
import org.matrix.rustcomponents.sdk.HumanQrLoginException
import org.matrix.rustcomponents.sdk.OidcConfiguration
import org.matrix.rustcomponents.sdk.QrCodeData
import org.matrix.rustcomponents.sdk.QrCodeDecodeException
import org.matrix.rustcomponents.sdk.QrLoginProgress
import org.matrix.rustcomponents.sdk.QrLoginProgressListener
@ -55,6 +62,7 @@ class RustMatrixAuthenticationService @Inject constructor(
private val rustMatrixClientFactory: RustMatrixClientFactory,
private val passphraseGenerator: PassphraseGenerator,
private val oidcConfigurationProvider: OidcConfigurationProvider,
private val appPreferencesStore: AppPreferencesStore,
) : MatrixAuthenticationService {
// Passphrase which will be used for new sessions. Existing sessions will use the passphrase
// stored in the SessionData.
@ -117,9 +125,10 @@ class RustMatrixAuthenticationService @Inject constructor(
withContext(coroutineDispatchers.io) {
val emptySessionPath = rotateSessionPath()
runCatching {
val client = getBaseClientBuilder(emptySessionPath)
.serverNameOrHomeserverUrl(homeserver)
.build()
val client = makeClient(sessionPaths = emptySessionPath) {
serverNameOrHomeserverUrl(homeserver)
}
currentClient = client
val homeServerDetails = client.homeserverLoginDetails().map()
currentHomeserver.value = homeServerDetails.copy(url = homeserver)
@ -207,23 +216,24 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) =
withContext(coroutineDispatchers.io) {
val sdkQrCodeLoginData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData
val emptySessionPaths = rotateSessionPath()
val oidcConfiguration = oidcConfigurationProvider.get()
val progressListener = object : QrLoginProgressListener {
override fun onUpdate(state: QrLoginProgress) {
Timber.d("QR Code login progress: $state")
progress(state.toStep())
}
}
runCatching {
val client = rustMatrixClientFactory.getBaseClientBuilder(
val client = makeQrCodeLoginClient(
sessionPaths = emptySessionPaths,
passphrase = pendingPassphrase,
restore = false,
qrCodeData = sdkQrCodeLoginData,
oidcConfiguration = oidcConfiguration,
progressListener = progressListener,
)
.buildWithQrCode(
qrCodeData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData,
oidcConfiguration = oidcConfigurationProvider.get(),
progressListener = object : QrLoginProgressListener {
override fun onUpdate(state: QrLoginProgress) {
Timber.d("QR Code login progress: $state")
progress(state.toStep())
}
}
)
client.use { rustClient ->
val sessionData = rustClient.session()
.toSessionData(
@ -249,14 +259,80 @@ class RustMatrixAuthenticationService @Inject constructor(
}
}
private suspend fun getBaseClientBuilder(
private suspend fun makeClient(
sessionPaths: SessionPaths,
) = rustMatrixClientFactory
.getBaseClientBuilder(
sessionPaths = sessionPaths,
passphrase = pendingPassphrase,
restore = false,
)
config: suspend ClientBuilder.() -> ClientBuilder,
): Client {
val slidingSyncType = getSlidingSyncType()
if (slidingSyncType is ClientBuilderSlidingSync.Simplified) {
Timber.d("Creating client with simplified sliding sync")
try {
return rustMatrixClientFactory
.getBaseClientBuilder(
sessionPaths = sessionPaths,
passphrase = pendingPassphrase,
slidingSyncType = slidingSyncType,
)
.run { config() }
.build()
} catch (e: HumanQrLoginException.SlidingSyncNotAvailable) {
Timber.e(e, "Failed to create client with simplified sliding sync, trying with Proxy now")
}
}
Timber.d("Creating client with Proxy sliding sync")
return rustMatrixClientFactory
.getBaseClientBuilder(
sessionPaths = sessionPaths,
passphrase = pendingPassphrase,
slidingSyncType = getSlidingSyncProxy(),
)
.run { config() }
.build()
}
private suspend fun makeQrCodeLoginClient(
sessionPaths: SessionPaths,
passphrase: String?,
qrCodeData: QrCodeData,
oidcConfiguration: OidcConfiguration,
progressListener: QrLoginProgressListener,
): Client {
val slidingSyncType = getSlidingSyncType()
if (slidingSyncType is ClientBuilderSlidingSync.Simplified) {
Timber.d("Creating client for QR Code login with simplified sliding sync")
try {
return rustMatrixClientFactory
.getBaseClientBuilder(
sessionPaths = sessionPaths,
passphrase = pendingPassphrase,
slidingSyncType = slidingSyncType,
)
.passphrase(passphrase)
.buildWithQrCode(qrCodeData, oidcConfiguration, progressListener)
} catch (e: HumanQrLoginException.SlidingSyncNotAvailable) {
Timber.e(e, "Failed to create client with simplified sliding sync, trying with Proxy now")
}
}
Timber.d("Creating client for QR Code login with Proxy sliding sync")
return rustMatrixClientFactory
.getBaseClientBuilder(
sessionPaths = sessionPaths,
passphrase = pendingPassphrase,
slidingSyncType = getSlidingSyncProxy(),
)
.passphrase(passphrase)
.buildWithQrCode(qrCodeData, oidcConfiguration, progressListener)
}
private suspend fun getSlidingSyncType(nativeSlidingSyncFailed: Boolean = false) = when {
appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first() && !nativeSlidingSyncFailed -> ClientBuilderSlidingSync.Simplified
else -> getSlidingSyncProxy()
}
private fun getSlidingSyncProxy() = when {
AuthenticationConfig.SLIDING_SYNC_PROXY_URL != null -> ClientBuilderSlidingSync.CustomProxy(AuthenticationConfig.SLIDING_SYNC_PROXY_URL!!)
else -> ClientBuilderSlidingSync.Discovered
}
private fun clear() {
currentClient?.close()

View file

@ -79,6 +79,7 @@ class FakeMatrixClient(
private val userIdServerNameLambda: () -> String = { lambdaError() },
private val getUrlLambda: (String) -> Result<String> = { lambdaError() },
var isNativeSlidingSyncSupportedLambda: suspend () -> Boolean = { true },
var isSlidingSyncProxySupportedLambda: suspend () -> Boolean = { true },
var isUsingNativeSlidingSyncLambda: () -> Boolean = { true },
) : MatrixClient {
var setDisplayNameCalled: Boolean = false
@ -324,6 +325,10 @@ class FakeMatrixClient(
return isNativeSlidingSyncSupportedLambda()
}
override suspend fun isSlidingSyncProxySupported(): Boolean {
return isSlidingSyncProxySupportedLambda()
}
override fun isUsingNativeSlidingSync(): Boolean {
return isUsingNativeSlidingSyncLambda()
}

View file

@ -87,7 +87,7 @@ class DefaultAppPreferencesStore @Inject constructor(
override fun isSimplifiedSlidingSyncEnabledFlow(): Flow<Boolean> {
return store.data.map { prefs ->
prefs[simplifiedSlidingSyncKey] ?: false
prefs[simplifiedSlidingSyncKey] ?: true
}
}

View file

@ -36,6 +36,7 @@
<string name="action_back">"Back"</string>
<string name="action_call">"Call"</string>
<string name="action_cancel">"Cancel"</string>
<string name="action_cancel_for_now">"Cancel for now"</string>
<string name="action_choose_photo">"Choose photo"</string>
<string name="action_clear">"Clear"</string>
<string name="action_close">"Close"</string>
@ -283,6 +284,12 @@ Reason: %1$s."</string>
<string name="screen_pinned_timeline_screen_title_empty">"Pinned messages"</string>
<string name="screen_reset_identity_confirmation_subtitle">"You\'re about to go to your %1$s account to reset your identity. Afterwards you\'ll be taken back to the app."</string>
<string name="screen_reset_identity_confirmation_title">"Can\'t confirm? Go to your account to reset your identity."</string>
<string name="screen_resolve_send_failure_changed_identity_primary_button_title">"Withdraw verification and send"</string>
<string name="screen_resolve_send_failure_changed_identity_subtitle">"You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$s."</string>
<string name="screen_resolve_send_failure_changed_identity_title">"Your message was not sent because %1$ss verified identity has changed"</string>
<string name="screen_resolve_send_failure_unsigned_device_primary_button_title">"Send message anyway"</string>
<string name="screen_resolve_send_failure_unsigned_device_subtitle">"%1$s is using one or more unverified devices. You can send the message anyway, or you can cancel for now and try again later after %2$s has verified all their devices."</string>
<string name="screen_resolve_send_failure_unsigned_device_title">"Your message was not sent because %1$s has not verified one or more devices"</string>
<string name="screen_room_details_pinned_events_row_title">"Pinned messages"</string>
<string name="screen_room_error_failed_processing_media">"Failed processing media to upload, please try again."</string>
<string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
@ -304,6 +311,8 @@ Reason: %1$s."</string>
<string name="screen_share_open_google_maps">"Open in Google Maps"</string>
<string name="screen_share_open_osm_maps">"Open in OpenStreetMap"</string>
<string name="screen_share_this_location_action">"Share this location"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Message not sent because %1$ss verified identity has changed."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Message not sent because %1$s has not verified one or more devices."</string>
<string name="screen_view_location_title">"Location"</string>
<string name="settings_version_number">"Version: %1$s (%2$s)"</string>
<string name="test_language_identifier">"en"</string>