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

@ -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()