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

@ -9,4 +9,6 @@ package io.element.android.appnav.loggedin
sealed interface LoggedInEvents {
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents
data object CheckSlidingSyncProxyAvailability : LoggedInEvents
data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvents
}

View file

@ -16,6 +16,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
import im.vector.app.features.analytics.plan.UserProperties
import io.element.android.features.networkmonitor.api.NetworkMonitor
@ -29,6 +30,7 @@ import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.pushproviders.api.RegistrationFailure
import io.element.android.services.analytics.api.AnalyticsService
@ -48,6 +50,7 @@ class LoggedInPresenter @Inject constructor(
private val sessionVerificationService: SessionVerificationService,
private val analyticsService: AnalyticsService,
private val encryptionService: EncryptionService,
private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase,
) : Presenter<LoggedInState> {
@Composable
override fun present(): LoggedInState {
@ -78,6 +81,7 @@ class LoggedInPresenter @Inject constructor(
networkStatus == NetworkStatus.Online && syncIndicator == RoomListService.SyncIndicator.Show
}
}
var forceNativeSlidingSyncMigration by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
combine(
sessionVerificationService.sessionVerifiedStatus,
@ -97,6 +101,18 @@ class LoggedInPresenter @Inject constructor(
}
}
}
LoggedInEvents.CheckSlidingSyncProxyAvailability -> coroutineScope.launch {
// Force the user to log out if they were using the proxy sliding sync and it's no longer available, but native sliding sync is.
forceNativeSlidingSyncMigration = !matrixClient.isUsingNativeSlidingSync() &&
matrixClient.isNativeSlidingSyncSupported() &&
!matrixClient.isSlidingSyncProxySupported()
}
LoggedInEvents.LogoutAndMigrateToNativeSlidingSync -> coroutineScope.launch {
// Enable native sliding sync if it wasn't already the case
enableNativeSlidingSyncUseCase()
// Then force the logout
matrixClient.logout(userInitiated = true, ignoreSdkError = true)
}
}
}
@ -104,6 +120,7 @@ class LoggedInPresenter @Inject constructor(
showSyncSpinner = showSyncSpinner,
pusherRegistrationState = pusherRegistrationState.value,
ignoreRegistrationError = ignoreRegistrationError,
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
eventSink = ::handleEvent
)
}

View file

@ -13,5 +13,6 @@ data class LoggedInState(
val showSyncSpinner: Boolean,
val pusherRegistrationState: AsyncData<Unit>,
val ignoreRegistrationError: Boolean,
val forceNativeSlidingSyncMigration: Boolean,
val eventSink: (LoggedInEvents) -> Unit,
)

View file

@ -16,15 +16,18 @@ open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
aLoggedInState(),
aLoggedInState(showSyncSpinner = true),
aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())),
aLoggedInState(forceNativeSlidingSyncMigration = true),
)
}
fun aLoggedInState(
showSyncSpinner: Boolean = false,
pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized,
forceNativeSlidingSyncMigration: Boolean = false,
) = LoggedInState(
showSyncSpinner = showSyncSpinner,
pusherRegistrationState = pusherRegistrationState,
ignoreRegistrationError = false,
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
eventSink = {},
)

View file

