Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

1 change: 1 addition & 0 deletions appnav/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ dependencies {
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.impl)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.features.networkmonitor.test)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -97,13 +101,26 @@ 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)
}
}
}

return LoggedInState(
showSyncSpinner = showSyncSpinner,
pusherRegistrationState = pusherRegistrationState.value,
ignoreRegistrationError = ignoreRegistrationError,
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
eventSink = ::handleEvent
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ data class LoggedInState(
val showSyncSpinner: Boolean,
val pusherRegistrationState: AsyncData<Unit>,
val ignoreRegistrationError: Boolean,
val forceNativeSlidingSyncMigration: Boolean,
val eventSink: (LoggedInEvents) -> Unit,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {},
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@
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

Expand All @@ -28,6 +32,11 @@
navigateToNotificationTroubleshoot: () -> Unit,
modifier: Modifier = Modifier
) {
OnLifecycleEvent { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
state.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
}
}
Box(
modifier = modifier
.fillMaxSize()
Expand Down Expand Up @@ -61,6 +70,13 @@
}
}
}

// Set the force migration dialog here so it's always displayed over every screen
if (state.forceNativeSlidingSyncMigration) {
ForceNativeSlidingSyncMigrationDialog(onSubmit = {
state.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync)

Check warning on line 77 in appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L77 was not covered by tests
})
}
}

private fun Throwable.getReason(): String? {
Expand All @@ -80,6 +96,19 @@
}
}

@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 {
Expand Down
5 changes: 5 additions & 0 deletions appnav/src/main/res/values/localazy.xml
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ internal fun CallScreenView(
is AsyncData.Failure ->
ErrorDialog(
content = state.urlState.error.message.orEmpty(),
onDismiss = { state.eventSink(CallScreenEvents.Hangup) },
onSubmit = { state.eventSink(CallScreenEvents.Hangup) },
)
is AsyncData.Success -> Unit
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
is LeaveRoomState.Error.Hidden -> {}
is LeaveRoomState.Error.Shown -> ErrorDialog(
content = stringResource(CommonStrings.error_unknown),
onDismiss = { state.eventSink(LeaveRoomEvent.HideError) }
onSubmit = { state.eventSink(LeaveRoomEvent.HideError) }

Check warning on line 108 in features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L108 was not covered by tests
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ private fun SetupPinContent(
ErrorDialog(
title = state.setupPinFailure.title(),
content = state.setupPinFailure.content(),
onDismiss = {
onSubmit = {
state.eventSink(SetupPinEvents.ClearFailure)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
if (state.showBiometricUnlockError) {
ErrorDialog(
content = state.biometricUnlockErrorMessage ?: "",
onDismiss = { state.eventSink(PinUnlockEvents.ClearBiometricError) }
onSubmit = { state.eventSink(PinUnlockEvents.ClearBiometricError) }

Check warning on line 107 in features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt

View check run for this annotation

Codecov / codecov/patch

features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt#L107

Added line #L107 was not covered by tests
)
}
}
Expand Down Expand Up @@ -206,7 +206,7 @@
ErrorDialog(
title = stringResource(id = R.string.screen_app_lock_signout_alert_title),
content = stringResource(id = R.string.screen_app_lock_signout_alert_message),
onDismiss = onSignOut,
onSubmit = onSignOut,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fun ChangeServerView(
ErrorDialog(
modifier = modifier,
content = error.message(),
onDismiss = {
onSubmit = {
eventSink.invoke(ChangeServerEvents.ClearError)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
is ChangeServerError.Error -> {
ErrorDialog(
content = error.message(),
onDismiss = {
onSubmit = {

Check warning on line 106 in features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt

View check run for this annotation

Codecov / codecov/patch

features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt#L106

Added line #L106 was not covered by tests
eventSink.invoke(ConfirmAccountProviderEvents.ClearError)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ private fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) {
ErrorDialog(
title = stringResource(id = CommonStrings.dialog_title_error),
content = stringResource(loginError(error)),
onDismiss = onDismiss
onSubmit = onDismiss
)
}

Expand Down
6 changes: 0 additions & 6 deletions features/login/impl/src/main/res/values-be/translations.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,4 @@
<string name="screen_server_confirmation_message_register">"Тут будуць захоўвацца вашыя размовы - сапраўды гэтак жа, як вы выкарыстоўваеце паштовага правайдара для захоўвання сваіх лістоў."</string>
<string name="screen_server_confirmation_title_login">"Вы збіраецеся ўвайсці ў %1$s"</string>
<string name="screen_server_confirmation_title_register">"Вы збіраецеся стварыць уліковы запіс на %1$s"</string>
<string name="screen_waitlist_message">"Зараз існуе высокі попыт на %1$s на %2$s. Калі ласка, вярніцеся ў праграму праз некалькі дзён і паспрабуйце зноў.

Дзякуй за цярпенне!"</string>
<string name="screen_waitlist_message_success">"Вітаем у %1$s!"</string>
<string name="screen_waitlist_title">"Амаль гатова."</string>
<string name="screen_waitlist_title_success">"Вы зарэгістраваны."</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,4 @@
<string name="screen_server_confirmation_message_register">"Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли."</string>
<string name="screen_server_confirmation_title_login">"На път сте да влезете в %1$s"</string>
<string name="screen_server_confirmation_title_register">"На път сте да създадете акаунт в %1$s"</string>
<string name="screen_waitlist_message_success">"Добре дошли в %1$s!"</string>
</resources>
Loading
Loading