Skip to content

Commit 663362a

Browse files
jmartinespElementBot
andauthored
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 <[email protected]>
1 parent da3f5e0 commit 663362a

File tree

79 files changed

+315
-231
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+315
-231
lines changed

appnav/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ dependencies {
5959
testImplementation(libs.test.turbine)
6060
testImplementation(projects.libraries.matrix.test)
6161
testImplementation(projects.libraries.oidc.impl)
62+
testImplementation(projects.libraries.preferences.test)
6263
testImplementation(projects.libraries.push.test)
6364
testImplementation(projects.libraries.pushproviders.test)
6465
testImplementation(projects.features.networkmonitor.test)

appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ package io.element.android.appnav.loggedin
99

1010
sealed interface LoggedInEvents {
1111
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents
12+
data object CheckSlidingSyncProxyAvailability : LoggedInEvents
13+
data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvents
1214
}

appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import androidx.compose.runtime.getValue
1616
import androidx.compose.runtime.mutableStateOf
1717
import androidx.compose.runtime.remember
1818
import androidx.compose.runtime.rememberCoroutineScope
19+
import androidx.compose.runtime.setValue
1920
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
2021
import im.vector.app.features.analytics.plan.UserProperties
2122
import io.element.android.features.networkmonitor.api.NetworkMonitor
@@ -29,6 +30,7 @@ import io.element.android.libraries.matrix.api.encryption.RecoveryState
2930
import io.element.android.libraries.matrix.api.roomlist.RoomListService
3031
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
3132
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
33+
import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase
3234
import io.element.android.libraries.push.api.PushService
3335
import io.element.android.libraries.pushproviders.api.RegistrationFailure
3436
import io.element.android.services.analytics.api.AnalyticsService
@@ -48,6 +50,7 @@ class LoggedInPresenter @Inject constructor(
4850
private val sessionVerificationService: SessionVerificationService,
4951
private val analyticsService: AnalyticsService,
5052
private val encryptionService: EncryptionService,
53+
private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase,
5154
) : Presenter<LoggedInState> {
5255
@Composable
5356
override fun present(): LoggedInState {
@@ -78,6 +81,7 @@ class LoggedInPresenter @Inject constructor(
7881
networkStatus == NetworkStatus.Online && syncIndicator == RoomListService.SyncIndicator.Show
7982
}
8083
}
84+
var forceNativeSlidingSyncMigration by remember { mutableStateOf(false) }
8185
LaunchedEffect(Unit) {
8286
combine(
8387
sessionVerificationService.sessionVerifiedStatus,
@@ -97,13 +101,26 @@ class LoggedInPresenter @Inject constructor(
97101
}
98102
}
99103
}
104+
LoggedInEvents.CheckSlidingSyncProxyAvailability -> coroutineScope.launch {
105+
// 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.
106+
forceNativeSlidingSyncMigration = !matrixClient.isUsingNativeSlidingSync() &&
107+
matrixClient.isNativeSlidingSyncSupported() &&
108+
!matrixClient.isSlidingSyncProxySupported()
109+
}
110+
LoggedInEvents.LogoutAndMigrateToNativeSlidingSync -> coroutineScope.launch {
111+
// Enable native sliding sync if it wasn't already the case
112+
enableNativeSlidingSyncUseCase()
113+
// Then force the logout
114+
matrixClient.logout(userInitiated = true, ignoreSdkError = true)
115+
}
100116
}
101117
}
102118

103119
return LoggedInState(
104120
showSyncSpinner = showSyncSpinner,
105121
pusherRegistrationState = pusherRegistrationState.value,
106122
ignoreRegistrationError = ignoreRegistrationError,
123+
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
107124
eventSink = ::handleEvent
108125
)
109126
}

appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ data class LoggedInState(
1313
val showSyncSpinner: Boolean,
1414
val pusherRegistrationState: AsyncData<Unit>,
1515
val ignoreRegistrationError: Boolean,
16+
val forceNativeSlidingSyncMigration: Boolean,
1617
val eventSink: (LoggedInEvents) -> Unit,
1718
)

appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,18 @@ open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
1616
aLoggedInState(),
1717
aLoggedInState(showSyncSpinner = true),
1818
aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())),
19+
aLoggedInState(forceNativeSlidingSyncMigration = true),
1920
)
2021
}
2122

2223
fun aLoggedInState(
2324
showSyncSpinner: Boolean = false,
2425
pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized,
26+
forceNativeSlidingSyncMigration: Boolean = false,
2527
) = LoggedInState(
2628
showSyncSpinner = showSyncSpinner,
2729
pusherRegistrationState = pusherRegistrationState,
2830
ignoreRegistrationError = false,
31+
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
2932
eventSink = {},
3033
)

appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@ import androidx.compose.ui.Alignment
1515
import androidx.compose.ui.Modifier
1616
import androidx.compose.ui.res.stringResource
1717
import androidx.compose.ui.tooling.preview.PreviewParameter
18+
import androidx.lifecycle.Lifecycle
19+
import io.element.android.appnav.R
1820
import io.element.android.libraries.architecture.AsyncData
21+
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
1922
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogWithDoNotShowAgain
2023
import io.element.android.libraries.designsystem.preview.ElementPreview
2124
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
25+
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
2226
import io.element.android.libraries.matrix.api.exception.isNetworkError
2327
import io.element.android.libraries.ui.strings.CommonStrings
2428

@@ -28,6 +32,11 @@ fun LoggedInView(
2832
navigateToNotificationTroubleshoot: () -> Unit,
2933
modifier: Modifier = Modifier
3034
) {
35+
OnLifecycleEvent { _, event ->
36+
if (event == Lifecycle.Event.ON_RESUME) {
37+
state.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
38+
}
39+
}
3140
Box(
3241
modifier = modifier
3342
.fillMaxSize()
@@ -61,6 +70,13 @@ fun LoggedInView(
6170
}
6271
}
6372
}
73+
74+
// Set the force migration dialog here so it's always displayed over every screen
75+
if (state.forceNativeSlidingSyncMigration) {
76+
ForceNativeSlidingSyncMigrationDialog(onSubmit = {
77+
state.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync)
78+
})
79+
}
6480
}
6581

6682
private fun Throwable.getReason(): String? {
@@ -80,6 +96,19 @@ private fun Throwable.getReason(): String? {
8096
}
8197
}
8298