@ -15,10 +15,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.lifecycle.Lifecycle
import io.element.android.appnav.R
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogWithDoNotShowAgain
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.matrix.api.exception.isNetworkError
import io.element.android.libraries.ui.strings.CommonStrings
@ -28,6 +32,11 @@ fun LoggedInView(
navigateToNotificationTroubleshoot: () -> Unit,
modifier: Modifier = Modifier
) {
OnLifecycleEvent { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
state.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
}
}
Box(
modifier = modifier
.fillMaxSize()
@ -61,6 +70,13 @@ fun LoggedInView(
}
}
}
// Set the force migration dialog here so it's always displayed over every screen
if (state.forceNativeSlidingSyncMigration) {
ForceNativeSlidingSyncMigrationDialog(onSubmit = {
state.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync)
})
}
}
private fun Throwable.getReason(): String? {
@ -80,6 +96,19 @@ private fun Throwable.getReason(): String? {
}
}
@Composable
private fun ForceNativeSlidingSyncMigrationDialog(
onSubmit: () -> Unit,
) {
ErrorDialog(
title = null,
content = stringResource(R.string.banner_migrate_to_native_sliding_sync_force_logout_title),
submitText = stringResource(R.string.banner_migrate_to_native_sliding_sync_action),
onSubmit = onSubmit,
canDismiss = false,
)
}
@PreviewsDayNight
@Composable
internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = ElementPreview {

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Log Out &amp; Upgrade"</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."</string>
</resources>

View file

@ -29,6 +29,8 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.push.test.FakePushService
import io.element.android.libraries.pushproviders.api.Distributor
@ -42,6 +44,10 @@ import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@ -91,7 +97,8 @@ class LoggedInPresenterTest {
pushService = FakePushService(),
sessionVerificationService = verificationService,
analyticsService = analyticsService,
encryptionService = encryptionService
encryptionService = encryptionService,
enableNativeSlidingSyncUseCase = EnableNativeSlidingSyncUseCase(InMemoryAppPreferencesStore(), this),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -487,26 +494,103 @@ class LoggedInPresenterTest {
)
}
@Test
fun `present - CheckSlidingSyncProxyAvailability forces the sliding sync migration under the right circumstances`() = runTest {
// The migration will be forced if:
// - The user is not using the native sliding sync
// - The sliding sync proxy is no longer supported
// - The native sliding sync is supported
val matrixClient = FakeMatrixClient(
isUsingNativeSlidingSyncLambda = { false },
isSlidingSyncProxySupportedLambda = { false },
isNativeSlidingSyncSupportedLambda = { true },
)
val presenter = createLoggedInPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.forceNativeSlidingSyncMigration).isFalse()
initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
assertThat(awaitItem().forceNativeSlidingSyncMigration).isTrue()
}
}
@Test
fun `present - CheckSlidingSyncProxyAvailability will not force the migration if native sliding sync is not supported too`() = runTest {
val matrixClient = FakeMatrixClient(
isUsingNativeSlidingSyncLambda = { false },
isSlidingSyncProxySupportedLambda = { false },
isNativeSlidingSyncSupportedLambda = { false },
)
val presenter = createLoggedInPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.forceNativeSlidingSyncMigration).isFalse()
initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
expectNoEvents()
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - LogoutAndMigrateToNativeSlidingSync enables native sliding sync and logs out the user`() = runTest {
val logoutLambda = lambdaRecorder<Boolean, Boolean, String?> { userInitiated, ignoreSdkError ->
assertThat(userInitiated).isTrue()
assertThat(ignoreSdkError).isTrue()
null
}
val matrixClient = FakeMatrixClient().apply {
this.logoutLambda = logoutLambda
}
val appPreferencesStore = InMemoryAppPreferencesStore()
val enableNativeSlidingSyncUseCase = EnableNativeSlidingSyncUseCase(appPreferencesStore, this)
val presenter = createLoggedInPresenter(matrixClient = matrixClient, enableNativeSlidingSyncUseCase = enableNativeSlidingSyncUseCase)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse()
initialState.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync)
advanceUntilIdle()
assertThat(appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue()
assertThat(logoutLambda.assertions().isCalledOnce())
}
}
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
skipItems(1)
return awaitItem()
}
private fun createLoggedInPresenter(
private fun TestScope.createLoggedInPresenter(
roomListService: RoomListService = FakeRoomListService(),
networkStatus: NetworkStatus = NetworkStatus.Offline,
analyticsService: AnalyticsService = FakeAnalyticsService(),
sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(),
encryptionService: EncryptionService = FakeEncryptionService(),
pushService: PushService = FakePushService(),
enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase = EnableNativeSlidingSyncUseCase(InMemoryAppPreferencesStore(), this),
matrixClient: MatrixClient = FakeMatrixClient(roomListService = roomListService),
): LoggedInPresenter {
return LoggedInPresenter(
matrixClient = FakeMatrixClient(roomListService = roomListService),
matrixClient = matrixClient,
networkMonitor = FakeNetworkMonitor(networkStatus),
pushService = pushService,
sessionVerificationService = sessionVerificationService,
analyticsService = analyticsService,
encryptionService = encryptionService
encryptionService = encryptionService,
enableNativeSlidingSyncUseCase = enableNativeSlidingSyncUseCase,
)
}
}