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:
parent
da3f5e00dc
commit
663362ac7f
79 changed files with 315 additions and 231 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,6 @@ data class LoggedInState(
|
|||
val showSyncSpinner: Boolean,
|
||||
val pusherRegistrationState: AsyncData<Unit>,
|
||||
val ignoreRegistrationError: Boolean,
|
||||
val forceNativeSlidingSyncMigration: Boolean,
|
||||
val eventSink: (LoggedInEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
5
appnav/src/main/res/values/localazy.xml
Normal file
5
appnav/src/main/res/values/localazy.xml
Normal 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 & 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>
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue