Repository: element-hq/element-x-android (language: kotlin)

Issue:
Offer user to transition to SSS (Android)
1. Follow the logic defined in the parent issue to prompt the user to log out to complete the migration to SSS.
2. For prompt choose platform-native implementation (whatever is the easiest to implement).


Discussion hints:
@amshakal Would you explicitly create the designs in Figma in such a case? I think it would be preferred to make it more specific.
Designs for Android: https://www.figma.com/design/LwZ8S0VbzKqekK1akuoh8z/Launch-%2B-Onboarding-(new)?node-id=241-4337&t=Y89GDbdGbVjhMV83-1

Pull request: Add banner for optional migration to simplified sliding sync
<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
 
## Content

- Add `MatrixClient.isNativeSlidingSyncSupported()` and `MatrixClient.isUsingNativeSlidingSync` to check whether the home server supports native sliding sync and we're already using it.
- Add `NativeSlidingSyncMigrationBanner` composable to the `RoomList` screen when the home server supports native sliding sync but the current session is not using it.
- Add an extra logout successful action to the logout flow, create `EnableNativeSlidingSyncUseCase` so it can be used there.

## Motivation and context

Closes #3427.

## Screenshots / GIFs

In the PR.

## Tests

<!-- Explain how you tested your development -->

- With simplified sliding sync disabled in the client, log into a homeserver which has it enabled (i.e. element.io).
- You should see the migration banner.
- When tapping in log out & upgrade, a confirmation will be displayed.
- Once logged out if you log in again you'll be using SSS.
- The banner won't be visible then.

## Tested devices

- [x] Physical
- [ ] Emulator
- OS version(s): 14

## Checklist

<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->

- [ ] Changes have been tested on an Android device or Android emulator with API 23
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [x] Pull request is based on the develop branch
- [x] Pull request title will be used in the release note, it clearly define what will change for the user
- [x] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
- [x] You've made a self review of your PR


Test command: ./gradlew :features:roomlist:impl:testDebugUnitTest --tests "io.element.android.features.roomlist.impl.RoomListViewTest" --tests "io.element.android.features.roomlist.impl.RoomListPresenterTest" :libraries:preferences:api:testDebugUnitTest --tests "io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCaseTest"

Fix patch:
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
index db153314d62..5a8db185ef3 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
@@ -40,6 +40,7 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint
 import io.element.android.features.ftue.api.FtueEntryPoint
 import io.element.android.features.ftue.api.state.FtueService
 import io.element.android.features.ftue.api.state.FtueState
+import io.element.android.features.logout.api.LogoutEntryPoint
 import io.element.android.features.networkmonitor.api.NetworkMonitor
 import io.element.android.features.networkmonitor.api.NetworkStatus
 import io.element.android.features.preferences.api.PreferencesEntryPoint
@@ -65,6 +66,7 @@ import io.element.android.libraries.matrix.api.core.UserId
 import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
 import io.element.android.libraries.matrix.api.permalink.PermalinkData
 import io.element.android.libraries.matrix.api.sync.SyncState
+import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase
 import io.element.android.services.appnavstate.api.AppNavigationStateService
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.FlowPreview
@@ -96,6 +98,8 @@ class LoggedInFlowNode @AssistedInject constructor(
     private val shareEntryPoint: ShareEntryPoint,
     private val matrixClient: MatrixClient,
     private val sendingQueue: SendQueues,
+    private val logoutEntryPoint: LogoutEntryPoint,
+    private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase,
     snackbarDispatcher: SnackbarDispatcher,
 ) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
     backstack = BackStack(
@@ -225,6 +229,9 @@ class LoggedInFlowNode @AssistedInject constructor(
 
         @Parcelize
         data class IncomingShare(val intent: Intent) : NavTarget
+
+        @Parcelize
+        data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget
     }
 
     override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -271,6 +278,10 @@ class LoggedInFlowNode @AssistedInject constructor(
                     override fun onRoomDirectorySearchClick() {
                         backstack.push(NavTarget.RoomDirectorySearch)
                     }
+
+                    override fun onLogoutForNativeSlidingSyncMigrationNeeded() {
+                        backstack.push(NavTarget.LogoutForNativeSlidingSyncMigrationNeeded)
+                    }
                 }
                 roomListEntryPoint
                     .nodeBuilder(this, buildContext)
@@ -407,6 +418,20 @@ class LoggedInFlowNode @AssistedInject constructor(
                     .params(ShareEntryPoint.Params(intent = navTarget.intent))
                     .build()
             }
+            is NavTarget.LogoutForNativeSlidingSyncMigrationNeeded -> {
+                val callback = object : LogoutEntryPoint.Callback {
+                    override fun onChangeRecoveryKeyClick() {
+                        backstack.push(NavTarget.SecureBackup())
+                    }
+                }
+
+                logoutEntryPoint.nodeBuilder(this, buildContext)
+                    .onSuccessfulLogoutPendingAction {
+                        enableNativeSlidingSyncUseCase()
+                    }
+                    .callback(callback)
+                    .build()
+            }
         }
     }
 
diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt
index d90f039ec1b..ea952b01004 100644
--- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt
+++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt
@@ -16,6 +16,7 @@ interface LogoutEntryPoint : FeatureEntryPoint {
     fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
 
     interface NodeBuilder {
+        fun onSuccessfulLogoutPendingAction(action: () -> Unit): NodeBuilder
         fun callback(callback: Callback): NodeBuilder
         fun build(): Node
     }
diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt
index 7c7264a00f8..08de15c2ee1 100644
--- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt
+++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt
@@ -27,6 +27,15 @@ class DefaultLogoutEntryPoint @Inject constructor() : LogoutEntryPoint {
                 return this
             }
 
+            override fun onSuccessfulLogoutPendingAction(action: () -> Unit): LogoutEntryPoint.NodeBuilder {
+                plugins += object : LogoutNode.SuccessfulLogoutPendingAction, Plugin {
+                    override fun onSuccessfulLogoutPendingAction() {
+                        action()
+                    }
+                }
+                return this
+            }
+
             override fun build(): Node {
                 return parentNode.createNode<LogoutNode>(buildContext, plugins)
             }
diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt
index fb2c1585a05..dd9788cd1d5 100644
--- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt
+++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt
@@ -33,6 +33,12 @@ class LogoutNode @AssistedInject constructor(
         plugins<LogoutEntryPoint.Callback>().forEach { it.onChangeRecoveryKeyClick() }
     }
 
+    interface SuccessfulLogoutPendingAction : Plugin {
+        fun onSuccessfulLogoutPendingAction()
+    }
+
+    private val customOnSuccessfulLogoutPendingAction = plugins<SuccessfulLogoutPendingAction>().firstOrNull()
+
     @Composable
     override fun View(modifier: Modifier) {
         val state = presenter.present()
@@ -41,7 +47,10 @@ class LogoutNode @AssistedInject constructor(
         LogoutView(
             state = state,
             onChangeRecoveryKeyClick = ::onChangeRecoveryKeyClick,
-            onSuccessLogout = { onSuccessLogout(activity, isDark, it) },
+            onSuccessLogout = {
+                customOnSuccessfulLogoutPendingAction?.onSuccessfulLogoutPendingAction()
+                onSuccessLogout(activity, isDark, it)
+            },
             onBackClick = ::navigateUp,
             modifier = modifier,
         )
diff --git a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt
index 7070fbe9150..98ed717f7b9 100644
--- a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt
+++ b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt
@@ -29,5 +29,6 @@ interface RoomListEntryPoint : FeatureEntryPoint {
         fun onRoomSettingsClick(roomId: RoomId)
         fun onReportBugClick()
         fun onRoomDirectorySearchClick()
+        fun onLogoutForNativeSlidingSyncMigrationNeeded()
     }
 }
diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts
index 5e3dc513c64..ce747859f5c 100644
--- a/features/roomlist/impl/build.gradle.kts
+++ b/features/roomlist/impl/build.gradle.kts
@@ -49,6 +49,7 @@ dependencies {
     implementation(projects.libraries.push.api)
     implementation(projects.features.invite.api)
     implementation(projects.features.networkmonitor.api)
+    implementation(projects.features.logout.api)
     implementation(projects.features.leaveroom.api)
     implementation(projects.services.analytics.api)
     implementation(libs.androidx.datastore.preferences)
@@ -75,6 +76,7 @@ dependencies {
     testImplementation(projects.services.analytics.test)
     testImplementation(projects.services.toolbox.test)
     testImplementation(projects.features.networkmonitor.test)
+    testImplementation(projects.features.logout.test)
     testImplementation(projects.tests.testutils)
     testImplementation(projects.features.leaveroom.test)
 }
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt
index 4e586056fd1..6656ca8231c 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt
@@ -20,6 +20,7 @@ open class RoomListContentStateProvider : PreviewParameterProvider<RoomListConte
             aRoomsContentState(summaries = persistentListOf()),
             aSkeletonContentState(),
             anEmptyContentState(),
+            aRoomsContentState(securityBannerState = SecurityBannerState.NeedsNativeSlidingSyncMigration),
         )
 }
 
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt
index 50a29f3ff76..9a8f2353ba4 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt
@@ -13,7 +13,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
 sealed interface RoomListEvents {
     data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
     data object DismissRequestVerificationPrompt : RoomListEvents
-    data object DismissRecoveryKeyPrompt : RoomListEvents
+    data object DismissBanner : RoomListEvents
     data object ToggleSearchResults : RoomListEvents
     data class AcceptInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
     data class DeclineInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt
index bdb5425b826..1481027310b 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt
@@ -21,11 +21,14 @@ import dagger.assisted.AssistedInject
 import im.vector.app.features.analytics.plan.MobileScreen
 import io.element.android.anvilannotations.ContributesNode
 import io.element.android.features.invite.api.response.AcceptDeclineInviteView
+import io.element.android.features.logout.api.direct.DirectLogoutEvents
+import io.element.android.features.logout.api.direct.DirectLogoutView
 import io.element.android.features.roomlist.api.RoomListEntryPoint
 import io.element.android.features.roomlist.impl.components.RoomListMenuAction
 import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
 import io.element.android.libraries.di.SessionScope
 import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase
 import io.element.android.services.analytics.api.AnalyticsService
 
 @ContributesNode(SessionScope::class)
@@ -36,6 +39,8 @@ class RoomListNode @AssistedInject constructor(
     private val inviteFriendsUseCase: InviteFriendsUseCase,
     private val analyticsService: AnalyticsService,
     private val acceptDeclineInviteView: AcceptDeclineInviteView,
+    private val directLogoutView: DirectLogoutView,
+    private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase,
 ) : Node(buildContext, plugins = plugins) {
     init {
         lifecycle.subscribe(
@@ -88,6 +93,7 @@ class RoomListNode @AssistedInject constructor(
     override fun View(modifier: Modifier) {
         val state = presenter.present()
         val activity = LocalContext.current as Activity
+
         RoomListView(
             state = state,
             onRoomClick = this::onRoomClick,
@@ -98,6 +104,13 @@ class RoomListNode @AssistedInject constructor(
             onRoomSettingsClick = this::onRoomSettingsClick,
             onMenuActionClick = { onMenuActionClick(activity, it) },
             onRoomDirectorySearchClick = this::onRoomDirectorySearchClick,
+            onMigrateToNativeSlidingSyncClick = {
+                if (state.directLogoutState.canDoDirectSignOut) {
+                    state.directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
+                } else {
+                    plugins<RoomListEntryPoint.Callback>().forEach { it.onLogoutForNativeSlidingSyncMigrationNeeded() }
+                }
+            },
             modifier = modifier,
         ) {
             acceptDeclineInviteView.Render(
@@ -107,5 +120,9 @@ class RoomListNode @AssistedInject constructor(
                 modifier = Modifier
             )
         }
+
+        directLogoutView.Render(state.directLogoutState) {
+            enableNativeSlidingSyncUseCase()
+        }
     }
 }
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
index d2b45c0d061..2983b6b6952 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
@@ -29,6 +29,7 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteState
 import io.element.android.features.invite.api.response.InviteData
 import io.element.android.features.leaveroom.api.LeaveRoomEvent
 import io.element.android.features.leaveroom.api.LeaveRoomPresenter
+import io.element.android.features.logout.api.direct.DirectLogoutPresenter
 import io.element.android.features.networkmonitor.api.NetworkMonitor
 import io.element.android.features.networkmonitor.api.NetworkStatus
 import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
@@ -88,6 +89,7 @@ class RoomListPresenter @Inject constructor(
     private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
     private val fullScreenIntentPermissionsPresenter: FullScreenIntentPermissionsPresenter,
     private val notificationCleaner: NotificationCleaner,
+    private val logoutPresenter: DirectLogoutPresenter,
 ) : Presenter<RoomListState> {
     private val encryptionService: EncryptionService = client.encryptionService()
     private val syncService: SyncService = client.syncService()
@@ -115,13 +117,15 @@ class RoomListPresenter @Inject constructor(
 
         val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
 
+        val directLogoutState = logoutPresenter.present()
+
         fun handleEvents(event: RoomListEvents) {
             when (event) {
                 is RoomListEvents.UpdateVisibleRange -> coroutineScope.launch {
                     updateVisibleRange(event.range)
                 }
                 RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true
-                RoomListEvents.DismissRecoveryKeyPrompt -> securityBannerDismissed = true
+                RoomListEvents.DismissBanner -> securityBannerDismissed = true
                 RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
                 is RoomListEvents.ShowContextMenu -> {
                     coroutineScope.showContextMenu(event, contextMenu)
@@ -161,6 +165,7 @@ class RoomListPresenter @Inject constructor(
             searchState = searchState,
             contentState = contentState,
             acceptDeclineInviteState = acceptDeclineInviteState,
+            directLogoutState = directLogoutState,
             eventSink = ::handleEvents,
         )
     }
@@ -168,6 +173,7 @@ class RoomListPresenter @Inject constructor(
     @Composable
     private fun securityBannerState(
         securityBannerDismissed: Boolean,
+        needsSlidingSyncMigration: Boolean,
     ): State<SecurityBannerState> {
         val currentSecurityBannerDismissed by rememberUpdatedState(securityBannerDismissed)
         val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
@@ -185,6 +191,7 @@ class RoomListPresenter @Inject constructor(
                             RecoveryState.ENABLED -> SecurityBannerState.None
                         }
                     }
+                    needsSlidingSyncMigration -> SecurityBannerState.NeedsNativeSlidingSyncMigration
                     else -> SecurityBannerState.None
                 }
             }
@@ -209,11 +216,14 @@ class RoomListPresenter @Inject constructor(
                 loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading
             }
         }
+        val needsSlidingSyncMigration by produceState(false) {
+            value = client.isNativeSlidingSyncSupported() && !client.isUsingNativeSlidingSync()
+        }
         return when {
             showEmpty -> RoomListContentState.Empty
             showSkeleton -> RoomListContentState.Skeleton(count = 16)
             else -> {
-                val securityBannerState by securityBannerState(securityBannerDismissed)
+                val securityBannerState by securityBannerState(securityBannerDismissed, needsSlidingSyncMigration)
                 RoomListContentState.Rooms(
                     securityBannerState = securityBannerState,
                     fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
index bb95b85293f..ae9a721bfac 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
@@ -10,6 +10,7 @@ package io.element.android.features.roomlist.impl
 import androidx.compose.runtime.Immutable
 import io.element.android.features.invite.api.response.AcceptDeclineInviteState
 import io.element.android.features.leaveroom.api.LeaveRoomState
+import io.element.android.features.logout.api.direct.DirectLogoutState
 import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
 import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
 import io.element.android.features.roomlist.impl.search.RoomListSearchState
@@ -31,6 +32,7 @@ data class RoomListState(
     val searchState: RoomListSearchState,
     val contentState: RoomListContentState,
     val acceptDeclineInviteState: AcceptDeclineInviteState,
+    val directLogoutState: DirectLogoutState,
     val eventSink: (RoomListEvents) -> Unit,
 ) {
     val displayFilters = contentState is RoomListContentState.Rooms
@@ -59,6 +61,7 @@ enum class SecurityBannerState {
     None,
     SetUpRecovery,
     RecoveryKeyConfirmation,
+    NeedsNativeSlidingSyncMigration,
 }
 
 @Immutable
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt
index 31499600ba1..b9d003e32c0 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt
@@ -12,6 +12,8 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteState
 import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
 import io.element.android.features.leaveroom.api.LeaveRoomState
 import io.element.android.features.leaveroom.api.aLeaveRoomState
+import io.element.android.features.logout.api.direct.DirectLogoutState
+import io.element.android.features.logout.api.direct.aDirectLogoutState
 import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
 import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
 import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
@@ -57,6 +59,7 @@ internal fun aRoomListState(
     filtersState: RoomListFiltersState = aRoomListFiltersState(),
     contentState: RoomListContentState = aRoomsContentState(),
     acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
+    directLogoutState: DirectLogoutState = aDirectLogoutState(),
     eventSink: (RoomListEvents) -> Unit = {}
 ) = RoomListState(
     matrixUser = matrixUser,
@@ -69,6 +72,7 @@ internal fun aRoomListState(
     searchState = searchState,
     contentState = contentState,
     acceptDeclineInviteState = acceptDeclineInviteState,
+    directLogoutState = directLogoutState,
     eventSink = eventSink,
 )
 
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt
index cfa5605c1e2..4db0f68e4e9 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt
@@ -50,6 +50,7 @@ fun RoomListView(
     onRoomSettingsClick: (roomId: RoomId) -> Unit,
     onMenuActionClick: (RoomListMenuAction) -> Unit,
     onRoomDirectorySearchClick: () -> Unit,
+    onMigrateToNativeSlidingSyncClick: () -> Unit,
     modifier: Modifier = Modifier,
     acceptDeclineInviteView: @Composable () -> Unit,
 ) {
@@ -76,6 +77,7 @@ fun RoomListView(
                 onOpenSettings = onSettingsClick,
                 onCreateRoomClick = onCreateRoomClick,
                 onMenuActionClick = onMenuActionClick,
+                onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
                 modifier = Modifier.padding(top = topPadding),
             )
             // This overlaid view will only be visible when state.displaySearchResults is true
@@ -105,6 +107,7 @@ private fun RoomListScaffold(
     onOpenSettings: () -> Unit,
     onCreateRoomClick: () -> Unit,
     onMenuActionClick: (RoomListMenuAction) -> Unit,
+    onMigrateToNativeSlidingSyncClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
     fun onRoomClick(room: RoomListRoomSummary) {
@@ -140,6 +143,7 @@ private fun RoomListScaffold(
                 onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
                 onRoomClick = ::onRoomClick,
                 onCreateRoomClick = onCreateRoomClick,
+                onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
                 modifier = Modifier
                     .padding(padding)
                     .consumeWindowInsets(padding)
@@ -180,5 +184,6 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class)
         onMenuActionClick = {},
         onRoomDirectorySearchClick = {},
         acceptDeclineInviteView = {},
+        onMigrateToNativeSlidingSyncClick = {},
     )
 }
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/NativeSlidingSyncMigrationBanner.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/NativeSlidingSyncMigrationBanner.kt
new file mode 100644
index 00000000000..6d058d6a8aa
--- /dev/null
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/NativeSlidingSyncMigrationBanner.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.roomlist.impl.components
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import io.element.android.features.roomlist.impl.R
+import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+
+@Composable
+internal fun NativeSlidingSyncMigrationBanner(
+    onContinueClick: () -> Unit,
+    onDismissClick: () -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    DialogLikeBannerMolecule(
+        modifier = modifier,
+        title = stringResource(R.string.banner_migrate_to_native_sliding_sync_title),
+        content = stringResource(R.string.banner_migrate_to_native_sliding_sync_description),
+        actionText = stringResource(R.string.banner_migrate_to_native_sliding_sync_action),
+        onSubmitClick = onContinueClick,
+        onDismissClick = onDismissClick,
+    )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun NativeSlidingSyncMigrationBannerPreview() = ElementPreview {
+    NativeSlidingSyncMigrationBanner(
+        onContinueClick = {},
+        onDismissClick = {},
+    )
+}
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt
index 8abbce3068c..d4af5a3fe54 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt
@@ -64,6 +64,7 @@ fun RoomListContentView(
     onConfirmRecoveryKeyClick: () -> Unit,
     onRoomClick: (RoomListRoomSummary) -> Unit,
     onCreateRoomClick: () -> Unit,
+    onMigrateToNativeSlidingSyncClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
     Box(modifier = modifier) {
@@ -85,6 +86,7 @@ fun RoomListContentView(
                     eventSink = eventSink,
                     onSetUpRecoveryClick = onSetUpRecoveryClick,
                     onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
+                    onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
                     onRoomClick = onRoomClick,
                 )
             }
@@ -133,6 +135,7 @@ private fun RoomsView(
     onSetUpRecoveryClick: () -> Unit,
     onConfirmRecoveryKeyClick: () -> Unit,
     onRoomClick: (RoomListRoomSummary) -> Unit,
+    onMigrateToNativeSlidingSyncClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
     if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
@@ -147,6 +150,7 @@ private fun RoomsView(
             onSetUpRecoveryClick = onSetUpRecoveryClick,
             onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
             onRoomClick = onRoomClick,
+            onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
             modifier = modifier.fillMaxSize(),
         )
     }
@@ -159,6 +163,7 @@ private fun RoomsViewList(
     onSetUpRecoveryClick: () -> Unit,
     onConfirmRecoveryKeyClick: () -> Unit,
     onRoomClick: (RoomListRoomSummary) -> Unit,
+    onMigrateToNativeSlidingSyncClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
     val lazyListState = rememberLazyListState()
@@ -185,7 +190,7 @@ private fun RoomsViewList(
                 item {
                     SetUpRecoveryKeyBanner(
                         onContinueClick = onSetUpRecoveryClick,
-                        onDismissClick = { updatedEventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
+                        onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) }
                     )
                 }
             }
@@ -193,7 +198,15 @@ private fun RoomsViewList(
                 item {
                     ConfirmRecoveryKeyBanner(
                         onContinueClick = onConfirmRecoveryKeyClick,
-                        onDismissClick = { updatedEventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
+                        onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) }
+                    )
+                }
+            }
+            SecurityBannerState.NeedsNativeSlidingSyncMigration -> {
+                item {
+                    NativeSlidingSyncMigrationBanner(
+                        onContinueClick = onMigrateToNativeSlidingSyncClick,
+                        onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) }
                     )
                 }
             }
@@ -278,5 +291,6 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
         onConfirmRecoveryKeyClick = {},
         onRoomClick = {},
         onCreateRoomClick = {},
+        onMigrateToNativeSlidingSyncClick = {},
     )
 }
diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml
index 6affb2bb168..ff6c626c63f 100644
--- a/features/roomlist/impl/src/main/res/values/localazy.xml
+++ b/features/roomlist/impl/src/main/res/values/localazy.xml
@@ -1,5 +1,8 @@
 <?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_description">"Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."</string>
+  <string name="banner_migrate_to_native_sliding_sync_title">"Upgrade available"</string>
   <string name="banner_set_up_recovery_content">"Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."</string>
   <string name="banner_set_up_recovery_title">"Set up recovery"</string>
   <string name="confirm_recovery_key_banner_message">"Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."</string>
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt
index b9fd8d7fe04..966ce5b0b90 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt
@@ -39,6 +39,7 @@ fun DialogLikeBannerMolecule(
     onSubmitClick: () -> Unit,
     onDismissClick: (() -> Unit)?,
     modifier: Modifier = Modifier,
+    actionText: String = stringResource(CommonStrings.action_continue),
 ) {
     Box(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
         Surface(
@@ -74,7 +75,7 @@ fun DialogLikeBannerMolecule(
                 )
                 Spacer(modifier = Modifier.height(12.dp))
                 Button(
-                    text = stringResource(CommonStrings.action_continue),
+                    text = actionText,
                     size = ButtonSize.Medium,
                     modifier = Modifier.fillMaxWidth(),
                     onClick = onSubmitClick,
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
index 65dfa39c272..2591a5cfd31 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
@@ -126,4 +126,10 @@ interface MatrixClient : Closeable {
      */
     suspend fun getUrl(url: String): Result<String>
     suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<RoomPreview>
+
+    /** Returns `true` if the home server supports native sliding sync. */
+    suspend fun isNativeSlidingSyncSupported(): Boolean
+
+    /** Returns `true` if the current session is using native sliding sync. */
+    fun isUsingNativeSlidingSync(): Boolean
 }
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
index afdb9e99468..8077ef5e153 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
@@ -94,6 +94,7 @@ import org.matrix.rustcomponents.sdk.IgnoredUsersListener
 import org.matrix.rustcomponents.sdk.NotificationProcessSetup
 import org.matrix.rustcomponents.sdk.PowerLevels
 import org.matrix.rustcomponents.sdk.SendQueueRoomErrorListener
+import org.matrix.rustcomponents.sdk.SlidingSyncVersion
 import org.matrix.rustcomponents.sdk.TaskHandle
 import org.matrix.rustcomponents.sdk.use
 import timber.log.Timber
@@ -530,6 +531,14 @@ class RustMatrixClient(
         })
     }.buffer(Channel.UNLIMITED)
 
+    override suspend fun isNativeSlidingSyncSupported(): Boolean {
+        return client.availableSlidingSyncVersions().contains(SlidingSyncVersion.Native)
+    }
+
+    override fun isUsingNativeSlidingSync(): Boolean {
+        return client.session().slidingSyncVersion == SlidingSyncVersion.Native
+    }
+
     internal fun setDelegate(delegate: RustClientSessionDelegate) {
         client.setDelegate(delegate)
     }
diff --git a/libraries/preferences/api/build.gradle.kts b/libraries/preferences/api/build.gradle.kts
index a8a1566987f..b9ab6630df1 100644
--- a/libraries/preferences/api/build.gradle.kts
+++ b/libraries/preferences/api/build.gradle.kts
@@ -17,4 +17,8 @@ dependencies {
     implementation(libs.coroutines.core)
     implementation(projects.libraries.matrix.api)
     implementation(libs.androidx.datastore.preferences)
+
+    testImplementation(projects.libraries.preferences.test)
+    testImplementation(libs.test.truth)
+    testImplementation(libs.coroutines.test)
 }
diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCase.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCase.kt
new file mode 100644
index 00000000000..e5bcbad18ea
--- /dev/null
+++ b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCase.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.preferences.api.store
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class EnableNativeSlidingSyncUseCase @Inject constructor(
+    private val appPreferencesStore: AppPreferencesStore,
+    private val appCoroutineScope: CoroutineScope,
+) {
+    operator fun invoke() {
+        appCoroutineScope.launch {
+            appPreferencesStore.setSimplifiedSlidingSyncEnabled(true)
+        }
+    }
+}
diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts
index 40a930d7e6f..ae7b6d58d4b 100644
--- a/samples/minimal/build.gradle.kts
+++ b/samples/minimal/build.gradle.kts
@@ -56,6 +56,7 @@ dependencies {
     implementation(projects.features.roomlist.impl)
     implementation(projects.features.leaveroom.impl)
     implementation(projects.features.login.impl)
+    implementation(projects.features.logout.impl)
     implementation(projects.features.networkmonitor.impl)
     implementation(projects.services.toolbox.impl)
     implementation(projects.libraries.featureflag.impl)
diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt
index a3516ba43f0..70f2a1f8a4a 100644
--- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt
+++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
 import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter
 import io.element.android.features.invite.impl.response.AcceptDeclineInviteView
 import io.element.android.features.leaveroom.impl.DefaultLeaveRoomPresenter
+import io.element.android.features.logout.impl.direct.DefaultDirectLogoutPresenter
 import io.element.android.features.networkmonitor.impl.DefaultNetworkMonitor
 import io.element.android.features.roomlist.impl.RoomListPresenter
 import io.element.android.features.roomlist.impl.RoomListView
@@ -144,6 +145,7 @@ class RoomListScreen(
             }
         },
         notificationCleaner = FakeNotificationCleaner(),
+        logoutPresenter = DefaultDirectLogoutPresenter(matrixClient, encryptionService),
     )
 
     @Composable
@@ -172,7 +174,8 @@ class RoomListScreen(
             modifier = modifier,
             acceptDeclineInviteView = {
                 AcceptDeclineInviteView(state = state.acceptDeclineInviteState, onAcceptInvite = {}, onDeclineInvite = {})
-            }
+            },
+            onMigrateToNativeSlidingSyncClick = {},
         )
 
         DisposableEffect(Unit) {
diff --git a/tools/localazy/config.json b/tools/localazy/config.json
index c6a256b1bd3..f7368257257 100644
--- a/tools/localazy/config.json
+++ b/tools/localazy/config.json
@@ -139,6 +139,7 @@
         "session_verification_banner_.*",
         "confirm_recovery_key_banner_.*",
         "banner\\.set_up_recovery\\..*",
+        "banner\\.migrate_to_native_sliding_sync\\..*",
         "full_screen_intent_banner_.*",
         "screen_migration_.*",
         "screen_invites_.*"


Test patch:
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
index 1b639d2b193..8b7294b165c 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
@@ -7,6 +7,7 @@
 
 package io.element.android.features.roomlist.impl
 
+import androidx.compose.runtime.Composable
 import app.cash.molecule.RecompositionMode
 import app.cash.molecule.moleculeFlow
 import app.cash.turbine.test
@@ -18,6 +19,8 @@ import io.element.android.features.invite.api.response.anAcceptDeclineInviteStat
 import io.element.android.features.leaveroom.api.LeaveRoomEvent
 import io.element.android.features.leaveroom.api.LeaveRoomPresenter
 import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter
+import io.element.android.features.logout.api.direct.DirectLogoutPresenter
+import io.element.android.features.logout.api.direct.aDirectLogoutState
 import io.element.android.features.networkmonitor.api.NetworkMonitor
 import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
 import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
@@ -240,7 +243,7 @@ class RoomListPresenterTest {
             sessionVerificationService = FakeSessionVerificationService().apply {
                 givenNeedsSessionVerification(false)
             },
-            syncService = FakeSyncService(MutableStateFlow(SyncState.Running))
+            syncService = FakeSyncService(MutableStateFlow(SyncState.Running)),
         )
         val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
         val presenter = createRoomListPresenter(
@@ -268,7 +271,7 @@ class RoomListPresenterTest {
             assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
             encryptionService.emitRecoveryState(RecoveryState.DISABLED)
             assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SetUpRecovery)
-            nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt)
+            nextState.eventSink(RoomListEvents.DismissBanner)
             val finalState = awaitItem()
             assertThat(finalState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
             scope.cancel()
@@ -644,6 +647,10 @@ class RoomListPresenterTest {
         searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
         acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
         notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
+        logoutPresenter: DirectLogoutPresenter = object : DirectLogoutPresenter {
+            @Composable
+            override fun present() = aDirectLogoutState()
+        },
     ) = RoomListPresenter(
         client = client,
         networkMonitor = networkMonitor,
@@ -671,5 +678,6 @@ class RoomListPresenterTest {
         acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
         fullScreenIntentPermissionsPresenter = FakeFullScreenIntentPermissionsPresenter(),
         notificationCleaner = notificationCleaner,
+        logoutPresenter = logoutPresenter,
     )
 }
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt
index 1c218c9aaa8..63327d0d49a 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt
@@ -68,7 +68,7 @@ class RoomListViewTest {
 
         val close = rule.activity.getString(CommonStrings.action_close)
         rule.onNodeWithContentDescription(close).performClick()
-        eventsRecorder.assertSingle(RoomListEvents.DismissRecoveryKeyPrompt)
+        eventsRecorder.assertSingle(RoomListEvents.DismissBanner)
     }
 
     @Test
@@ -86,7 +86,7 @@ class RoomListViewTest {
 
         val close = rule.activity.getString(CommonStrings.action_close)
         rule.onNodeWithContentDescription(close).performClick()
-        eventsRecorder.assertSingle(RoomListEvents.DismissRecoveryKeyPrompt)
+        eventsRecorder.assertSingle(RoomListEvents.DismissBanner)
     }
 
     @Test
@@ -232,6 +232,21 @@ class RoomListViewTest {
             listOf(RoomListEvents.AcceptInvite(invitedRoom), RoomListEvents.DeclineInvite(invitedRoom)),
         )
     }
+
+    @Test
+    fun `clicking on logout and migrate calls the migration clicked callback`() {
+        val state = aRoomListState(
+            contentState = aRoomsContentState(securityBannerState = SecurityBannerState.NeedsNativeSlidingSyncMigration),
+            eventSink = {},
+        )
+        ensureCalledOnce { callback ->
+            rule.setRoomListView(
+                state = state,
+                onMigrateToNativeSlidingSyncClick = callback,
+            )
+            rule.clickOn(R.string.banner_migrate_to_native_sliding_sync_action)
+        }
+    }
 }
 
 private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomListView(
@@ -244,6 +259,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
     onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
     onMenuActionClick: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(),
     onRoomDirectorySearchClick: () -> Unit = EnsureNeverCalled(),
+    onMigrateToNativeSlidingSyncClick: () -> Unit = EnsureNeverCalled()
 ) {
     setContent {
         RoomListView(
@@ -256,6 +272,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
             onRoomSettingsClick = onRoomSettingsClick,
             onMenuActionClick = onMenuActionClick,
             onRoomDirectorySearchClick = onRoomDirectorySearchClick,
+            onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
             acceptDeclineInviteView = { },
         )
     }
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
index 040f24fb959..f4085bfb515 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
@@ -77,6 +77,8 @@ class FakeMatrixClient(
     private val clearCacheLambda: () -> Unit = { lambdaError() },
     private val userIdServerNameLambda: () -> String = { lambdaError() },
     private val getUrlLambda: (String) -> Result<String> = { lambdaError() },
+    var isNativeSlidingSyncSupportedLambda: suspend () -> Boolean = { true },
+    var isUsingNativeSlidingSyncLambda: () -> Boolean = { true },
 ) : MatrixClient {
     var setDisplayNameCalled: Boolean = false
         private set
@@ -316,4 +318,12 @@ class FakeMatrixClient(
     override suspend fun getUrl(url: String): Result<String> {
         return getUrlLambda(url)
     }
+
+    override suspend fun isNativeSlidingSyncSupported(): Boolean {
+        return isNativeSlidingSyncSupportedLambda()
+    }
+
+    override fun isUsingNativeSlidingSync(): Boolean {
+        return isUsingNativeSlidingSyncLambda()
+    }
 }
diff --git a/libraries/preferences/api/src/test/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCaseTest.kt b/libraries/preferences/api/src/test/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCaseTest.kt
new file mode 100644
index 00000000000..315c8f91d60
--- /dev/null
+++ b/libraries/preferences/api/src/test/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCaseTest.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.preferences.api.store
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class EnableNativeSlidingSyncUseCaseTest {
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun `ensure that the use case sets the simplified sliding sync enabled flag`() = runTest {
+        val preferencesStore = InMemoryAppPreferencesStore()
+        val useCase = EnableNativeSlidingSyncUseCase(preferencesStore, this)
+        assertThat(preferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse()
+
+        useCase()
+        advanceUntilIdle()
+
+        assertThat(preferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue()
+    }
+}
diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en.png
new file mode 100644
index 00000000000..d5c79327d58
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:45f8b8767d1a54afde878f1c853a7fd6dd5823d04db3b75a7c18c5d6fc742d8e
+size 36110
diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en.png
new file mode 100644
index 00000000000..f875fdea799
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6849a6c73341edc3f37fd6ef968c07e63277d18f833028386012fa2a438ade7e
+size 34946
diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png
new file mode 100644
index 00000000000..dfcee5c2f68
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f4ee87f2ae823b3d026b70d6b91463ba6aa3ad7e454b915c0eba3901b219169b
+size 72188
diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png
new file mode 100644
index 00000000000..ff6ac651553
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:863e7f66501f357728aa048560dc59fccd63bff8fc4b648296d82db119b0137d
+size 71031