99+
@Composable
100+
private fun ForceNativeSlidingSyncMigrationDialog(
101+
onSubmit: () -> Unit,
102+
) {
103+
ErrorDialog(
104+
title = null,
105+
content = stringResource(R.string.banner_migrate_to_native_sliding_sync_force_logout_title),
106+
submitText = stringResource(R.string.banner_migrate_to_native_sliding_sync_action),
107+
onSubmit = onSubmit,
108+
canDismiss = false,
109+
)
110+
}
111+
83112
@PreviewsDayNight
84113
@Composable
85114
internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = ElementPreview {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
3+
<string name="banner_migrate_to_native_sliding_sync_action">"Log Out &amp; Upgrade"</string>
4+
<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>
5+
</resources>

appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
2929
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
3030
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
3131
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
32+
import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase
33+
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
3234
import io.element.android.libraries.push.api.PushService
3335
import io.element.android.libraries.push.test.FakePushService
3436
import io.element.android.libraries.pushproviders.api.Distributor
@@ -42,6 +44,10 @@ import io.element.android.tests.testutils.lambda.any
4244
import io.element.android.tests.testutils.lambda.lambdaError
4345
import io.element.android.tests.testutils.lambda.lambdaRecorder
4446
import io.element.android.tests.testutils.lambda.value
47+
import kotlinx.coroutines.ExperimentalCoroutinesApi
48+
import kotlinx.coroutines.flow.first
49+
import kotlinx.coroutines.test.TestScope
50+
import kotlinx.coroutines.test.advanceUntilIdle
4551
import kotlinx.coroutines.test.runTest
4652
import org.junit.Rule
4753
import org.junit.Test
@@ -91,7 +97,8 @@ class LoggedInPresenterTest {
9197
pushService = FakePushService(),
9298
sessionVerificationService = verificationService,
9399
analyticsService = analyticsService,
94-
encryptionService = encryptionService
100+
encryptionService = encryptionService,
101+
enableNativeSlidingSyncUseCase = EnableNativeSlidingSyncUseCase(InMemoryAppPreferencesStore(), this),
95102
)
96103
moleculeFlow(RecompositionMode.Immediate) {
97104
presenter.present()
@@ -487,26 +494,103 @@ class LoggedInPresenterTest {
487494
)
488495
}
489496

497+
@Test
498+
fun `present - CheckSlidingSyncProxyAvailability forces the sliding sync migration under the right circumstances`() = runTest {
499+
// The migration will be forced if:
500+
// - The user is not using the native sliding sync
501+
// - The sliding sync proxy is no longer supported
502+
// - The native sliding sync is supported
503+
val matrixClient = FakeMatrixClient(
504+
isUsingNativeSlidingSyncLambda = { false },
505+
isSlidingSyncProxySupportedLambda = { false },
506+
isNativeSlidingSyncSupportedLambda = { true },
507+
)
508+
val presenter = createLoggedInPresenter(matrixClient = matrixClient)
509+
moleculeFlow(RecompositionMode.Immediate) {
510+
presenter.present()
511+
}.test {
512+
val initialState = awaitItem()
513+
assertThat(initialState.forceNativeSlidingSyncMigration).isFalse()
514+
515+
initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
516+
517+
assertThat(awaitItem().forceNativeSlidingSyncMigration).isTrue()
518+
}
519+
}
520+
521+
@Test
522+
fun `present - CheckSlidingSyncProxyAvailability will not force the migration if native sliding sync is not supported too`() = runTest {
523+
val matrixClient = FakeMatrixClient(
524+
isUsingNativeSlidingSyncLambda = { false },
525+
isSlidingSyncProxySupportedLambda = { false },
526+
isNativeSlidingSyncSupportedLambda = { false },
527+
)
528+
val presenter = createLoggedInPresenter(matrixClient = matrixClient)
529+
moleculeFlow(RecompositionMode.Immediate) {
530+
presenter.present()
531+
}.test {
532+
val initialState = awaitItem()
533+
assertThat(initialState.forceNativeSlidingSyncMigration).isFalse()
534+
535+
initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
536+
537+
expectNoEvents()
538+
}
539+
}
540+
541+
@OptIn(ExperimentalCoroutinesApi::class)
542+
@Test
543+
fun `present - LogoutAndMigrateToNativeSlidingSync enables native sliding sync and logs out the user`() = runTest {
544+
val logoutLambda = lambdaRecorder<Boolean, Boolean, String?> { userInitiated, ignoreSdkError ->
545+
assertThat(userInitiated).isTrue()
546+
assertThat(ignoreSdkError).isTrue()
547+
null
548+
}
549+
val matrixClient = FakeMatrixClient().apply {
550+
this.logoutLambda = logoutLambda
551+
}
552+
val appPreferencesStore = InMemoryAppPreferencesStore()
553+
val enableNativeSlidingSyncUseCase = EnableNativeSlidingSyncUseCase(appPreferencesStore, this)
554+
val presenter = createLoggedInPresenter(matrixClient = matrixClient, enableNativeSlidingSyncUseCase = enableNativeSlidingSyncUseCase)
555+
moleculeFlow(RecompositionMode.Immediate) {
556+
presenter.present()
557+
}.test {
558+
val initialState = awaitItem()
559+
560+
assertThat(appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse()
561+
562+
initialState.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync)
563+
564+
advanceUntilIdle()
565+
566+
assertThat(appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue()
567+
assertThat(logoutLambda.assertions().isCalledOnce())
568+
}
569+
}
570+
490571
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
491572
skipItems(1)
492573
return awaitItem()
493574
}
494575

495-
private fun createLoggedInPresenter(
576+
private fun TestScope.createLoggedInPresenter(
496577
roomListService: RoomListService = FakeRoomListService(),
497578
networkStatus: NetworkStatus = NetworkStatus.Offline,
498579
analyticsService: AnalyticsService = FakeAnalyticsService(),
499580
sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(),
500581
encryptionService: EncryptionService = FakeEncryptionService(),
501582
pushService: PushService = FakePushService(),
583+
enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase = EnableNativeSlidingSyncUseCase(InMemoryAppPreferencesStore(), this),
584+
matrixClient: MatrixClient = FakeMatrixClient(roomListService = roomListService),
502585
): LoggedInPresenter {
503586
return LoggedInPresenter(
504-
matrixClient = FakeMatrixClient(roomListService = roomListService),
587+
matrixClient = matrixClient,
505588
networkMonitor = FakeNetworkMonitor(networkStatus),
506589
pushService = pushService,
507590
sessionVerificationService = sessionVerificationService,
508591
analyticsService = analyticsService,
509-
encryptionService = encryptionService
592+
encryptionService = encryptionService,
593+
enableNativeSlidingSyncUseCase = enableNativeSlidingSyncUseCase,
510594
)
511595
}
512596
}

features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ internal fun CallScreenView(
111111
is AsyncData.Failure ->
112112
ErrorDialog(
113113
content = state.urlState.error.message.orEmpty(),
114-
onDismiss = { state.eventSink(CallScreenEvents.Hangup) },
114+
onSubmit = { state.eventSink(CallScreenEvents.Hangup) },
115115
)
116116
is AsyncData.Success -> Unit
117117
}

features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ private fun LeaveRoomErrorDialog(
105105
is LeaveRoomState.Error.Hidden -> {}
106106
is LeaveRoomState.Error.Shown -> ErrorDialog(
107107
content = stringResource(CommonStrings.error_unknown),
108-
onDismiss = { state.eventSink(LeaveRoomEvent.HideError) }
108+
onSubmit = { state.eventSink(LeaveRoomEvent.HideError) }
109109
)
110110
}
111111
}

0 commit comments

Comments
 (0)