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

Issue:
Finish OIDC integration
OIDC has been merged into the SDK

A couple of changes worth pointing out:

- [x] Client.accountURL contains a URL to be presented from an Account and devices button in the Settings screen.
- [x] Client.logout now returns an optional URL. When the URL is there, it should be presented to the user after logout for RP initiated logout on their account page.


Discussion hints:
nan

Pull request: Enable OIDC support
- upgrade to SDK 0.1.45

## Oidc

- enable OIDC (remove the TODO and update the code)
- add setting entry point to configure account (with Localazy English sync to get `screen_settings_oidc_account`)
- require a database migration to add `oidcData` field from the SDK.
- Open URL on signout if provided (no url is provided on element.io)

Closes #1114 

Tested with my element.io account, and it's working fine.

## Refresh token

With this new version of the SDK, we now have didRefreshTokens() callback.
- update session db in case of didRefreshTokens() invocation

Not tested, but added a unit test about it.

Test command: ./gradlew :features:preferences:impl:testDebugUnitTest --tests "io.element.android.features.preferences.impl.root.PreferencesRootPresenterTest" :features:login:impl:testDebugUnitTest --tests "io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenterTest"

Fix patch:
diff --git a/changelog.d/1127.feature b/changelog.d/1127.feature
new file mode 100644
index 00000000000..da2c6b3cf1b
--- /dev/null
+++ b/changelog.d/1127.feature
@@ -0,0 +1,1 @@
+Enable OIDC support.
diff --git a/docs/oidc.md b/docs/oidc.md
index 5f9e70268d4..0e4ad448529 100644
--- a/docs/oidc.md
+++ b/docs/oidc.md
@@ -45,3 +45,7 @@ state: ex6mNJVFZ5jn9wL8
 
 Oidc client example: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/examples/oidc_cli/src/main.rs
 Oidc sdk doc: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/crates/matrix-sdk/src/oidc.rs
+
+
+Test server:
+synapse-oidc.lab.element.dev
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt
index 1a021ad6056..61ef4d724b2 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt
@@ -17,6 +17,7 @@
 package io.element.android.features.login.impl.screens.confirmaccountprovider
 
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
@@ -26,8 +27,11 @@ import androidx.compose.runtime.rememberCoroutineScope
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.features.login.impl.DefaultLoginUserStory
 import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
 import io.element.android.features.login.impl.error.ChangeServerError
+import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
 import io.element.android.libraries.architecture.Async
 import io.element.android.libraries.architecture.Presenter
 import io.element.android.libraries.architecture.runCatchingUpdatingState
@@ -40,7 +44,9 @@ import java.net.URL
 class ConfirmAccountProviderPresenter @AssistedInject constructor(
     @Assisted private val params: Params,
     private val accountProviderDataSource: AccountProviderDataSource,
-    private val authenticationService: MatrixAuthenticationService
+    private val authenticationService: MatrixAuthenticationService,
+    private val defaultOidcActionFlow: DefaultOidcActionFlow,
+    private val defaultLoginUserStory: DefaultLoginUserStory,
 ) : Presenter<ConfirmAccountProviderState> {
 
     data class Params(
@@ -61,6 +67,14 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
             mutableStateOf(Async.Uninitialized)
         }
 
+        LaunchedEffect(Unit) {
+            launch {
+                defaultOidcActionFlow.collect {
+                    onOidcAction(it, loginFlowAction)
+                }
+            }
+        }
+
         fun handleEvents(event: ConfirmAccountProviderEvents) {
             when (event) {
                 ConfirmAccountProviderEvents.Continue -> {
@@ -97,4 +111,33 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
             }.getOrThrow()
         }.runCatchingUpdatingState(loginFlowAction, errorTransform = ChangeServerError::from)
     }
+
+    private suspend fun onOidcAction(
+        oidcAction: OidcAction?,
+        loginFlowAction: MutableState<Async<LoginFlow>>,
+    ) {
+        oidcAction ?: return
+        loginFlowAction.value = Async.Loading()
+        when (oidcAction) {
+            OidcAction.GoBack -> {
+                authenticationService.cancelOidcLogin()
+                    .onSuccess {
+                        loginFlowAction.value = Async.Uninitialized
+                    }
+                    .onFailure { failure ->
+                        loginFlowAction.value = Async.Failure(failure)
+                    }
+            }
+            is OidcAction.Success -> {
+                authenticationService.loginWithOidc(oidcAction.url)
+                    .onSuccess { _ ->
+                        defaultLoginUserStory.setLoginFlowIsDone(true)
+                    }
+                    .onFailure { failure ->
+                        loginFlowAction.value = Async.Failure(failure)
+                    }
+            }
+        }
+        defaultOidcActionFlow.reset()
+    }
 }
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt
index e8bcea990e5..152b1a094c5 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt
@@ -21,7 +21,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
 object LoginConstants {
     const val MATRIX_ORG_URL = "matrix.org"
 
-    const val DEFAULT_HOMESERVER_URL = "matrix.org" // TODO Oidc "synapse-oidc.lab.element.dev"
+    const val DEFAULT_HOMESERVER_URL = "matrix.org"
     const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
 }
 
diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceScreen.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceScreen.kt
index 60844f44771..f9f23ac9c45 100644
--- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceScreen.kt
+++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceScreen.kt
@@ -34,12 +34,12 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
 @Composable
 fun LogoutPreferenceView(
     state: LogoutPreferenceState,
-    onSuccessLogout: () -> Unit = {}
+    onSuccessLogout: (String?) -> Unit = {}
 ) {
     val eventSink = state.eventSink
     if (state.logoutAction is Async.Success) {
         LaunchedEffect(state.logoutAction) {
-            onSuccessLogout()
+            onSuccessLogout(state.logoutAction.data)
         }
         return
     }
diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceState.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceState.kt
index e5fd05ba8e6..95550ef4c84 100644
--- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceState.kt
+++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceState.kt
@@ -19,6 +19,6 @@ package io.element.android.features.logout.api
 import io.element.android.libraries.architecture.Async
 
 data class LogoutPreferenceState(
-    val logoutAction: Async<Unit>,
+    val logoutAction: Async<String?>,
     val eventSink: (LogoutPreferenceEvents) -> Unit,
 )
diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt
index e957755b98c..2fece4449b2 100644
--- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt
+++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt
@@ -40,7 +40,7 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli
     @Composable
     override fun present(): LogoutPreferenceState {
         val localCoroutineScope = rememberCoroutineScope()
-        val logoutAction: MutableState<Async<Unit>> = remember {
+        val logoutAction: MutableState<Async<String?>> = remember {
             mutableStateOf(Async.Uninitialized)
         }
 
@@ -56,7 +56,7 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli
         )
     }
 
-    private fun CoroutineScope.logout(logoutAction: MutableState<Async<Unit>>) = launch {
+    private fun CoroutineScope.logout(logoutAction: MutableState<Async<String?>>) = launch {
         suspend {
             matrixClient.logout()
         }.runCatchingUpdatingState(logoutAction)
diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml
index c88beff81a0..105cb1fc7bf 100644
--- a/features/messages/impl/src/main/res/values/localazy.xml
+++ b/features/messages/impl/src/main/res/values/localazy.xml
@@ -26,11 +26,13 @@
   <string name="screen_room_notification_settings_default_setting_footnote">"You can change it in your %1$s."</string>
   <string name="screen_room_notification_settings_default_setting_footnote_content_link">"global settings"</string>
   <string name="screen_room_notification_settings_default_setting_title">"Default setting"</string>
+  <string name="screen_room_notification_settings_edit_remove_setting">"Remove custom setting"</string>
   <string name="screen_room_notification_settings_error_loading_settings">"An error occurred while loading notification settings."</string>
   <string name="screen_room_notification_settings_error_restoring_default">"Failed restoring the default mode, please try again."</string>
   <string name="screen_room_notification_settings_error_setting_mode">"Failed setting the mode, please try again."</string>
   <string name="screen_room_notification_settings_mode_all_messages">"All messages"</string>
   <string name="screen_room_notification_settings_mode_mentions_and_keywords">"Mentions and Keywords only"</string>
+  <string name="screen_room_notification_settings_room_custom_settings_title">"In this room, notify me for"</string>
   <string name="screen_room_reactions_show_less">"Show less"</string>
   <string name="screen_room_reactions_show_more">"Show more"</string>
   <string name="screen_room_retry_send_menu_send_again_action">"Send again"</string>
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt
index a564927101f..e90569b40ea 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt
@@ -16,8 +16,10 @@
 
 package io.element.android.features.preferences.impl.root
 
+import android.app.Activity
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
 import com.bumble.appyx.core.modality.BuildContext
 import com.bumble.appyx.core.node.Node
 import com.bumble.appyx.core.plugin.Plugin
@@ -25,7 +27,9 @@ import com.bumble.appyx.core.plugin.plugins
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedInject
 import io.element.android.anvilannotations.ContributesNode
+import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
 import io.element.android.libraries.di.SessionScope
+import timber.log.Timber
 
 @ContributesNode(SessionScope::class)
 class PreferencesRootNode @AssistedInject constructor(
@@ -62,9 +66,16 @@ class PreferencesRootNode @AssistedInject constructor(
         plugins<Callback>().forEach { it.onOpenAbout() }
     }
 
+    private fun onManageAccountClicked(activity: Activity, accountManagementUrl: String?) {
+        accountManagementUrl?.let {
+            activity.openUrlInChromeCustomTab(null, false, it)
+        }
+    }
+
     @Composable
     override fun View(modifier: Modifier) {
         val state = presenter.present()
+        val activity = LocalContext.current as Activity
         PreferencesRootView(
             state = state,
             modifier = modifier,
@@ -73,7 +84,16 @@ class PreferencesRootNode @AssistedInject constructor(
             onOpenAnalytics = this::onOpenAnalytics,
             onOpenAbout = this::onOpenAbout,
             onVerifyClicked = this::onVerifyClicked,
-            onOpenDeveloperSettings = this::onOpenDeveloperSettings
+            onOpenDeveloperSettings = this::onOpenDeveloperSettings,
+            onSuccessLogout = { onSuccessLogout(activity, it) },
+            onManageAccountClicked = { onManageAccountClicked(activity, state.accountManagementUrl) },
         )
     }
+
+    private fun onSuccessLogout(activity: Activity, url: String?) {
+        Timber.d("Success logout with result url: $url")
+        url?.let {
+            activity.openUrlInChromeCustomTab(null, false, it)
+        }
+    }
 }
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt
index 0cd2e7f7db0..b33940b21e6 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt
@@ -68,6 +68,14 @@ class PreferencesRootPresenter @Inject constructor(
             derivedStateOf { sessionVerifiedStatus == SessionVerifiedStatus.NotVerified }
         }
 
+        val accountManagementUrl: MutableState<String?> = remember {
+            mutableStateOf(null)
+        }
+
+        LaunchedEffect(Unit) {
+            initAccountManagementUrl(accountManagementUrl)
+        }
+
         val logoutState = logoutPresenter.present()
         val showDeveloperSettings = buildType != BuildType.RELEASE
         return PreferencesRootState(
@@ -75,6 +83,7 @@ class PreferencesRootPresenter @Inject constructor(
             myUser = matrixUser.value,
             version = versionFormatter.get(),
             showCompleteVerification = sessionIsNotVerified,
+            accountManagementUrl = accountManagementUrl.value,
             showAnalyticsSettings = hasAnalyticsProviders,
             showDeveloperSettings = showDeveloperSettings,
             snackbarMessage = snackbarMessage,
@@ -84,4 +93,8 @@ class PreferencesRootPresenter @Inject constructor(
     private fun CoroutineScope.initialLoad(matrixUser: MutableState<MatrixUser?>) = launch {
         matrixUser.value = matrixClient.getCurrentUser()
     }
+
+    private fun CoroutineScope.initAccountManagementUrl(accountManagementUrl: MutableState<String?>) = launch {
+        accountManagementUrl.value = matrixClient.getAccountManagementUrl().getOrNull()
+    }
 }
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt
index 540c4708156..af3a0906305 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt
@@ -25,6 +25,7 @@ data class PreferencesRootState(
     val myUser: MatrixUser?,
     val version: String,
     val showCompleteVerification: Boolean,
+    val accountManagementUrl: String?,
     val showAnalyticsSettings: Boolean,
     val showDeveloperSettings: Boolean,
     val snackbarMessage: SnackbarMessage?,
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt
index e8c148267f8..931a560c1d6 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt
@@ -25,6 +25,7 @@ fun aPreferencesRootState() = PreferencesRootState(
     myUser = null,
     version = "Version 1.1 (1)",
     showCompleteVerification = true,
+    accountManagementUrl = "aUrl",
     showAnalyticsSettings = true,
     showDeveloperSettings = true,
     snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt
index 4b589ad2b00..c24a2ec8751 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt
@@ -23,6 +23,7 @@ import androidx.compose.material.icons.outlined.BugReport
 import androidx.compose.material.icons.outlined.DeveloperMode
 import androidx.compose.material.icons.outlined.Help
 import androidx.compose.material.icons.outlined.InsertChart
+import androidx.compose.material.icons.outlined.ManageAccounts
 import androidx.compose.material.icons.outlined.VerifiedUser
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
@@ -51,10 +52,12 @@ fun PreferencesRootView(
     state: PreferencesRootState,
     onBackPressed: () -> Unit,
     onVerifyClicked: () -> Unit,
+    onManageAccountClicked: () -> Unit,
     onOpenAnalytics: () -> Unit,
     onOpenRageShake: () -> Unit,
     onOpenAbout: () -> Unit,
     onOpenDeveloperSettings: () -> Unit,
+    onSuccessLogout: (String?) -> Unit,
     modifier: Modifier = Modifier,
 ) {
     val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
@@ -75,6 +78,13 @@ fun PreferencesRootView(
             )
             HorizontalDivider()
         }
+        if (state.accountManagementUrl != null) {
+            PreferenceText(
+                title = stringResource(id = CommonStrings.screen_settings_oidc_account),
+                icon = Icons.Outlined.ManageAccounts,
+                onClick = onManageAccountClicked,
+            )
+        }
         if (state.showAnalyticsSettings) {
             PreferenceText(
                 title = stringResource(id = CommonStrings.common_analytics),
@@ -98,6 +108,7 @@ fun PreferencesRootView(
         HorizontalDivider()
         LogoutPreferenceView(
             state = state.logoutState,
+            onSuccessLogout = onSuccessLogout,
         )
         Text(
             modifier = Modifier
@@ -140,5 +151,7 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
         onOpenDeveloperSettings = {},
         onOpenAbout = {},
         onVerifyClicked = {},
+        onSuccessLogout = {},
+        onManageAccountClicked = {},
     )
 }
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d79df0df1ad..d664e325307 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -146,7 +146,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
 appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
 molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
 timber = "com.jakewharton.timber:timber:5.0.1"
-matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.44"
+matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.45"
 sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
 sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
 sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
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 67c0625a916..be64a3371f1 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
@@ -55,9 +55,15 @@ interface MatrixClient : Closeable {
      * Will close the client and delete the cache data.
      */
     suspend fun clearCache()
-    suspend fun logout()
+
+    /**
+     * Logout the user.
+     * Returns an optional URL. When the URL is there, it should be presented to the user after logout for RP initiated logout on their account page.
+     */
+    suspend fun logout(): String?
     suspend fun loadUserDisplayName(): Result<String>
     suspend fun loadUserAvatarURLString(): Result<String?>
+    suspend fun getAccountManagementUrl(): Result<String?>
     suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
     fun roomMembershipObserver(): RoomMembershipObserver
 
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt
index 48712b7ddf2..9620dc6d7fc 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt
@@ -22,6 +22,5 @@ sealed class AuthenticationException(message: String) : Exception(message) {
     class SlidingSyncNotAvailable(message: String) : AuthenticationException(message)
     class SessionMissing(message: String) : AuthenticationException(message)
     class Generic(message: String) : AuthenticationException(message)
-    // TODO Oidc
-    // class OidcError(type: String, message: String) : AuthenticationException(message)
+    data class OidcError(val type: String, override val message: String) : AuthenticationException(message)
 }
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 8f5cfa496b5..c68913b7058 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
@@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
 import io.element.android.libraries.matrix.api.user.MatrixUser
 import io.element.android.libraries.matrix.api.verification.SessionVerificationService
 import io.element.android.libraries.matrix.impl.core.toProgressWatcher
+import io.element.android.libraries.matrix.impl.mapper.toSessionData
 import io.element.android.libraries.matrix.impl.media.RustMediaLoader
 import io.element.android.libraries.matrix.impl.notification.RustNotificationService
 import io.element.android.libraries.matrix.impl.pushers.RustPushersService
@@ -119,6 +120,13 @@ class RustMatrixClient constructor(
                 Timber.v("didReceiveAuthError -> already cleaning up")
             }
         }
+
+        override fun didRefreshTokens() {
+            Timber.w("didRefreshTokens()")
+            appCoroutineScope.launch {
+                sessionStore.updateData(client.session().toSessionData())
+            }
+        }
     }
 
     private val rustRoomListService: RoomListService =
@@ -287,21 +295,30 @@ class RustMatrixClient constructor(
         baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false)
     }
 
-    override suspend fun logout() = doLogout(doRequest = true)
+    override suspend fun logout(): String? = doLogout(doRequest = true)
 
-    private suspend fun doLogout(doRequest: Boolean) = withContext(sessionDispatcher) {
-        if (doRequest) {
-            try {
-                client.logout()
-            } catch (failure: Throwable) {
-                Timber.e(failure, "Fail to call logout on HS. Still delete local files.")
+    private suspend fun doLogout(doRequest: Boolean): String? {
+        var result: String? = null
+        withContext(sessionDispatcher) {
+            if (doRequest) {
+                try {
+                    result = client.logout()
+                } catch (failure: Throwable) {
+                    Timber.e(failure, "Fail to call logout on HS. Still delete local files.")
+                }
             }
+            close()
+            baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true)
+            sessionStore.removeSession(sessionId.value)
         }
-        close()
-        baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true)
-        sessionStore.removeSession(sessionId.value)
+        return result
     }
 
+    override suspend fun getAccountManagementUrl(): Result<String?> = withContext(sessionDispatcher) {
+        runCatching {
+            client.accountUrl()
+        }
+    }
     override suspend fun loadUserDisplayName(): Result<String> = withContext(sessionDispatcher) {
         runCatching {
             client.displayName()
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt
index 931133c2663..bb800322306 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt
@@ -75,4 +75,5 @@ private fun SessionData.toSession() = Session(
     deviceId = deviceId,
     homeserverUrl = homeserverUrl,
     slidingSyncProxy = slidingSyncProxy,
+    oidcData = oidcData,
 )
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt
index f0feb2857d7..8f7f63e503d 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt
@@ -26,15 +26,12 @@ fun Throwable.mapAuthenticationException(): AuthenticationException {
         is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(this.message!!)
         is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(this.message!!)
         is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(this.message!!)
-
-        /* TODO Oidc
         is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message!!)
         is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message!!)
         is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message!!)
-        is RustAuthenticationException.OidcNotStarted -> AuthenticationException.OidcError("OidcNotStarted", message!!)
         is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message!!)
-         */
-
+        is RustAuthenticationException.OidcCancelled -> AuthenticationException.OidcError("OidcCancelled", message!!)
+        is RustAuthenticationException.OidcCallbackUrlInvalid -> AuthenticationException.OidcError("OidcCallbackUrlInvalid", message!!)
         else -> AuthenticationException.Generic(this.message ?: "Unknown error")
     }
 }
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetails.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetails.kt
index a3d277c6da5..f1d3e34bf85 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetails.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetails.kt
@@ -23,6 +23,6 @@ fun HomeserverLoginDetails.map(): MatrixHomeServerDetails = use {
     MatrixHomeServerDetails(
         url = url(),
         supportsPasswordLogin = supportsPasswordLogin(),
-        supportsOidcLogin = false // TODO Oidc supportsOidcLogin(),
+        supportsOidcLogin = supportsOidcLogin(),
     )
 }
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt
index b5115ffad41..401fa0ce832 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt
@@ -16,17 +16,19 @@
 
 package io.element.android.libraries.matrix.impl.auth
 
-// TODO Oidc
-// import io.element.android.libraries.matrix.api.auth.OidcConfig
-// import org.matrix.rustcomponents.sdk.OidcClientMetadata
+import io.element.android.libraries.matrix.api.auth.OidcConfig
+import org.matrix.rustcomponents.sdk.OidcConfiguration
 
-/*
-val oidcClientMetadata: OidcClientMetadata = OidcClientMetadata(
+val oidcConfiguration: OidcConfiguration = OidcConfiguration(
     clientName = "Element",
     redirectUri = OidcConfig.redirectUri,
     clientUri = "https://element.io",
     tosUri = "https://element.io/user-terms-of-service",
-    policyUri = "https://element.io/privacy"
+    policyUri = "https://element.io/privacy",
+    /**
+     * Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually
+     */
+    staticRegistrations = mapOf(
+        "https://id.thirdroom.io/realms/thirdroom" to "elementx",
+    ),
 )
- */
-
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
index ba068910130..60146447336 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
@@ -16,8 +16,6 @@
 
 package io.element.android.libraries.matrix.impl.auth
 
-// TODO Oidc
-// import org.matrix.rustcomponents.sdk.OidcAuthenticationUrl
 import com.squareup.anvil.annotations.ContributesBinding
 import io.element.android.libraries.core.coroutine.CoroutineDispatchers
 import io.element.android.libraries.core.extensions.mapFailure
@@ -30,17 +28,16 @@ import io.element.android.libraries.matrix.api.auth.OidcDetails
 import io.element.android.libraries.matrix.api.core.SessionId
 import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
 import io.element.android.libraries.matrix.impl.exception.mapClientException
+import io.element.android.libraries.matrix.impl.mapper.toSessionData
 import io.element.android.libraries.network.useragent.UserAgentProvider
-import io.element.android.libraries.sessionstorage.api.SessionData
 import io.element.android.libraries.sessionstorage.api.SessionStore
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.withContext
-import org.matrix.rustcomponents.sdk.Session
+import org.matrix.rustcomponents.sdk.OidcAuthenticationData
 import org.matrix.rustcomponents.sdk.use
 import java.io.File
-import java.util.Date
 import javax.inject.Inject
 import org.matrix.rustcomponents.sdk.AuthenticationService as RustAuthenticationService
 
@@ -57,9 +54,8 @@ class RustMatrixAuthenticationService @Inject constructor(
     private val authService: RustAuthenticationService = RustAuthenticationService(
         basePath = baseDirectory.absolutePath,
         passphrase = null,
-        // TODO Oidc
-        // oidcClientMetadata = oidcClientMetadata,
         userAgent = userAgentProvider.provide(),
+        oidcConfiguration = oidcConfiguration,
         customSlidingSyncProxy = null,
     )
     private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
@@ -112,68 +108,48 @@ class RustMatrixAuthenticationService @Inject constructor(
             }
         }
 
-    // TODO Oidc
-    //  private var pendingUrlForOidcLogin: OidcAuthenticationUrl? = null
+    private var pendingOidcAuthenticationData: OidcAuthenticationData? = null
 
     override suspend fun getOidcUrl(): Result<OidcDetails> {
-        TODO("Oidc")
-        /*
         return withContext(coroutineDispatchers.io) {
             runCatching {
-                val urlForOidcLogin = authService.urlForOidcLogin()
-                val url = urlForOidcLogin.loginUrl()
-                pendingUrlForOidcLogin = urlForOidcLogin
+                val oidcAuthenticationData = authService.urlForOidcLogin()
+                val url = oidcAuthenticationData.loginUrl()
+                pendingOidcAuthenticationData = oidcAuthenticationData
                 OidcDetails(url)
             }.mapFailure { failure ->
                 failure.mapAuthenticationException()
             }
         }
-         */
     }
 
     override suspend fun cancelOidcLogin(): Result<Unit> {
-        TODO("Oidc")
-        /*
         return withContext(coroutineDispatchers.io) {
             runCatching {
-                pendingUrlForOidcLogin?.close()
-                pendingUrlForOidcLogin = null
+                pendingOidcAuthenticationData?.close()
+                pendingOidcAuthenticationData = null
             }.mapFailure { failure ->
                 failure.mapAuthenticationException()
             }
         }
-         */
     }
 
     /**
      * callbackUrl should be the uriRedirect from OidcClientMetadata (with all the parameters).
      */
     override suspend fun loginWithOidc(callbackUrl: String): Result<SessionId> {
-        TODO("Oidc")
-        /*
         return withContext(coroutineDispatchers.io) {
             runCatching {
-                val urlForOidcLogin = pendingUrlForOidcLogin ?: error("You need to call `getOidcUrl()` first")
+                val urlForOidcLogin = pendingOidcAuthenticationData ?: error("You need to call `getOidcUrl()` first")
                 val client = authService.loginWithOidcCallback(urlForOidcLogin, callbackUrl)
                 val sessionData = client.use { it.session().toSessionData() }
-                pendingUrlForOidcLogin = null
+                pendingOidcAuthenticationData?.close()
+                pendingOidcAuthenticationData = null
                 sessionStore.storeData(sessionData)
                 SessionId(sessionData.userId)
             }.mapFailure { failure ->
                 failure.mapAuthenticationException()
             }
         }
-         */
     }
-
 }
-
-private fun Session.toSessionData() = SessionData(
-    userId = userId,
-    deviceId = deviceId,
-    accessToken = accessToken,
-    refreshToken = refreshToken,
-    homeserverUrl = homeserverUrl,
-    slidingSyncProxy = slidingSyncProxy,
-    loginTimestamp = Date(),
-)
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt
new file mode 100644
index 00000000000..825c6f4397b
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.matrix.impl.mapper
+
+import io.element.android.libraries.sessionstorage.api.SessionData
+import org.matrix.rustcomponents.sdk.Session
+import java.util.Date
+
+internal fun Session.toSessionData() = SessionData(
+    userId = userId,
+    deviceId = deviceId,
+    accessToken = accessToken,
+    refreshToken = refreshToken,
+    homeserverUrl = homeserverUrl,
+    oidcData = oidcData,
+    slidingSyncProxy = slidingSyncProxy,
+    loginTimestamp = Date(),
+)
diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt
index cc106f960a1..e14c3feeab4 100644
--- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt
+++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt
@@ -24,6 +24,7 @@ data class SessionData(
     val accessToken: String,
     val refreshToken: String?,
     val homeserverUrl: String,
+    val oidcData: String?,
     val slidingSyncProxy: String?,
     val loginTimestamp: Date?,
 )
diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt
index d79d700030c..2b3398f76ce 100644
--- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt
+++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt
@@ -23,6 +23,12 @@ interface SessionStore {
     fun isLoggedIn(): Flow<Boolean>
     fun sessionsFlow(): Flow<List<SessionData>>
     suspend fun storeData(sessionData: SessionData)
+
+    /**
+     * Will update the session data matching the userId, except the value of loginTimestamp.
+     * No op if userId is not found in DB.
+     */
+    suspend fun updateData(sessionData: SessionData)
     suspend fun getSession(sessionId: String): SessionData?
     suspend fun getAllSessions(): List<SessionData>
     suspend fun getLatestSession(): SessionData?
diff --git a/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt b/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt
index e23e34983c3..df78149eef9 100644
--- a/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt
+++ b/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt
@@ -38,6 +38,10 @@ class InMemorySessionStore : SessionStore {
         sessionDataFlow.value = sessionData
     }
 
+    override suspend fun updateData(sessionData: SessionData) {
+        sessionDataFlow.value = sessionData
+    }
+
     override suspend fun getSession(sessionId: String): SessionData? {
         return sessionDataFlow.value.takeIf { it?.userId == sessionId }
     }
diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt
index 9394b66e66d..eb273411a01 100644
--- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt
+++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt
@@ -46,6 +46,24 @@ class DatabaseSessionStore @Inject constructor(
         database.sessionDataQueries.insertSessionData(sessionData.toDbModel())
     }
 
+    override suspend fun updateData(sessionData: SessionData) {
+        val result = database.sessionDataQueries.selectByUserId(sessionData.userId)
+            .executeAsOneOrNull()
+            ?.toApiModel()
+
+        if (result == null) {
+            Timber.e("User ${sessionData.userId} not found in session database")
+            return
+        }
+
+        // Copy new data from SDK, but keep login timestamp
+        database.sessionDataQueries.updateSession(
+            sessionData.copy(
+                loginTimestamp = result.loginTimestamp,
+            ).toDbModel()
+        )
+    }
+
     override suspend fun getLatestSession(): SessionData? {
         return database.sessionDataQueries.selectFirst()
             .executeAsOneOrNull()
diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt
index dbb42a8451f..d0c89d9896f 100644
--- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt
+++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt
@@ -27,6 +27,7 @@ internal fun SessionData.toDbModel(): DbSessionData {
         accessToken = accessToken,
         refreshToken = refreshToken,
         homeserverUrl = homeserverUrl,
+        oidcData = oidcData,
         slidingSyncProxy = slidingSyncProxy,
         loginTimestamp = loginTimestamp?.time,
     )
@@ -39,6 +40,7 @@ internal fun DbSessionData.toApiModel(): SessionData {
         accessToken = accessToken,
         refreshToken = refreshToken,
         homeserverUrl = homeserverUrl,
+        oidcData = oidcData,
         slidingSyncProxy = slidingSyncProxy,
         loginTimestamp = loginTimestamp?.let { Date(it) }
     )
diff --git a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq
index c3123f2ffb7..05049c56357 100644
--- a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq
+++ b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq
@@ -5,7 +5,8 @@ CREATE TABLE SessionData (
     refreshToken TEXT,
     homeserverUrl TEXT NOT NULL,
     slidingSyncProxy TEXT,
-    loginTimestamp INTEGER
+    loginTimestamp INTEGER,
+    oidcData TEXT
 );
 
 
@@ -23,3 +24,6 @@ INSERT INTO SessionData VALUES ?;
 
 removeSession:
 DELETE FROM SessionData WHERE userId = ?;
+
+updateSession:
+REPLACE INTO SessionData VALUES ?;
diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/2.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/2.sqm
new file mode 100644
index 00000000000..9fc7f2fdaab
--- /dev/null
+++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/2.sqm
@@ -0,0 +1,1 @@
+ALTER TABLE SessionData ADD COLUMN oidcData TEXT;
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index 3b4c305ffc6..e38c4fc6b83 100644
--- a/libraries/ui-strings/src/main/res/values/localazy.xml
+++ b/libraries/ui-strings/src/main/res/values/localazy.xml
@@ -180,11 +180,21 @@
   <string name="screen_migration_title">"Setting up your account."</string>
   <string name="screen_notification_settings_additional_settings_section_title">"Additional settings"</string>
   <string name="screen_notification_settings_calls_label">"Audio and video calls"</string>
+  <string name="screen_notification_settings_configuration_mismatch">"Configuration mismatch"</string>
+  <string name="screen_notification_settings_configuration_mismatch_description">"We’ve simplified Notifications Settings to make options easier to find. 
+
+Some custom settings you’ve chosen in the past are not shown here, but they’re still active. 
+
+If you proceed, some of your settings may change."</string>
   <string name="screen_notification_settings_direct_chats">"Direct chats"</string>
+  <string name="screen_notification_settings_edit_custom_settings_section_title">"Custom setting per chat"</string>
   <string name="screen_notification_settings_edit_failed_updating_default_mode">"An error occurred while updating the notification setting."</string>
+  <string name="screen_notification_settings_edit_mode_all_messages">"All messages"</string>
+  <string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Mentions and Keywords only"</string>
   <string name="screen_notification_settings_edit_screen_direct_section_header">"On direct chats, notify me for"</string>
   <string name="screen_notification_settings_edit_screen_group_section_header">"On group chats, notify me for"</string>
   <string name="screen_notification_settings_enable_notifications">"Enable notifications on this device"</string>
+  <string name="screen_notification_settings_failed_fixing_configuration">"The configuration has not been corrected, please try again."</string>
   <string name="screen_notification_settings_group_chats">"Group chats"</string>
   <string name="screen_notification_settings_mentions_section_title">"Mentions"</string>
   <string name="screen_notification_settings_mode_all">"All"</string>
@@ -196,6 +206,7 @@
   <string name="screen_notification_settings_system_notifications_turned_off">"System notifications turned off"</string>
   <string name="screen_notification_settings_title">"Notifications"</string>
   <string name="screen_report_content_block_user_hint">"Check if you want to hide all current and future messages from this user"</string>
+  <string name="screen_settings_oidc_account">"Account and devices"</string>
   <string name="screen_share_location_title">"Share location"</string>
   <string name="screen_share_my_location_action">"Share my location"</string>
   <string name="screen_share_open_apple_maps">"Open in Apple Maps"</string>


Test patch:
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt
index 76a3ad3d22e..95cd9bf053d 100644
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt
@@ -20,24 +20,25 @@ import app.cash.molecule.RecompositionMode
 import app.cash.molecule.moleculeFlow
 import app.cash.turbine.test
 import com.google.common.truth.Truth.assertThat
+import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.features.login.impl.DefaultLoginUserStory
 import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
+import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
 import io.element.android.features.login.impl.util.defaultAccountProvider
 import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
 import io.element.android.libraries.matrix.test.A_HOMESERVER
 import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
 import io.element.android.libraries.matrix.test.A_THROWABLE
 import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
+import io.element.android.tests.testutils.waitForPredicate
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 
 class ConfirmAccountProviderPresenterTest {
     @Test
     fun `present - initial test`() = runTest {
-        val presenter = ConfirmAccountProviderPresenter(
-            ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
-            AccountProviderDataSource(),
-            FakeAuthenticationService(),
-        )
+        val presenter = createConfirmAccountProviderPresenter()
         moleculeFlow(RecompositionMode.Immediate) {
             presenter.present()
         }.test {
@@ -51,13 +52,11 @@ class ConfirmAccountProviderPresenterTest {
 
     @Test
     fun `present - continue password login`() = runTest {
-        val authServer = FakeAuthenticationService()
-        val presenter = ConfirmAccountProviderPresenter(
-            ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
-            AccountProviderDataSource(),
-            authServer,
+        val authenticationService = FakeAuthenticationService()
+        val presenter = createConfirmAccountProviderPresenter(
+            matrixAuthenticationService = authenticationService,
         )
-        authServer.givenHomeserver(A_HOMESERVER)
+        authenticationService.givenHomeserver(A_HOMESERVER)
         moleculeFlow(RecompositionMode.Immediate) {
             presenter.present()
         }.test {
@@ -75,13 +74,63 @@ class ConfirmAccountProviderPresenterTest {
 
     @Test
     fun `present - continue oidc`() = runTest {
-        val authServer = FakeAuthenticationService()
-        val presenter = ConfirmAccountProviderPresenter(
-            ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
-            AccountProviderDataSource(),
-            authServer,
+        val authenticationService = FakeAuthenticationService()
+        val presenter = createConfirmAccountProviderPresenter(
+            matrixAuthenticationService = authenticationService,
+        )
+        authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
+        moleculeFlow(RecompositionMode.Immediate) {
+            presenter.present()
+        }.test {
+            val initialState = awaitItem()
+            initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
+            val loadingState = awaitItem()
+            assertThat(loadingState.submitEnabled).isTrue()
+            assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
+            val successState = awaitItem()
+            assertThat(successState.submitEnabled).isFalse()
+            assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
+            assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
+        }
+    }
+
+    @Test
+    fun `present - oidc - cancel with failure`() = runTest {
+        val authenticationService = FakeAuthenticationService()
+        val defaultOidcActionFlow = DefaultOidcActionFlow()
+        val presenter = createConfirmAccountProviderPresenter(
+            matrixAuthenticationService = authenticationService,
+            defaultOidcActionFlow = defaultOidcActionFlow,
+        )
+        authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
+        moleculeFlow(RecompositionMode.Immediate) {
+            presenter.present()
+        }.test {
+            val initialState = awaitItem()
+            initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
+            val loadingState = awaitItem()
+            assertThat(loadingState.submitEnabled).isTrue()
+            assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
+            val successState = awaitItem()
+            assertThat(successState.submitEnabled).isFalse()
+            assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
+            assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
+            authenticationService.givenOidcCancelError(A_THROWABLE)
+            defaultOidcActionFlow.post(OidcAction.GoBack)
+            val cancelFailureState = awaitItem()
+            assertThat(cancelFailureState.loginFlow).isInstanceOf(Async.Failure::class.java)
+        }
+    }
+
+    @Test
+    fun `present - oidc - cancel with success`() = runTest {
+        val authenticationService = FakeAuthenticationService()
+        val defaultOidcActionFlow = DefaultOidcActionFlow()
+        val presenter = createConfirmAccountProviderPresenter(
+            matrixAuthenticationService = authenticationService,
+            defaultOidcActionFlow = defaultOidcActionFlow,
         )
-        authServer.givenHomeserver(A_HOMESERVER_OIDC)
+        authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
         moleculeFlow(RecompositionMode.Immediate) {
             presenter.present()
         }.test {
@@ -94,22 +143,86 @@ class ConfirmAccountProviderPresenterTest {
             assertThat(successState.submitEnabled).isFalse()
             assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
             assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
+            defaultOidcActionFlow.post(OidcAction.GoBack)
+            val cancelFinalState = awaitItem()
+            assertThat(cancelFinalState.loginFlow).isInstanceOf(Async.Uninitialized::class.java)
+        }
+    }
+
+    @Test
+    fun `present - oidc - success with failure`() = runTest {
+        val authenticationService = FakeAuthenticationService()
+        val defaultOidcActionFlow = DefaultOidcActionFlow()
+        val presenter = createConfirmAccountProviderPresenter(
+            matrixAuthenticationService = authenticationService,
+            defaultOidcActionFlow = defaultOidcActionFlow,
+        )
+        authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
+        moleculeFlow(RecompositionMode.Immediate) {
+            presenter.present()
+        }.test {
+            val initialState = awaitItem()
+            initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
+            val loadingState = awaitItem()
+            assertThat(loadingState.submitEnabled).isTrue()
+            assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
+            val successState = awaitItem()
+            assertThat(successState.submitEnabled).isFalse()
+            assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
+            assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
+            authenticationService.givenLoginError(A_THROWABLE)
+            defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
+            val cancelLoadingState = awaitItem()
+            assertThat(cancelLoadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
+            val cancelFailureState = awaitItem()
+            assertThat(cancelFailureState.loginFlow).isInstanceOf(Async.Failure::class.java)
+        }
+    }
+
+    @Test
+    fun `present - oidc - success with success`() = runTest {
+        val authenticationService = FakeAuthenticationService()
+        val defaultOidcActionFlow = DefaultOidcActionFlow()
+        val defaultLoginUserStory = DefaultLoginUserStory().apply {
+            setLoginFlowIsDone(false)
+        }
+        val presenter = createConfirmAccountProviderPresenter(
+            matrixAuthenticationService = authenticationService,
+            defaultOidcActionFlow = defaultOidcActionFlow,
+            defaultLoginUserStory = defaultLoginUserStory,
+        )
+        authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
+        moleculeFlow(RecompositionMode.Immediate) {
+            presenter.present()
+        }.test {
+            val initialState = awaitItem()
+            initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
+            val loadingState = awaitItem()
+            assertThat(loadingState.submitEnabled).isTrue()
+            assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
+            val successState = awaitItem()
+            assertThat(successState.submitEnabled).isFalse()
+            assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
+            assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
+            assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse()
+            defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
+            val successSuccessState = awaitItem()
+            assertThat(successSuccessState.loginFlow).isInstanceOf(Async.Loading::class.java)
+            waitForPredicate { defaultLoginUserStory.loginFlowIsDone.value }
         }
     }
 
     @Test
     fun `present - submit fails`() = runTest {
-        val authServer = FakeAuthenticationService()
-        val presenter = ConfirmAccountProviderPresenter(
-            ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
-            AccountProviderDataSource(),
-            authServer,
+        val authenticationService = FakeAuthenticationService()
+        val presenter = createConfirmAccountProviderPresenter(
+            matrixAuthenticationService = authenticationService,
         )
         moleculeFlow(RecompositionMode.Immediate) {
             presenter.present()
         }.test {
             val initialState = awaitItem()
-            authServer.givenChangeServerError(Throwable())
+            authenticationService.givenChangeServerError(Throwable())
             initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
             skipItems(1) // Loading
             val failureState = awaitItem()
@@ -121,10 +234,8 @@ class ConfirmAccountProviderPresenterTest {
     @Test
     fun `present - clear error`() = runTest {
         val authenticationService = FakeAuthenticationService()
-        val presenter = ConfirmAccountProviderPresenter(
-            ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
-            AccountProviderDataSource(),
-            authenticationService,
+        val presenter = createConfirmAccountProviderPresenter(
+            matrixAuthenticationService = authenticationService,
         )
         moleculeFlow(RecompositionMode.Immediate) {
             presenter.present()
@@ -147,4 +258,18 @@ class ConfirmAccountProviderPresenterTest {
             assertThat(clearedState.loginFlow).isEqualTo(Async.Uninitialized)
         }
     }
+
+    private fun createConfirmAccountProviderPresenter(
+        params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
+        accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(),
+        matrixAuthenticationService: MatrixAuthenticationService = FakeAuthenticationService(),
+        defaultOidcActionFlow: DefaultOidcActionFlow = DefaultOidcActionFlow(),
+        defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(),
+    ) = ConfirmAccountProviderPresenter(
+        params = params,
+        accountProviderDataSource = accountProviderDataSource,
+        authenticationService = matrixAuthenticationService,
+        defaultOidcActionFlow = defaultOidcActionFlow,
+        defaultLoginUserStory = defaultLoginUserStory,
+    )
 }
diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt
index 580426fcfa9..5d508912a88 100644
--- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt
+++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt
@@ -64,6 +64,7 @@ class PreferencesRootPresenterTest {
             )
             assertThat(loadedState.showDeveloperSettings).isEqualTo(true)
             assertThat(loadedState.showAnalyticsSettings).isEqualTo(false)
+            assertThat(loadedState.accountManagementUrl).isNull()
         }
     }
 }
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 1229836e302..83f7f3ad799 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
@@ -51,6 +51,7 @@ class FakeMatrixClient(
     private val pushersService: FakePushersService = FakePushersService(),
     private val notificationService: FakeNotificationService = FakeNotificationService(),
     private val syncService: FakeSyncService = FakeSyncService(),
+    private val accountManagementUrlString: Result<String?> = Result.success(null),
 ) : MatrixClient {
 
     private var ignoreUserResult: Result<Unit> = Result.success(Unit)
@@ -109,9 +110,10 @@ class FakeMatrixClient(
     override suspend fun clearCache() {
     }
 
-    override suspend fun logout() {
+    override suspend fun logout(): String? {
         delay(100)
         logoutFailure?.let { throw it }
+        return null
     }
 
     override fun close() = Unit
@@ -124,6 +126,9 @@ class FakeMatrixClient(
         return userAvatarURLString
     }
 
+    override suspend fun getAccountManagementUrl(): Result<String?> {
+        return accountManagementUrlString
+    }
     override suspend fun uploadMedia(
         mimeType: String,
         data: ByteArray,
diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt
index fc24c5a0116..e035ff9ae1a 100644
--- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt
+++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt
@@ -37,6 +37,7 @@ class DatabaseSessionStoreTests {
         homeserverUrl = "homeserverUrl",
         slidingSyncProxy = null,
         loginTimestamp = null,
+        oidcData = "aOidcData",
     )
 
     @Before
@@ -108,4 +109,45 @@ class DatabaseSessionStoreTests {
 
         assertThat(database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOneOrNull()).isNull()
     }
+
+    @Test
+    fun `update session update all fields except loginTimestamp`() = runTest {
+        val firstSessionData = SessionData(
+            userId = "userId",
+            deviceId = "deviceId",
+            accessToken = "accessToken",
+            refreshToken = "refreshToken",
+            homeserverUrl = "homeserverUrl",
+            slidingSyncProxy = "slidingSyncProxy",
+            loginTimestamp = 1,
+            oidcData = "aOidcData",
+        )
+        val secondSessionData = SessionData(
+            userId = "userId",
+            deviceId = "deviceIdAltered",
+            accessToken = "accessTokenAltered",
+            refreshToken = "refreshTokenAltered",
+            homeserverUrl = "homeserverUrlAltered",
+            slidingSyncProxy = "slidingSyncProxyAltered",
+            loginTimestamp = 2,
+            oidcData = "aOidcDataAltered",
+        )
+        assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId)
+        assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp)
+
+        database.sessionDataQueries.insertSessionData(firstSessionData)
+        databaseSessionStore.updateData(secondSessionData.toApiModel())
+
+        // Get the altered session
+        val alteredSession = databaseSessionStore.getSession(firstSessionData.userId)!!.toDbModel()
+
+        assertThat(alteredSession.userId).isEqualTo(secondSessionData.userId)
+        assertThat(alteredSession.deviceId).isEqualTo(secondSessionData.deviceId)
+        assertThat(alteredSession.accessToken).isEqualTo(secondSessionData.accessToken)
+        assertThat(alteredSession.refreshToken).isEqualTo(secondSessionData.refreshToken)
+        assertThat(alteredSession.homeserverUrl).isEqualTo(secondSessionData.homeserverUrl)
+        assertThat(alteredSession.slidingSyncProxy).isEqualTo(secondSessionData.slidingSyncProxy)
+        assertThat(alteredSession.loginTimestamp).isEqualTo(/* Not altered! */ firstSessionData.loginTimestamp)
+        assertThat(alteredSession.oidcData).isEqualTo(secondSessionData.oidcData)
+    }
 }
diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/WaitingForAssertion.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/WaitingForAssertion.kt
new file mode 100644
index 00000000000..6818aafc5b9
--- /dev/null
+++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/WaitingForAssertion.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.tests.testutils
+
+import kotlinx.coroutines.delay
+
+suspend fun waitForPredicate(
+    delayBetweenAttemptsMillis: Long = 1,
+    maxNumberOfAttempts: Int = 20,
+    predicate: () -> Boolean,
+) {
+    for (i in 0..maxNumberOfAttempts) {
+        if (predicate()) return
+        if (i < maxNumberOfAttempts) delay(delayBetweenAttemptsMillis)
+    }
+    throw AssertionError("Predicate was not true after $maxNumberOfAttempts attempts")
+}
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,en].png
index 9202a41eb06..d590c4eb8e7 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:2a80a93fd971e46b3a62c5e2679bfdab30cc01589398842346a2c7815af299fe
-size 35413
+oid sha256:72eee76cc8244eb54f147fc589c7b200dc3a46db4ea7306dbd6757918e4fffde
+size 39744
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,en].png
index 7def318962a..a3449da98e3 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:49d6cc10dee437df8428616173de9b94b1b80c4b9edc72057e64b6dd51fa608d
-size 34701
+oid sha256:8796e5f70cdd09087ed22ede78c3aed985dcd57e073f68d83dd5884e4f29a2c8
+size 39042
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,en].png
index 3eb39d88627..370689a2fc5 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:522926ef7065fab0176b686e49cf27ad29d4fd44d18d462cb156d45332cf368d
-size 37511
+oid sha256:97250f48dfa0cf2320f837eb87ad90d8a1e73642fbb2d571f326d689c4fc10e0
+size 42373
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,en].png
index 914485e483c..6bf7f8fdb02 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:6aaddaf0c8b536ddc231c74b6bdcb22114fca8e7445e97af91c064333873af2c
-size 37614
+oid sha256:73f2811197c014d91834d39dad1fe18abdaea9b2510d8deccead39c10a1b5aa8
+size 42473