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

Issue:
[Story] Crypto identity reset
See details in the parent ticket


Discussion hints:

Pull request: Feature: identity reset
<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
 
## Content

- Moves OIDC related code to their own modules: `libraries/oidc`.
- Improves the custom chrome tab implementation and fixes issues with the fallback webview one (it was freezing after signing in).
- Creates `OidcEntryPoint` which allows opening any of the OIDC flow options (custom tab, webview).
- Adds new flows for identity reset: root screen with confirmation, OIDC one which uses the extracted OIDC logic, user+password one.
- Create `IdentityResetHandle` wrappers for both OIDC and password variants.

## Motivation and context

Closes #3268.

## Screenshots / GIFs

In the PR.

## Tests

With a user + password based account whose data you don't mind losing:

- Log in.
- Select 'Can't confirm?'.
- Continue, add your password and confirm.
- You should be fully logged in now and the system will ask you to add key backup.

With an OIDC one (maybe using `synapse-oidc.element.dev`?):

- Log in.
- Select 'Can't confirm?'.
- Continue, confirm the reset in the custom tab/webview, then go back.
- You should be fully logged in now and the system will ask you to add key backup.

## Tested devices

- [ ] Physical
- [x] Emulator
- OS version(s): 13

## 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:securebackup:impl:testDebugUnitTest --tests "io.element.android.features.securebackup.impl.reset.root.ResetIdentityRootViewTest" --tests "io.element.android.features.securebackup.impl.reset.password.ResetIdentityPasswordViewTest" --tests "io.element.android.features.securebackup.impl.reset.root.ResetIdentityRootPresenterTest" --tests "io.element.android.features.securebackup.impl.reset.ResetIdentityFlowManagerTest" --tests "io.element.android.features.securebackup.impl.reset.password.ResetIdentityPasswordPresenterTest" :features:login:impl:testDebugUnitTest --tests "io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenterTest" :features:verifysession:impl:testDebugUnitTest --tests "io.element.android.features.verifysession.impl.VerifySelfSessionViewTest" :libraries:oidc:impl:testDebugUnitTest --tests "io.element.android.libraries.oidc.impl.OidcUrlParserTest" --tests "io.element.android.libraries.oidc.impl.webview.OidcPresenterTest" :appnav:testDebugUnitTest --tests "io.element.android.appnav.intent.IntentResolverTest"

Fix patch:
diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts
index 53680fd44ea..887f48e203b 100644
--- a/appnav/build.gradle.kts
+++ b/appnav/build.gradle.kts
@@ -42,6 +42,7 @@ dependencies {
     implementation(projects.libraries.architecture)
     implementation(projects.libraries.deeplink)
     implementation(projects.libraries.matrix.api)
+    implementation(projects.libraries.oidc.api)
     implementation(projects.libraries.preferences.api)
     implementation(projects.libraries.push.api)
     implementation(projects.libraries.pushproviders.api)
@@ -66,6 +67,7 @@ dependencies {
     testImplementation(libs.test.truth)
     testImplementation(libs.test.turbine)
     testImplementation(projects.libraries.matrix.test)
+    testImplementation(projects.libraries.oidc.impl)
     testImplementation(projects.libraries.push.test)
     testImplementation(projects.libraries.pushproviders.test)
     testImplementation(projects.features.networkmonitor.test)
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
index d200acb84e8..87e08b6bb90 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
@@ -42,8 +42,6 @@ import io.element.android.appnav.intent.ResolvedIntent
 import io.element.android.appnav.root.RootNavStateFlowFactory
 import io.element.android.appnav.root.RootPresenter
 import io.element.android.appnav.root.RootView
-import io.element.android.features.login.api.oidc.OidcAction
-import io.element.android.features.login.api.oidc.OidcActionFlow
 import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
 import io.element.android.features.signedout.api.SignedOutEntryPoint
 import io.element.android.features.viewfolder.api.ViewFolderEntryPoint
@@ -58,6 +56,8 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
 import io.element.android.libraries.matrix.api.core.SessionId
 import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
 import io.element.android.libraries.matrix.api.permalink.PermalinkData
+import io.element.android.libraries.oidc.api.OidcAction
+import io.element.android.libraries.oidc.api.OidcActionFlow
 import io.element.android.libraries.sessionstorage.api.LoggedInState
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.launchIn
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
index d41fbbfebb3..73ad6f60085 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
@@ -17,12 +17,12 @@
 package io.element.android.appnav.intent
 
 import android.content.Intent
-import io.element.android.features.login.api.oidc.OidcAction
-import io.element.android.features.login.api.oidc.OidcIntentResolver
 import io.element.android.libraries.deeplink.DeeplinkData
 import io.element.android.libraries.deeplink.DeeplinkParser
 import io.element.android.libraries.matrix.api.permalink.PermalinkData
 import io.element.android.libraries.matrix.api.permalink.PermalinkParser
+import io.element.android.libraries.oidc.api.OidcAction
+import io.element.android.libraries.oidc.api.OidcIntentResolver
 import timber.log.Timber
 import javax.inject.Inject
 
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt
index a5dc8402670..3e4ed218a5a 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt
@@ -58,6 +58,9 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
 
         @Parcelize
         data object EnterRecoveryKey : NavTarget
+
+        @Parcelize
+        data object ResetIdentity : NavTarget
     }
 
     interface Callback : Plugin {
@@ -85,6 +88,10 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
                         override fun onDone() {
                             plugins<Callback>().forEach { it.onDone() }
                         }
+
+                        override fun onResetKey() {
+                            backstack.push(NavTarget.ResetIdentity)
+                        }
                     })
                     .build()
             }
@@ -94,6 +101,16 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
                     .callback(secureBackupEntryPointCallback)
                     .build()
             }
+            is NavTarget.ResetIdentity -> {
+                secureBackupEntryPoint.nodeBuilder(this, buildContext)
+                    .params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.ResetIdentity))
+                    .callback(object : SecureBackupEntryPoint.Callback {
+                        override fun onDone() {
+                            plugins<Callback>().forEach { it.onDone() }
+                        }
+                    })
+                    .build()
+            }
         }
     }
 
diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts
index 6fcdc2c1de3..3fbe55f1f9e 100644
--- a/features/login/impl/build.gradle.kts
+++ b/features/login/impl/build.gradle.kts
@@ -50,6 +50,7 @@ dependencies {
     implementation(projects.libraries.uiStrings)
     implementation(projects.libraries.permissions.api)
     implementation(projects.libraries.qrcode)
+    implementation(projects.libraries.oidc.api)
     implementation(libs.androidx.browser)
     implementation(platform(libs.network.retrofit.bom))
     implementation(libs.network.retrofit)
@@ -65,6 +66,7 @@ dependencies {
     testImplementation(libs.test.truth)
     testImplementation(libs.test.turbine)
     testImplementation(projects.libraries.matrix.test)
+    testImplementation(projects.libraries.oidc.impl)
     testImplementation(projects.libraries.permissions.test)
     testImplementation(projects.tests.testutils)
     testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
index 0af102a4038..13aac03de4a 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
@@ -36,12 +36,7 @@ import dagger.assisted.AssistedInject
 import io.element.android.anvilannotations.ContributesNode
 import io.element.android.compound.theme.ElementTheme
 import io.element.android.features.login.api.LoginFlowType
-import io.element.android.features.login.api.oidc.OidcAction
-import io.element.android.features.login.api.oidc.OidcActionFlow
 import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
-import io.element.android.features.login.impl.oidc.CustomTabAvailabilityChecker
-import io.element.android.features.login.impl.oidc.customtab.CustomTabHandler
-import io.element.android.features.login.impl.oidc.webview.OidcNode
 import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode
 import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode
 import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode
@@ -56,6 +51,9 @@ import io.element.android.libraries.architecture.createNode
 import io.element.android.libraries.architecture.inputs
 import io.element.android.libraries.di.AppScope
 import io.element.android.libraries.matrix.api.auth.OidcDetails
+import io.element.android.libraries.oidc.api.OidcAction
+import io.element.android.libraries.oidc.api.OidcActionFlow
+import io.element.android.libraries.oidc.api.OidcEntryPoint
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import kotlinx.parcelize.Parcelize
@@ -64,11 +62,10 @@ import kotlinx.parcelize.Parcelize
 class LoginFlowNode @AssistedInject constructor(
     @Assisted buildContext: BuildContext,
     @Assisted plugins: List<Plugin>,
-    private val customTabAvailabilityChecker: CustomTabAvailabilityChecker,
-    private val customTabHandler: CustomTabHandler,
     private val accountProviderDataSource: AccountProviderDataSource,
     private val defaultLoginUserStory: DefaultLoginUserStory,
     private val oidcActionFlow: OidcActionFlow,
+    private val oidcEntryPoint: OidcEntryPoint,
 ) : BaseFlowNode<LoginFlowNode.NavTarget>(
     backstack = BackStack(
         initialElement = NavTarget.Root,
@@ -146,11 +143,11 @@ class LoginFlowNode @AssistedInject constructor(
                 )
                 val callback = object : ConfirmAccountProviderNode.Callback {
                     override fun onOidcDetails(oidcDetails: OidcDetails) {
-                        if (customTabAvailabilityChecker.supportCustomTab()) {
+                        if (oidcEntryPoint.canUseCustomTab()) {
                             // In this case open a Chrome Custom tab
                             activity?.let {
                                 customChromeTabStarted = true
-                                customTabHandler.open(it, darkTheme, oidcDetails.url)
+                                oidcEntryPoint.openUrlInCustomTab(it, darkTheme, oidcDetails.url)
                             }
                         } else {
                             // Fallback to WebView mode
@@ -201,8 +198,7 @@ class LoginFlowNode @AssistedInject constructor(
                 createNode<LoginPasswordNode>(buildContext, plugins = listOf(callback))
             }
             is NavTarget.OidcView -> {
-                val input = OidcNode.Inputs(navTarget.oidcDetails)
-                createNode<OidcNode>(buildContext, plugins = listOf(input))
+                oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.oidcDetails.url)
             }
             is NavTarget.WaitList -> {
                 val inputs = WaitListNode.Inputs(
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 27aec75739f..fdf7994d7eb 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
@@ -27,15 +27,15 @@ 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.AsyncData
 import io.element.android.libraries.architecture.Presenter
 import io.element.android.libraries.architecture.runCatchingUpdatingState
 import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
+import io.element.android.libraries.oidc.api.OidcAction
+import io.element.android.libraries.oidc.api.OidcActionFlow
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 
@@ -43,7 +43,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
     @Assisted private val params: Params,
     private val accountProviderDataSource: AccountProviderDataSource,
     private val authenticationService: MatrixAuthenticationService,
-    private val defaultOidcActionFlow: DefaultOidcActionFlow,
+    private val oidcActionFlow: OidcActionFlow,
     private val defaultLoginUserStory: DefaultLoginUserStory,
 ) : Presenter<ConfirmAccountProviderState> {
     data class Params(
@@ -65,7 +65,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
         }
 
         LaunchedEffect(Unit) {
-            defaultOidcActionFlow.collect { oidcAction ->
+            oidcActionFlow.collect { oidcAction ->
                 if (oidcAction != null) {
                     onOidcAction(oidcAction, loginFlowAction)
                 }
@@ -133,6 +133,6 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
                     }
             }
         }
-        defaultOidcActionFlow.reset()
+        oidcActionFlow.reset()
     }
 }
diff --git a/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt b/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt
index 45e3a75738f..416ecff1bc7 100644
--- a/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt
+++ b/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt
@@ -34,6 +34,9 @@ interface SecureBackupEntryPoint : FeatureEntryPoint {
 
         @Parcelize
         data object CreateNewRecoveryKey : InitialTarget
+
+        @Parcelize
+        data object ResetIdentity : InitialTarget
     }
 
     data class Params(val initialElement: InitialTarget) : NodeInputs
diff --git a/features/securebackup/impl/build.gradle.kts b/features/securebackup/impl/build.gradle.kts
index 41f3ba8942c..a2a2e04a5d8 100644
--- a/features/securebackup/impl/build.gradle.kts
+++ b/features/securebackup/impl/build.gradle.kts
@@ -45,6 +45,7 @@ dependencies {
     implementation(projects.libraries.matrix.api)
     implementation(projects.libraries.matrixui)
     implementation(projects.libraries.designsystem)
+    implementation(projects.libraries.oidc.api)
     implementation(projects.libraries.uiStrings)
     implementation(projects.libraries.testtags)
     api(libs.statemachine)
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt
index f54bfaee967..d741eb11a7e 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt
@@ -34,6 +34,7 @@ import io.element.android.features.securebackup.impl.createkey.CreateNewRecovery
 import io.element.android.features.securebackup.impl.disable.SecureBackupDisableNode
 import io.element.android.features.securebackup.impl.enable.SecureBackupEnableNode
 import io.element.android.features.securebackup.impl.enter.SecureBackupEnterRecoveryKeyNode
+import io.element.android.features.securebackup.impl.reset.ResetIdentityFlowNode
 import io.element.android.features.securebackup.impl.root.SecureBackupRootNode
 import io.element.android.features.securebackup.impl.setup.SecureBackupSetupNode
 import io.element.android.libraries.architecture.BackstackView
@@ -48,10 +49,11 @@ class SecureBackupFlowNode @AssistedInject constructor(
     @Assisted plugins: List<Plugin>,
 ) : BaseFlowNode<SecureBackupFlowNode.NavTarget>(
     backstack = BackStack(
-        initialElement = when (plugins.filterIsInstance(SecureBackupEntryPoint.Params::class.java).first().initialElement) {
+        initialElement = when (plugins.filterIsInstance<SecureBackupEntryPoint.Params>().first().initialElement) {
             SecureBackupEntryPoint.InitialTarget.Root -> NavTarget.Root
             SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey -> NavTarget.EnterRecoveryKey
             SecureBackupEntryPoint.InitialTarget.CreateNewRecoveryKey -> NavTarget.CreateNewRecoveryKey
+            is SecureBackupEntryPoint.InitialTarget.ResetIdentity -> NavTarget.ResetIdentity
         },
         savedStateMap = buildContext.savedStateMap,
     ),
@@ -79,6 +81,9 @@ class SecureBackupFlowNode @AssistedInject constructor(
 
         @Parcelize
         data object CreateNewRecoveryKey : NavTarget
+
+        @Parcelize
+        data object ResetIdentity : NavTarget
     }
 
     private val callbacks = plugins<SecureBackupEntryPoint.Callback>()
@@ -146,6 +151,14 @@ class SecureBackupFlowNode @AssistedInject constructor(
             NavTarget.CreateNewRecoveryKey -> {
                 createNode<CreateNewRecoveryKeyNode>(buildContext)
             }
+            is NavTarget.ResetIdentity -> {
+                val callback = object : ResetIdentityFlowNode.Callback {
+                    override fun onDone() {
+                        callbacks.forEach { it.onDone() }
+                    }
+                }
+                createNode<ResetIdentityFlowNode>(buildContext, listOf(callback))
+            }
         }
     }
 
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt
new file mode 100644
index 00000000000..16850890a7f
--- /dev/null
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset
+
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
+import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle
+import io.element.android.libraries.matrix.api.verification.SessionVerificationService
+import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class ResetIdentityFlowManager @Inject constructor(
+    private val matrixClient: MatrixClient,
+    @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
+    private val sessionVerificationService: SessionVerificationService,
+) {
+    private val resetHandleFlow: MutableStateFlow<AsyncData<IdentityResetHandle>> = MutableStateFlow(AsyncData.Uninitialized)
+    val currentHandleFlow: StateFlow<AsyncData<IdentityResetHandle>> = resetHandleFlow
+    private var whenResetIsDoneWaitingJob: Job? = null
+
+    fun whenResetIsDone(block: () -> Unit) {
+        whenResetIsDoneWaitingJob = sessionCoroutineScope.launch {
+            sessionVerificationService.sessionVerifiedStatus.filterIsInstance<SessionVerifiedStatus.Verified>().first()
+            block()
+        }
+    }
+
+    fun getResetHandle(): StateFlow<AsyncData<IdentityResetHandle>> {
+        return if (resetHandleFlow.value.isLoading() || resetHandleFlow.value.isSuccess()) {
+            resetHandleFlow
+        } else {
+            resetHandleFlow.value = AsyncData.Loading()
+
+            sessionCoroutineScope.launch {
+                matrixClient.encryptionService().startIdentityReset()
+                    .onSuccess { handle ->
+                        resetHandleFlow.value = if (handle != null) {
+                            AsyncData.Success(handle)
+                        } else {
+                            AsyncData.Failure(IllegalStateException("Could not get a reset identity handle"))
+                        }
+                    }
+                    .onFailure { resetHandleFlow.value = AsyncData.Failure(it) }
+            }
+
+            resetHandleFlow
+        }
+    }
+
+    suspend fun cancel() {
+        currentHandleFlow.value.dataOrNull()?.cancel()
+        resetHandleFlow.value = AsyncData.Uninitialized
+
+        whenResetIsDoneWaitingJob?.cancel()
+        whenResetIsDoneWaitingJob = null
+    }
+}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt
new file mode 100644
index 00000000000..50dcc2dd12a
--- /dev/null
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset
+
+import android.app.Activity
+import android.os.Parcelable
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.window.DialogProperties
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import com.bumble.appyx.core.plugin.plugins
+import com.bumble.appyx.navmodel.backstack.BackStack
+import com.bumble.appyx.navmodel.backstack.operation.push
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.features.securebackup.impl.reset.password.ResetIdentityPasswordNode
+import io.element.android.features.securebackup.impl.reset.root.ResetIdentityRootNode
+import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.architecture.BackstackView
+import io.element.android.libraries.architecture.BaseFlowNode
+import io.element.android.libraries.architecture.createNode
+import io.element.android.libraries.designsystem.components.ProgressDialog
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle
+import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
+import io.element.android.libraries.oidc.api.OidcEntryPoint
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import kotlinx.parcelize.Parcelize
+import timber.log.Timber
+
+@ContributesNode(SessionScope::class)
+class ResetIdentityFlowNode @AssistedInject constructor(
+    @Assisted buildContext: BuildContext,
+    @Assisted plugins: List<Plugin>,
+    private val resetIdentityFlowManager: ResetIdentityFlowManager,
+    private val coroutineScope: CoroutineScope,
+    private val oidcEntryPoint: OidcEntryPoint,
+) : BaseFlowNode<ResetIdentityFlowNode.NavTarget>(
+    backstack = BackStack(initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap),
+    buildContext = buildContext,
+    plugins = plugins,
+) {
+    interface Callback : Plugin {
+        fun onDone()
+    }
+
+    sealed interface NavTarget : Parcelable {
+        @Parcelize
+        data object Root : NavTarget
+
+        @Parcelize
+        data object ResetPassword : NavTarget
+
+        @Parcelize
+        data class ResetOidc(val url: String) : NavTarget
+    }
+
+    private lateinit var activity: Activity
+    private var resetJob: Job? = null
+
+    override fun onBuilt() {
+        super.onBuilt()
+
+        resetIdentityFlowManager.whenResetIsDone {
+            plugins<Callback>().forEach { it.onDone() }
+        }
+
+        lifecycle.addObserver(object : DefaultLifecycleObserver {
+            override fun onStart(owner: LifecycleOwner) {
+                // If the custom tab was opened, we need to cancel the reset job
+                // when we come back to the node if the reset wasn't successful
+                cancelResetJob()
+            }
+
+            override fun onDestroy(owner: LifecycleOwner) {
+                // Make sure we cancel the reset job when the node is destroyed, just in case
+                cancelResetJob()
+            }
+        })
+    }
+
+    override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
+        return when (navTarget) {
+            is NavTarget.Root -> {
+                val callback = object : ResetIdentityRootNode.Callback {
+                    override fun onContinue() {
+                        coroutineScope.startReset()
+                    }
+                }
+                createNode<ResetIdentityRootNode>(buildContext, listOf(callback))
+            }
+            is NavTarget.ResetPassword -> {
+                val handle = resetIdentityFlowManager.currentHandleFlow.value.dataOrNull() as? IdentityPasswordResetHandle ?: error("No password handle found")
+                createNode<ResetIdentityPasswordNode>(
+                    buildContext,
+                    listOf(ResetIdentityPasswordNode.Inputs(handle))
+                )
+            }
+            is NavTarget.ResetOidc -> {
+                oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.url)
+            }
+        }
+    }
+
+    private fun CoroutineScope.startReset() = launch {
+        resetIdentityFlowManager.getResetHandle()
+            .collectLatest { state ->
+                when (state) {
+                    is AsyncData.Failure -> {
+                        cancelResetJob()
+                        Timber.e(state.error, "Could not load the reset identity handle.")
+                    }
+                    is AsyncData.Success -> {
+                        when (val handle = state.data) {
+                            is IdentityOidcResetHandle -> {
+                                if (oidcEntryPoint.canUseCustomTab()) {
+                                    activity.openUrlInChromeCustomTab(null, false, handle.url)
+                                } else {
+                                    backstack.push(NavTarget.ResetOidc(handle.url))
+                                }
+                                resetJob = launch { handle.resetOidc() }
+                            }
+                            is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword)
+                        }
+                    }
+                    else -> Unit
+                }
+            }
+    }
+
+    private fun cancelResetJob() {
+        resetJob?.cancel()
+        resetJob = null
+        coroutineScope.launch { resetIdentityFlowManager.cancel() }
+    }
+
+    @Composable
+    override fun View(modifier: Modifier) {
+        // Workaround to get the current activity
+        if (!this::activity.isInitialized) {
+            activity = LocalContext.current as Activity
+        }
+
+        val startResetState by resetIdentityFlowManager.currentHandleFlow.collectAsState()
+        if (startResetState.isLoading()) {
+            ProgressDialog(
+                properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true),
+                onDismissRequest = { cancelResetJob() }
+            )
+        }
+
+        BackstackView(modifier)
+    }
+}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordEvent.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordEvent.kt
new file mode 100644
index 00000000000..b76dff920f1
--- /dev/null
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordEvent.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset.password
+
+sealed interface ResetIdentityPasswordEvent {
+    data class Reset(val password: String) : ResetIdentityPasswordEvent
+    data object DismissError : ResetIdentityPasswordEvent
+}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt
new file mode 100644
index 00000000000..75d487d00e0
--- /dev/null
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset.password
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.architecture.inputs
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
+
+@ContributesNode(SessionScope::class)
+class ResetIdentityPasswordNode @AssistedInject constructor(
+    @Assisted buildContext: BuildContext,
+    @Assisted plugins: List<Plugin>,
+    coroutineDispatchers: CoroutineDispatchers,
+) : Node(buildContext, plugins = plugins) {
+    data class Inputs(val handle: IdentityPasswordResetHandle) : NodeInputs
+
+    private val presenter = ResetIdentityPasswordPresenter(
+        identityPasswordResetHandle = inputs<Inputs>().handle,
+        dispatchers = coroutineDispatchers
+    )
+
+    @Composable
+    override fun View(modifier: Modifier) {
+        val state = presenter.present()
+        ResetIdentityPasswordView(
+            state = state,
+            onBack = ::navigateUp
+        )
+    }
+}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenter.kt
new file mode 100644
index 00000000000..baa8ab28449
--- /dev/null
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenter.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset.password
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.architecture.runCatchingUpdatingState
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+class ResetIdentityPasswordPresenter(
+    private val identityPasswordResetHandle: IdentityPasswordResetHandle,
+    private val dispatchers: CoroutineDispatchers,
+) : Presenter<ResetIdentityPasswordState> {
+    @Composable
+    override fun present(): ResetIdentityPasswordState {
+        val coroutineScope = rememberCoroutineScope()
+
+        val resetAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
+
+        fun handleEvent(event: ResetIdentityPasswordEvent) {
+            when (event) {
+                is ResetIdentityPasswordEvent.Reset -> coroutineScope.reset(event.password, resetAction)
+                ResetIdentityPasswordEvent.DismissError -> resetAction.value = AsyncAction.Uninitialized
+            }
+        }
+
+        return ResetIdentityPasswordState(
+            resetAction = resetAction.value,
+            eventSink = ::handleEvent
+        )
+    }
+
+    private fun CoroutineScope.reset(password: String, action: MutableState<AsyncAction<Unit>>) = launch(dispatchers.io) {
+        suspend {
+            identityPasswordResetHandle.resetPassword(password).getOrThrow()
+        }.runCatchingUpdatingState(action)
+    }
+}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordState.kt
new file mode 100644
index 00000000000..47662b623e8
--- /dev/null
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordState.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset.password
+
+import io.element.android.libraries.architecture.AsyncAction
+
+data class ResetIdentityPasswordState(
+    val resetAction: AsyncAction<Unit>,
+    val eventSink: (ResetIdentityPasswordEvent) -> Unit,
+)
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordStateProvider.kt
new file mode 100644
index 00000000000..1a1e7684261
--- /dev/null
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordStateProvider.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset.password
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.architecture.AsyncAction
+
+class ResetIdentityPasswordStateProvider : PreviewParameterProvider<ResetIdentityPasswordState> {
+    override val values: Sequence<ResetIdentityPasswordState>
+        get() = sequenceOf(
+            aResetIdentityPasswordState(),
+            aResetIdentityPasswordState(resetAction = AsyncAction.Loading),
+            aResetIdentityPasswordState(resetAction = AsyncAction.Success(Unit)),
+            aResetIdentityPasswordState(resetAction = AsyncAction.Failure(IllegalStateException("Failed"))),
+        )
+}
+
+private fun aResetIdentityPasswordState(
+    resetAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
+    eventSink: (ResetIdentityPasswordEvent) -> Unit = {},
+) = ResetIdentityPasswordState(
+    resetAction = resetAction,
+    eventSink = eventSink,
+)
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt
new file mode 100644
index 00000000000..074752ddead
--- /dev/null
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset.password
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.features.securebackup.impl.R
+import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
+import io.element.android.libraries.designsystem.components.BigIcon
+import io.element.android.libraries.designsystem.components.ProgressDialog
+import io.element.android.libraries.designsystem.components.form.textFieldState
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.Button
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.IconButton
+import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
+import io.element.android.libraries.ui.strings.CommonStrings
+
+@Composable
+fun ResetIdentityPasswordView(
+    state: ResetIdentityPasswordState,
+    onBack: () -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    val passwordState = textFieldState(stateValue = "")
+    FlowStepPage(
+        modifier = modifier,
+        iconStyle = BigIcon.Style.Default(CompoundIcons.LockSolid()),
+        title = stringResource(R.string.screen_reset_encryption_password_title),
+        subTitle = stringResource(R.string.screen_reset_encryption_password_subtitle),
+        onBackClick = onBack,
+        content = {
+            Content(
+                text = passwordState.value,
+                onTextChange = { newText ->
+                    if (state.resetAction.isFailure()) {
+                        state.eventSink(ResetIdentityPasswordEvent.DismissError)
+                    }
+                    passwordState.value = newText
+                },
+                hasError = state.resetAction.isFailure(),
+            )
+        },
+        buttons = {
+            Button(
+                modifier = Modifier.fillMaxWidth(),
+                text = stringResource(CommonStrings.action_reset_identity),
+                onClick = { state.eventSink(ResetIdentityPasswordEvent.Reset(passwordState.value)) },
+                destructive = true,
+                enabled = passwordState.value.isNotEmpty(),
+            )
+        }
+    )
+
+    // On success we need to wait until the screen is automatically dismissed, so we keep the progress dialog
+    if (state.resetAction.isLoading() || state.resetAction.isSuccess()) {
+        ProgressDialog()
+    }
+}
+
+@Composable
+private fun Content(text: String, onTextChange: (String) -> Unit, hasError: Boolean) {
+    var showPassword by remember { mutableStateOf(false) }
+    OutlinedTextField(
+        modifier = Modifier
+            .fillMaxWidth()
+            .onTabOrEnterKeyFocusNext(LocalFocusManager.current),
+        value = text,
+        onValueChange = onTextChange,
+        label = { Text(stringResource(CommonStrings.common_password)) },
+        placeholder = { Text(stringResource(R.string.screen_reset_encryption_password_placeholder)) },
+        singleLine = true,
+        visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
+        trailingIcon = {
+            val image =
+                if (showPassword) CompoundIcons.VisibilityOn() else CompoundIcons.VisibilityOff()
+            val description =
+                if (showPassword) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password)
+
+            IconButton(onClick = { showPassword = !showPassword }) {
+                Icon(imageVector = image, description)
+            }
+        },
+        isError = hasError,
+        supportingText = if (hasError) {
+            { Text(stringResource(R.string.screen_reset_encryption_password_error)) }
+        } else {
+            null
+        }
+    )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun ResetIdentityPasswordViewPreview(@PreviewParameter(ResetIdentityPasswordStateProvider::class) state: ResetIdentityPasswordState) {
+    ElementPreview {
+        ResetIdentityPasswordView(
+            state = state,
+            onBack = {}
+        )
+    }
+}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootEvent.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootEvent.kt
new file mode 100644
index 00000000000..a1ec4cbe826
--- /dev/null
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootEvent.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset.root
+
+sealed interface ResetIdentityRootEvent {
+    data object Continue : ResetIdentityRootEvent
+    data object DismissDialog : ResetIdentityRootEvent
+}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt
new file mode 100644
index 00000000000..3dd9876fc0d
--- /dev/null
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset.root
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.libraries.di.SessionScope
+
+@ContributesNode(SessionScope::class)
+class ResetIdentityRootNode @AssistedInject constructor(
+    @Assisted buildContext: BuildContext,
+    @Assisted plugins: List<Plugin>,
+) : Node(buildContext, plugins = plugins) {
+    interface Callback : Plugin {
+        fun onContinue()
+    }
+
+    private val presenter = ResetIdentityRootPresenter()
+    private val callback: Callback = plugins.filterIsInstance<Callback>().first()
+
+    @Composable
+    override fun View(modifier: Modifier) {
+        val state = presenter.present()
+        ResetIdentityRootView(
+            modifier = modifier,
+            state = state,
+            onContinue = callback::onContinue,
+            onBack = ::navigateUp,
+        )
+    }
+}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenter.kt
new file mode 100644
index 00000000000..11c96e9ad8f
--- /dev/null
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenter.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset.root
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import io.element.android.libraries.architecture.Presenter
+
+class ResetIdentityRootPresenter : Presenter<ResetIdentityRootState> {
+    @Composable
+    override fun present(): ResetIdentityRootState {
+        var displayConfirmDialog by remember { mutableStateOf(false) }
+
+        fun handleEvent(event: ResetIdentityRootEvent) {
+            displayConfirmDialog = when (event) {
+                ResetIdentityRootEvent.Continue -> true
+                ResetIdentityRootEvent.DismissDialog -> false
+            }
+        }
+
+        return ResetIdentityRootState(
+            displayConfirmationDialog = displayConfirmDialog,
+            eventSink = ::handleEvent
+        )
+    }
+}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootState.kt
new file mode 100644
index 00000000000..de1054c97f4
--- /dev/null
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootState.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset.root
+
+data class ResetIdentityRootState(
+    val displayConfirmationDialog: Boolean,
+    val eventSink: (ResetIdentityRootEvent) -> Unit,
+)
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootStateProvider.kt
new file mode 100644
index 00000000000..8d780343fe3
--- /dev/null
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootStateProvider.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset.root
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+
+class ResetIdentityRootStateProvider : PreviewParameterProvider<ResetIdentityRootState> {
+    override val values: Sequence<ResetIdentityRootState>
+        get() = sequenceOf(
+            ResetIdentityRootState(
+                displayConfirmationDialog = false,
+                eventSink = {}
+            ),
+            ResetIdentityRootState(
+                displayConfirmationDialog = true,
+                eventSink = {}
+            )
+        )
+}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt
new file mode 100644
index 00000000000..9983f06b44e
--- /dev/null
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset.root
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.features.securebackup.impl.R
+import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
+import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
+import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
+import io.element.android.libraries.designsystem.components.BigIcon
+import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.Button
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.collections.immutable.persistentListOf
+
+@Composable
+fun ResetIdentityRootView(
+    state: ResetIdentityRootState,
+    onContinue: () -> Unit,
+    onBack: () -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    FlowStepPage(
+        modifier = modifier,
+        iconStyle = BigIcon.Style.AlertSolid,
+        title = stringResource(R.string.screen_encryption_reset_title),
+        subTitle = stringResource(R.string.screen_encryption_reset_subtitle),
+        isScrollable = true,
+        content = { Content() },
+        buttons = {
+            Button(
+                modifier = Modifier.fillMaxWidth(),
+                text = stringResource(id = CommonStrings.action_continue),
+                onClick = { state.eventSink(ResetIdentityRootEvent.Continue) },
+                destructive = true,
+            )
+        },
+        onBackClick = onBack,
+    )
+
+    if (state.displayConfirmationDialog) {
+        ConfirmationDialog(
+            title = stringResource(R.string.screen_reset_encryption_confirmation_alert_title),
+            content = stringResource(R.string.screen_reset_encryption_confirmation_alert_subtitle),
+            submitText = stringResource(R.string.screen_reset_encryption_confirmation_alert_action),
+            onSubmitClick = {
+                state.eventSink(ResetIdentityRootEvent.DismissDialog)
+                onContinue()
+            },
+            destructiveSubmit = true,
+            onDismiss = { state.eventSink(ResetIdentityRootEvent.DismissDialog) }
+        )
+    }
+}
+
+@Composable
+private fun Content() {
+    Column(
+        modifier = Modifier.padding(top = 8.dp, bottom = 40.dp),
+        verticalArrangement = Arrangement.spacedBy(24.dp),
+    ) {
+        InfoListOrganism(
+            modifier = Modifier.fillMaxWidth(),
+            items = persistentListOf(
+                InfoListItem(
+                    message = stringResource(R.string.screen_encryption_reset_bullet_1),
+                    iconComposable = {
+                        Icon(
+                            modifier = Modifier.size(20.dp),
+                            imageVector = CompoundIcons.Check(),
+                            contentDescription = null,
+                            tint = ElementTheme.colors.iconSuccessPrimary,
+                        )
+                    },
+                ),
+                InfoListItem(
+                    message = stringResource(R.string.screen_encryption_reset_bullet_2),
+                    iconComposable = {
+                        Icon(
+                            modifier = Modifier.size(20.dp),
+                            imageVector = CompoundIcons.Close(),
+                            contentDescription = null,
+                            tint = ElementTheme.colors.iconCriticalPrimary,
+                        )
+                    },
+                ),
+                InfoListItem(
+                    message = stringResource(R.string.screen_encryption_reset_bullet_3),
+                    iconComposable = {
+                        Icon(
+                            modifier = Modifier.size(20.dp),
+                            imageVector = CompoundIcons.Close(),
+                            contentDescription = null,
+                            tint = ElementTheme.colors.iconCriticalPrimary,
+                        )
+                    },
+                ),
+            ),
+            backgroundColor = ElementTheme.colors.bgActionSecondaryHovered,
+        )
+
+        Text(
+            modifier = Modifier.fillMaxWidth(),
+            text = stringResource(R.string.screen_encryption_reset_footer),
+            style = ElementTheme.typography.fontBodyMdMedium,
+            color = ElementTheme.colors.textActionPrimary,
+            textAlign = TextAlign.Center,
+        )
+    }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun ResetIdentityRootViewPreview(@PreviewParameter(ResetIdentityRootStateProvider::class) state: ResetIdentityRootState) {
+    ElementPreview {
+        ResetIdentityRootView(
+            state = state,
+            onContinue = {},
+            onBack = {},
+        )
+    }
+}
diff --git a/features/securebackup/impl/src/main/res/values/localazy.xml b/features/securebackup/impl/src/main/res/values/localazy.xml
index e1159031a2d..85e801fce17 100644
--- a/features/securebackup/impl/src/main/res/values/localazy.xml
+++ b/features/securebackup/impl/src/main/res/values/localazy.xml
@@ -16,6 +16,12 @@
   <string name="screen_create_new_recovery_key_list_item_4">"Follow the instructions to create a new recovery key"</string>
   <string name="screen_create_new_recovery_key_list_item_5">"Save your new recovery key in a password manager or encrypted note"</string>
   <string name="screen_create_new_recovery_key_title">"Reset the encryption for your account using another device"</string>
+  <string name="screen_encryption_reset_bullet_1">"Your account details, contacts, preferences, and chat list will be kept"</string>
+  <string name="screen_encryption_reset_bullet_2">"You will lose your existing message history"</string>
+  <string name="screen_encryption_reset_bullet_3">"You will need to verify all your existing devices and contacts again"</string>
+  <string name="screen_encryption_reset_footer">"Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key."</string>
+  <string name="screen_encryption_reset_subtitle">"If you’re not signed in to any other devices and you’ve lost your recovery key, then you’ll need to reset your identity to continue using the app. "</string>
+  <string name="screen_encryption_reset_title">"Reset your identity in case you can’t confirm another way"</string>
   <string name="screen_key_backup_disable_confirmation_action_turn_off">"Turn off"</string>
   <string name="screen_key_backup_disable_confirmation_description">"You will lose your encrypted messages if you are signed out of all devices."</string>
   <string name="screen_key_backup_disable_confirmation_title">"Are you sure you want to turn off backup?"</string>
@@ -51,4 +57,11 @@
   <string name="screen_recovery_key_setup_generate_key_description">"Make sure you can store your recovery key somewhere safe"</string>
   <string name="screen_recovery_key_setup_success">"Recovery setup successful"</string>
   <string name="screen_recovery_key_setup_title">"Set up recovery"</string>
+  <string name="screen_reset_encryption_confirmation_alert_action">"Yes, reset now"</string>
+  <string name="screen_reset_encryption_confirmation_alert_subtitle">"This process is irreversible."</string>
+  <string name="screen_reset_encryption_confirmation_alert_title">"Are you sure you want to reset your encryption?"</string>
+  <string name="screen_reset_encryption_password_error">"An unknown error happened. Please check your account password is correct and try again."</string>
+  <string name="screen_reset_encryption_password_placeholder">"Enter…"</string>
+  <string name="screen_reset_encryption_password_subtitle">"Confirm that you want to reset your encryption."</string>
+  <string name="screen_reset_encryption_password_title">"Enter your account password to continue"</string>
 </resources>
diff --git a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt
index 8d19ca56989..deb5cdf2672 100644
--- a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt
+++ b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt
@@ -31,6 +31,7 @@ interface VerifySessionEntryPoint : FeatureEntryPoint {
 
     interface Callback : Plugin {
         fun onEnterRecoveryKey()
+        fun onResetKey()
         fun onDone()
     }
 }
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt
index 9ce13586831..0ed9524626f 100644
--- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt
@@ -43,6 +43,7 @@ class VerifySelfSessionNode @AssistedInject constructor(
             state = state,
             modifier = modifier,
             onEnterRecoveryKey = callback::onEnterRecoveryKey,
+            onResetKey = callback::onResetKey,
             onFinish = callback::onDone,
         )
     }
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt
index 6b908e3ebd6..446114752e8 100644
--- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt
@@ -20,6 +20,7 @@ import androidx.activity.compose.BackHandler
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxHeight
@@ -53,6 +54,7 @@ import io.element.android.libraries.designsystem.components.PageTitle
 import io.element.android.libraries.designsystem.preview.ElementPreview
 import io.element.android.libraries.designsystem.preview.PreviewsDayNight
 import io.element.android.libraries.designsystem.theme.components.Button
+import io.element.android.libraries.designsystem.theme.components.OutlinedButton
 import io.element.android.libraries.designsystem.theme.components.Text
 import io.element.android.libraries.designsystem.theme.components.TextButton
 import io.element.android.libraries.designsystem.theme.components.TopAppBar
@@ -66,6 +68,7 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionState.Ver
 fun VerifySelfSessionView(
     state: VerifySelfSessionState,
     onEnterRecoveryKey: () -> Unit,
+    onResetKey: () -> Unit,
     onFinish: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
@@ -115,6 +118,7 @@ fun VerifySelfSessionView(
                 goBack = ::resetFlow,
                 onEnterRecoveryKey = onEnterRecoveryKey,
                 onFinish = onFinish,
+                onResetKey = onResetKey,
             )
         }
     ) {
@@ -226,6 +230,7 @@ private fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifie
 private fun BottomMenu(
     screenState: VerifySelfSessionState,
     onEnterRecoveryKey: () -> Unit,
+    onResetKey: () -> Unit,
     goBack: () -> Unit,
     onFinish: () -> Unit,
 ) {
@@ -236,42 +241,72 @@ private fun BottomMenu(
 
     when (verificationViewState) {
         is FlowStep.Initial -> {
-            if (verificationViewState.isLastDevice) {
-                BottomMenu(
-                    positiveButtonTitle = stringResource(R.string.screen_session_verification_enter_recovery_key),
-                    onPositiveButtonClick = onEnterRecoveryKey,
-                )
-            } else {
-                BottomMenu(
-                    positiveButtonTitle = stringResource(R.string.screen_identity_use_another_device),
-                    onPositiveButtonClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) },
-                    negativeButtonTitle = stringResource(R.string.screen_session_verification_enter_recovery_key),
-                    onNegativeButtonClick = onEnterRecoveryKey,
+            BottomMenu {
+                if (verificationViewState.isLastDevice) {
+                    Button(
+                        modifier = Modifier.fillMaxWidth(),
+                        text = stringResource(R.string.screen_session_verification_enter_recovery_key),
+                        onClick = onEnterRecoveryKey,
+                    )
+                } else {
+                    Button(
+                        modifier = Modifier.fillMaxWidth(),
+                        text = stringResource(R.string.screen_identity_use_another_device),
+                        onClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) },
+                    )
+                    OutlinedButton(
+                        modifier = Modifier.fillMaxWidth(),
+                        text = stringResource(R.string.screen_session_verification_enter_recovery_key),
+                        onClick = onEnterRecoveryKey,
+                    )
+                }
+                // This option should always be displayed
+                TextButton(
+                    modifier = Modifier.fillMaxWidth(),
+                    text = stringResource(R.string.screen_identity_confirmation_cannot_confirm),
+                    onClick = onResetKey,
                 )
             }
         }
         is FlowStep.Canceled -> {
-            BottomMenu(
-                positiveButtonTitle = stringResource(R.string.screen_session_verification_positive_button_canceled),
-                onPositiveButtonClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) },
-                negativeButtonTitle = stringResource(CommonStrings.action_cancel),
-                onNegativeButtonClick = goBack,
-            )
+            BottomMenu {
+                Button(
+                    modifier = Modifier.fillMaxWidth(),
+                    text = stringResource(R.string.screen_session_verification_positive_button_canceled),
+                    onClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) },
+                )
+                TextButton(
+                    modifier = Modifier.fillMaxWidth(),
+                    text = stringResource(CommonStrings.action_cancel),
+                    onClick = goBack,
+                )
+            }
         }
         is FlowStep.Ready -> {
-            BottomMenu(
-                positiveButtonTitle = stringResource(CommonStrings.action_start),
-                onPositiveButtonClick = { eventSink(VerifySelfSessionViewEvents.StartSasVerification) },
-                negativeButtonTitle = stringResource(CommonStrings.action_cancel),
-                onNegativeButtonClick = goBack,
-            )
+            BottomMenu {
+                Button(
+                    modifier = Modifier.fillMaxWidth(),
+                    text = stringResource(CommonStrings.action_start),
+                    onClick = { eventSink(VerifySelfSessionViewEvents.StartSasVerification) },
+                )
+                TextButton(
+                    modifier = Modifier.fillMaxWidth(),
+                    text = stringResource(CommonStrings.action_cancel),
+                    onClick = goBack,
+                )
+            }
         }
         is FlowStep.AwaitingOtherDeviceResponse -> {
-            BottomMenu(
-                positiveButtonTitle = stringResource(R.string.screen_identity_waiting_on_other_device),
-                onPositiveButtonClick = {},
-                isLoading = true,
-            )
+            BottomMenu {
+                Button(
+                    modifier = Modifier.fillMaxWidth(),
+                    text = stringResource(R.string.screen_identity_waiting_on_other_device),
+                    onClick = {},
+                    showProgress = true,
+                )
+                // Placeholder so the 1st button keeps its vertical position
+                Spacer(modifier = Modifier.height(40.dp))
+            }
         }
         is FlowStep.Verifying -> {
             val positiveButtonTitle = if (isVerifying) {
@@ -279,23 +314,34 @@ private fun BottomMenu(
             } else {
                 stringResource(R.string.screen_session_verification_they_match)
             }
-            BottomMenu(
-                positiveButtonTitle = positiveButtonTitle,
-                onPositiveButtonClick = {
-                    if (!isVerifying) {
-                        eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
-                    }
-                },
-                negativeButtonTitle = stringResource(R.string.screen_session_verification_they_dont_match),
-                onNegativeButtonClick = { eventSink(VerifySelfSessionViewEvents.DeclineVerification) },
-                isLoading = isVerifying,
-            )
+            BottomMenu {
+                Button(
+                    modifier = Modifier.fillMaxWidth(),
+                    text = positiveButtonTitle,
+                    showProgress = isVerifying,
+                    onClick = {
+                        if (!isVerifying) {
+                            eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
+                        }
+                    },
+                )
+                TextButton(
+                    modifier = Modifier.fillMaxWidth(),
+                    text = stringResource(R.string.screen_session_verification_they_dont_match),
+                    onClick = { eventSink(VerifySelfSessionViewEvents.DeclineVerification) },
+                )
+            }
         }
         is FlowStep.Completed -> {
-            BottomMenu(
-                positiveButtonTitle = stringResource(CommonStrings.action_continue),
-                onPositiveButtonClick = onFinish,
-            )
+            BottomMenu {
+                Button(
+                    modifier = Modifier.fillMaxWidth(),
+                    text = stringResource(CommonStrings.action_continue),
+                    onClick = onFinish,
+                )
+                // Placeholder so the 1st button keeps its vertical position
+                Spacer(modifier = Modifier.height(48.dp))
+            }
         }
         is FlowStep.Skipped -> return
     }
@@ -303,35 +349,13 @@ private fun BottomMenu(
 
 @Composable
 private fun BottomMenu(
-    positiveButtonTitle: String?,
-    onPositiveButtonClick: () -> Unit,
     modifier: Modifier = Modifier,
-    negativeButtonTitle: String? = null,
-    negativeButtonEnabled: Boolean = negativeButtonTitle != null,
-    onNegativeButtonClick: () -> Unit = {},
-    isLoading: Boolean = false,
+    buttons: @Composable ColumnScope.() -> Unit,
 ) {
     ButtonColumnMolecule(
         modifier = modifier.padding(bottom = 16.dp)
     ) {
-        if (positiveButtonTitle != null) {
-            Button(
-                text = positiveButtonTitle,
-                showProgress = isLoading,
-                modifier = Modifier.fillMaxWidth(),
-                onClick = onPositiveButtonClick,
-            )
-        }
-        if (negativeButtonTitle != null) {
-            TextButton(
-                text = negativeButtonTitle,
-                modifier = Modifier.fillMaxWidth(),
-                onClick = onNegativeButtonClick,
-                enabled = negativeButtonEnabled,
-            )
-        } else {
-            Spacer(modifier = Modifier.height(48.dp))
-        }
+        buttons()
     }
 }
 
@@ -341,6 +365,7 @@ internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionSta
     VerifySelfSessionView(
         state = state,
         onEnterRecoveryKey = {},
+        onResetKey = {},
         onFinish = {},
     )
 }
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt
index ec0d9662c7f..425133b797b 100644
--- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt
@@ -47,12 +47,19 @@ fun Activity.openUrlInChromeCustomTab(
                     true -> CustomTabsIntent.COLOR_SCHEME_DARK
                 }
             )
+            .setShareIdentityEnabled(false)
             // Note: setting close button icon does not work
             // .setCloseButtonIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_back_24dp))
             // .setStartAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out)
             // .setExitAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out)
             .apply { session?.let { setSession(it) } }
             .build()
+            .apply {
+                // Disable download button
+                intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_DOWNLOAD_BUTTON", true)
+                // Disable bookmark button
+                intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_START_BUTTON", true)
+            }
             .launchUrl(this, Uri.parse(url))
     } catch (activityNotFoundException: ActivityNotFoundException) {
         // TODO context.toast(R.string.error_no_external_application_found)
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt
index f47487c6346..5afb81648ba 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt
@@ -62,4 +62,52 @@ interface EncryptionService {
      * called the fingerprint of the device.
      */
     suspend fun deviceEd25519(): String?
+
+    /**
+     * Starts the identity reset process. This will return a handle that can be used to reset the identity.
+     */
+    suspend fun startIdentityReset(): Result<IdentityResetHandle?>
+}
+
+/**
+ * A handle to reset the user's identity.
+ */
+interface IdentityResetHandle {
+    /**
+     * Cancel the reset process and drops the existing handle in the SDK.
+     */
+    suspend fun cancel()
+}
+
+/**
+ * A handle to reset the user's identity with a password login type.
+ */
+interface IdentityPasswordResetHandle : IdentityResetHandle {
+    /**
+     * Reset the password of the user.
+     *
+     * This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is
+     * called, or the identity is reset.
+     *
+     * @param password the current password, which will be validated before the process takes place.
+     */
+    suspend fun resetPassword(password: String): Result<Unit>
+}
+
+/**
+ * A handle to reset the user's identity with an OIDC login type.
+ */
+interface IdentityOidcResetHandle : IdentityResetHandle {
+    /**
+     * The URL to open in a webview/custom tab to reset the identity.
+     */
+    val url: String
+
+    /**
+     * Reset the identity using the OIDC flow.
+     *
+     * This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is
+     * called, or the identity is reset.
+     */
+    suspend fun resetOidc(): Result<Unit>
 }
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
index 68ab4a611e3..ae681b27719 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
@@ -18,10 +18,12 @@ package io.element.android.libraries.matrix.impl.encryption
 
 import io.element.android.libraries.core.coroutine.CoroutineDispatchers
 import io.element.android.libraries.core.extensions.mapFailure
+import io.element.android.libraries.matrix.api.core.SessionId
 import io.element.android.libraries.matrix.api.encryption.BackupState
 import io.element.android.libraries.matrix.api.encryption.BackupUploadState
 import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
 import io.element.android.libraries.matrix.api.encryption.EncryptionService
+import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle
 import io.element.android.libraries.matrix.api.encryption.RecoveryState
 import io.element.android.libraries.matrix.api.sync.SyncState
 import io.element.android.libraries.matrix.impl.sync.RustSyncService
@@ -54,6 +56,7 @@ internal class RustEncryptionService(
     private val dispatchers: CoroutineDispatchers,
 ) : EncryptionService {
     private val service: Encryption = client.encryption()
+    private val sessionId = SessionId(client.session().userId)
 
     private val enableRecoveryProgressMapper = EnableRecoveryProgressMapper()
     private val backupUploadStateMapper = BackupUploadStateMapper()
@@ -198,4 +201,12 @@ internal class RustEncryptionService(
     override suspend fun deviceEd25519(): String? {
         return service.ed25519Key()
     }
+
+    override suspend fun startIdentityReset(): Result<IdentityResetHandle?> {
+        return runCatching {
+            service.resetIdentity()?.let { handle ->
+                RustIdentityResetHandleFactory.create(sessionId, handle)
+            }?.getOrNull()
+        }
+    }
 }
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt
new file mode 100644
index 00000000000..c4c20eb7d62
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.encryption
+
+import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle
+import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
+import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle
+import org.matrix.rustcomponents.sdk.AuthData
+import org.matrix.rustcomponents.sdk.AuthDataPasswordDetails
+import org.matrix.rustcomponents.sdk.CrossSigningResetAuthType
+
+object RustIdentityResetHandleFactory {
+    fun create(
+        userId: UserId,
+        identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle
+    ): Result<IdentityResetHandle> {
+        return runCatching {
+            when (val authType = identityResetHandle.authType()) {
+                is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl)
+                // User interactive authentication (user + password)
+                CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle)
+            }
+        }
+    }
+}
+
+class RustPasswordIdentityResetHandle(
+    private val userId: UserId,
+    private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle,
+) : IdentityPasswordResetHandle {
+    override suspend fun resetPassword(password: String): Result<Unit> {
+        return runCatching { identityResetHandle.reset(AuthData.Password(AuthDataPasswordDetails(userId.value, password))) }
+    }
+
+    override suspend fun cancel() {
+        identityResetHandle.cancelAndDestroy()
+    }
+}
+
+class RustOidcIdentityResetHandle(
+    private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle,
+    override val url: String,
+) : IdentityOidcResetHandle {
+    override suspend fun resetOidc(): Result<Unit> {
+        return runCatching { identityResetHandle.reset(null) }
+    }
+
+    override suspend fun cancel() {
+        identityResetHandle.cancelAndDestroy()
+    }
+}
+
+private suspend fun org.matrix.rustcomponents.sdk.IdentityResetHandle.cancelAndDestroy() {
+    cancel()
+    destroy()
+}
diff --git a/libraries/oidc/api/build.gradle.kts b/libraries/oidc/api/build.gradle.kts
new file mode 100644
index 00000000000..4e01547d2ca
--- /dev/null
+++ b/libraries/oidc/api/build.gradle.kts
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2024 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.
+ */
+plugins {
+    id("io.element.android-library")
+    id("kotlin-parcelize")
+}
+
+android {
+    namespace = "io.element.android.libraries.oidc.api"
+}
+
+dependencies {
+    implementation(projects.libraries.architecture)
+}
diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt
similarity index 93%
rename from features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt
rename to libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt
index 6d878728796..cc171599601 100644
--- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt
+++ b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.api.oidc
+package io.element.android.libraries.oidc.api
 
 sealed interface OidcAction {
     data object GoBack : OidcAction
diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcActionFlow.kt
similarity index 79%
rename from features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt
rename to libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcActionFlow.kt
index 004e7c8a51e..1fd67cfaf5b 100644
--- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt
+++ b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcActionFlow.kt
@@ -14,8 +14,12 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.api.oidc
+package io.element.android.libraries.oidc.api
+
+import kotlinx.coroutines.flow.FlowCollector
 
 interface OidcActionFlow {
     fun post(oidcAction: OidcAction)
+    suspend fun collect(collector: FlowCollector<OidcAction?>)
+    fun reset()
 }
diff --git a/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcEntryPoint.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcEntryPoint.kt
new file mode 100644
index 00000000000..c00bf263aff
--- /dev/null
+++ b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcEntryPoint.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.oidc.api
+
+import android.app.Activity
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+
+interface OidcEntryPoint {
+    fun canUseCustomTab(): Boolean
+    fun openUrlInCustomTab(activity: Activity, darkTheme: Boolean, url: String)
+    fun createFallbackWebViewNode(parentNode: Node, buildContext: BuildContext, url: String): Node
+}
diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcIntentResolver.kt
similarity index 93%
rename from features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt
rename to libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcIntentResolver.kt
index a6ecf26fca9..77597fd4322 100644
--- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt
+++ b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcIntentResolver.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.api.oidc
+package io.element.android.libraries.oidc.api
 
 import android.content.Intent
 
diff --git a/libraries/oidc/impl/build.gradle.kts b/libraries/oidc/impl/build.gradle.kts
new file mode 100644
index 00000000000..db1cdf8d0a0
--- /dev/null
+++ b/libraries/oidc/impl/build.gradle.kts
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2024 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.
+ */
+
+plugins {
+    id("io.element.android-compose-library")
+    alias(libs.plugins.anvil)
+    id("kotlin-parcelize")
+    alias(libs.plugins.kotlin.serialization)
+}
+
+android {
+    namespace = "io.element.android.libraries.oidc.impl"
+
+    testOptions {
+        unitTests {
+            isIncludeAndroidResources = true
+        }
+    }
+}
+
+anvil {
+    generateDaggerFactories.set(true)
+}
+
+dependencies {
+    implementation(projects.anvilannotations)
+    implementation(projects.appconfig)
+    anvil(projects.anvilcodegen)
+    implementation(projects.libraries.core)
+    implementation(projects.libraries.androidutils)
+    implementation(projects.libraries.architecture)
+    implementation(projects.libraries.matrix.api)
+    implementation(projects.libraries.designsystem)
+    implementation(projects.libraries.uiStrings)
+    implementation(libs.androidx.browser)
+    implementation(platform(libs.network.retrofit.bom))
+    implementation(libs.network.retrofit)
+    implementation(libs.serialization.json)
+    api(projects.libraries.oidc.api)
+
+    testImplementation(libs.test.junit)
+    testImplementation(libs.androidx.test.ext.junit)
+    testImplementation(libs.coroutines.test)
+    testImplementation(libs.molecule.runtime)
+    testImplementation(libs.test.truth)
+    testImplementation(libs.test.turbine)
+    testImplementation(projects.libraries.matrix.test)
+    testImplementation(projects.libraries.permissions.test)
+    testImplementation(projects.tests.testutils)
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabAvailabilityChecker.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/CustomTabAvailabilityChecker.kt
similarity index 95%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabAvailabilityChecker.kt
rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/CustomTabAvailabilityChecker.kt
index 424e9f13bc2..6258e67b0ed 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabAvailabilityChecker.kt
+++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/CustomTabAvailabilityChecker.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.impl.oidc
+package io.element.android.libraries.oidc.impl
 
 import android.content.Context
 import androidx.browser.customtabs.CustomTabsClient
diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt
new file mode 100644
index 00000000000..185c27bf19a
--- /dev/null
+++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.oidc.impl
+
+import android.app.Activity
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
+import io.element.android.libraries.architecture.createNode
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.matrix.api.auth.OidcDetails
+import io.element.android.libraries.oidc.api.OidcEntryPoint
+import io.element.android.libraries.oidc.impl.webview.OidcNode
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class DefaultOidcEntryPoint @Inject constructor(
+    private val customTabAvailabilityChecker: CustomTabAvailabilityChecker,
+) : OidcEntryPoint {
+    override fun canUseCustomTab(): Boolean {
+        return customTabAvailabilityChecker.supportCustomTab()
+    }
+
+    override fun openUrlInCustomTab(activity: Activity, darkTheme: Boolean, url: String) {
+        assert(canUseCustomTab()) { "Custom tab is not supported in this device." }
+        activity.openUrlInChromeCustomTab(null, darkTheme, url)
+    }
+
+    override fun createFallbackWebViewNode(parentNode: Node, buildContext: BuildContext, url: String): Node {
+        assert(!canUseCustomTab()) { "Custom tab should be used instead of the fallback node." }
+        val inputs = OidcNode.Inputs(OidcDetails(url))
+        return parentNode.createNode<OidcNode>(buildContext, listOf(inputs))
+    }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.kt
similarity index 85%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt
rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.kt
index 8b6844e0f3d..9777cee22fe 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt
+++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.kt
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.impl.oidc
+package io.element.android.libraries.oidc.impl
 
 import android.content.Intent
 import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.features.login.api.oidc.OidcAction
-import io.element.android.features.login.api.oidc.OidcIntentResolver
 import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.oidc.api.OidcAction
+import io.element.android.libraries.oidc.api.OidcIntentResolver
 import javax.inject.Inject
 
 @ContributesBinding(AppScope::class)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt
similarity index 93%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt
rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt
index 8c8c895acda..ae502d0a8cd 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt
+++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.impl.oidc
+package io.element.android.libraries.oidc.impl
 
-import io.element.android.features.login.api.oidc.OidcAction
 import io.element.android.libraries.matrix.api.auth.OidcConfig
+import io.element.android.libraries.oidc.api.OidcAction
 import javax.inject.Inject
 
 /**
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt
similarity index 97%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt
rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt
index b83c0eb1acc..21c673145ed 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt
+++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.impl.oidc.customtab
+package io.element.android.libraries.oidc.impl.customtab
 
 import android.app.Activity
 import android.content.ComponentName
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/DefaultOidcActionFlow.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlow.kt
similarity index 82%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/DefaultOidcActionFlow.kt
rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlow.kt
index 17dfa8418f7..0c0737b43e7 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/DefaultOidcActionFlow.kt
+++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlow.kt
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.impl.oidc.customtab
+package io.element.android.libraries.oidc.impl.customtab
 
 import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.features.login.api.oidc.OidcAction
-import io.element.android.features.login.api.oidc.OidcActionFlow
 import io.element.android.libraries.di.AppScope
 import io.element.android.libraries.di.SingleIn
+import io.element.android.libraries.oidc.api.OidcAction
+import io.element.android.libraries.oidc.api.OidcActionFlow
 import kotlinx.coroutines.flow.FlowCollector
 import kotlinx.coroutines.flow.MutableStateFlow
 import javax.inject.Inject
@@ -34,11 +34,11 @@ class DefaultOidcActionFlow @Inject constructor() : OidcActionFlow {
         mutableStateFlow.value = oidcAction
     }
 
-    suspend fun collect(collector: FlowCollector<OidcAction?>) {
+    override suspend fun collect(collector: FlowCollector<OidcAction?>) {
         mutableStateFlow.collect(collector)
     }
 
-    fun reset() {
+    override fun reset() {
         mutableStateFlow.value = null
     }
 }
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcEvents.kt
similarity index 86%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt
rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcEvents.kt
index 9d2f945e25b..602d50628a8 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt
+++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcEvents.kt
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.impl.oidc.webview
+package io.element.android.libraries.oidc.impl.webview
 
-import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.libraries.oidc.api.OidcAction
 
 sealed interface OidcEvents {
     data object Cancel : OidcEvents
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcNode.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcNode.kt
similarity index 96%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcNode.kt
rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcNode.kt
index 5cd7cf0c3b6..d31bdcd0320 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcNode.kt
+++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcNode.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.impl.oidc.webview
+package io.element.android.libraries.oidc.impl.webview
 
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenter.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt
similarity index 96%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenter.kt
rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt
index 3f10ba256a7..5e886644f61 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenter.kt
+++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.impl.oidc.webview
+package io.element.android.libraries.oidc.impl.webview
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
@@ -25,11 +25,11 @@ import androidx.compose.runtime.setValue
 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.libraries.architecture.AsyncAction
 import io.element.android.libraries.architecture.Presenter
 import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
 import io.element.android.libraries.matrix.api.auth.OidcDetails
+import io.element.android.libraries.oidc.api.OidcAction
 import kotlinx.coroutines.launch
 
 class OidcPresenter @AssistedInject constructor(
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcState.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcState.kt
similarity index 93%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcState.kt
rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcState.kt
index dc4f26d51e6..8b1b01b03dd 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcState.kt
+++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcState.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.impl.oidc.webview
+package io.element.android.libraries.oidc.impl.webview
 
 import io.element.android.libraries.architecture.AsyncAction
 import io.element.android.libraries.matrix.api.auth.OidcDetails
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcStateProvider.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcStateProvider.kt
similarity index 95%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcStateProvider.kt
rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcStateProvider.kt
index 5d446574628..7127c9160e9 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcStateProvider.kt
+++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcStateProvider.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.impl.oidc.webview
+package io.element.android.libraries.oidc.impl.webview
 
 import androidx.compose.ui.tooling.preview.PreviewParameterProvider
 import io.element.android.libraries.architecture.AsyncAction
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt
similarity index 63%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt
rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt
index c07078cdff7..0ba9d4ea528 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt
+++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt
@@ -14,32 +14,39 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.impl.oidc.webview
+package io.element.android.libraries.oidc.impl.webview
 
+import android.annotation.SuppressLint
 import android.webkit.WebView
 import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalInspectionMode
 import androidx.compose.ui.tooling.preview.PreviewParameter
 import androidx.compose.ui.viewinterop.AndroidView
-import io.element.android.features.login.impl.oidc.OidcUrlParser
 import io.element.android.libraries.core.bool.orFalse
 import io.element.android.libraries.designsystem.components.async.AsyncActionView
+import io.element.android.libraries.designsystem.components.button.BackButton
 import io.element.android.libraries.designsystem.preview.ElementPreview
 import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.Scaffold
+import io.element.android.libraries.designsystem.theme.components.TopAppBar
+import io.element.android.libraries.oidc.impl.OidcUrlParser
 
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun OidcView(
     state: OidcState,
     onNavigateBack: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
+    val isPreview = LocalInspectionMode.current
     val oidcUrlParser = remember { OidcUrlParser() }
     var webView by remember { mutableStateOf<WebView?>(null) }
     fun shouldOverrideUrl(url: String): Boolean {
@@ -55,7 +62,7 @@ fun OidcView(
         OidcWebViewClient(::shouldOverrideUrl)
     }
 
-    BackHandler {
+    fun onBack() {
         if (webView?.canGoBack().orFalse()) {
             webView?.goBack()
         } else {
@@ -64,12 +71,35 @@ fun OidcView(
         }
     }
 
-    Box(modifier = modifier.statusBarsPadding()) {
+    BackHandler { onBack() }
+
+    Scaffold(
+        modifier = modifier,
+        topBar = {
+            TopAppBar(
+                title = {},
+                navigationIcon = {
+                    BackButton(onClick = ::onBack)
+                },
+            )
+        }
+    ) { contentPadding ->
         AndroidView(
+            modifier = Modifier.padding(contentPadding),
             factory = { context ->
                 WebView(context).apply {
-                    webViewClient = oidcWebViewClient
-                    loadUrl(state.oidcDetails.url)
+                    if (!isPreview) {
+                        webViewClient = oidcWebViewClient
+                        settings.apply {
+                            @SuppressLint("SetJavaScriptEnabled")
+                            javaScriptEnabled = true
+                            allowContentAccess = true
+                            allowFileAccess = true
+                            databaseEnabled = true
+                            domStorageEnabled = true
+                        }
+                        loadUrl(state.oidcDetails.url)
+                    }
                 }.also {
                     webView = it
                 }
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcWebViewClient.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcWebViewClient.kt
similarity index 95%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcWebViewClient.kt
rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcWebViewClient.kt
index a9cd576e6de..d20eabf00e4 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcWebViewClient.kt
+++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcWebViewClient.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.impl.oidc.webview
+package io.element.android.libraries.oidc.impl.webview
 
 import android.webkit.WebResourceRequest
 import android.webkit.WebView
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/WebViewEventListener.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/WebViewEventListener.kt
similarity index 93%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/WebViewEventListener.kt
rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/WebViewEventListener.kt
index 446754aced7..8f587966bf3 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/WebViewEventListener.kt
+++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/WebViewEventListener.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.impl.oidc.webview
+package io.element.android.libraries.oidc.impl.webview
 
 fun interface WebViewEventListener {
     /**
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index ef997755dd8..730afa291e9 100644
--- a/libraries/ui-strings/src/main/res/values/localazy.xml
+++ b/libraries/ui-strings/src/main/res/values/localazy.xml
@@ -268,12 +268,6 @@ Reason: %1$s."</string>
   <string name="invite_friends_text">"Hey, talk to me on %1$s: %2$s"</string>
   <string name="login_initial_device_name_android">"%1$s Android"</string>
   <string name="preference_rageshake">"Rageshake to report bug"</string>
-  <string name="screen_encryption_reset_bullet_1">"Your account details, contacts, preferences, and chat list will be kept"</string>
-  <string name="screen_encryption_reset_bullet_2">"You will lose your existing message history"</string>
-  <string name="screen_encryption_reset_bullet_3">"You will need to verify all your existing devices and contacts again"</string>
-  <string name="screen_encryption_reset_footer">"Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key."</string>
-  <string name="screen_encryption_reset_subtitle">"If you’re not signed in to any other devices and you’ve lost your recovery key, then you’ll need to reset your identity to continue using the app. "</string>
-  <string name="screen_encryption_reset_title">"Reset your identity in case you can’t confirm another way"</string>
   <string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
   <string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
   <string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>
@@ -284,12 +278,6 @@ Reason: %1$s."</string>
     <item quantity="other">"%1$d Pinned messages"</item>
   </plurals>
   <string name="screen_pinned_timeline_screen_title_empty">"Pinned messages"</string>
-  <string name="screen_reset_encryption_confirmation_alert_action">"Yes, reset now"</string>
-  <string name="screen_reset_encryption_confirmation_alert_subtitle">"This process is irreversible."</string>
-  <string name="screen_reset_encryption_confirmation_alert_title">"Are you sure you want to reset your encryption?"</string>
-  <string name="screen_reset_encryption_password_placeholder">"Enter…"</string>
-  <string name="screen_reset_encryption_password_subtitle">"Confirm that you want to reset your encryption."</string>
-  <string name="screen_reset_encryption_password_title">"Enter your account password to continue"</string>
   <string name="screen_room_details_pinned_events_row_title">"Pinned messages"</string>
   <string name="screen_room_error_failed_processing_media">"Failed processing media to upload, please try again."</string>
   <string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt
index 9fd82af4ae4..bd83d527ca6 100644
--- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt
+++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt
@@ -116,6 +116,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
     implementation(project(":libraries:mediaviewer:impl"))
     implementation(project(":libraries:troubleshoot:impl"))
     implementation(project(":libraries:fullscreenintent:impl"))
+    implementation(project(":libraries:oidc:impl"))
 }
 
 fun DependencyHandlerScope.allServicesImpl() {
diff --git a/tools/localazy/config.json b/tools/localazy/config.json
index 47ef2c1c51f..82fb4f22777 100644
--- a/tools/localazy/config.json
+++ b/tools/localazy/config.json
@@ -210,7 +210,10 @@
         "screen_chat_backup_.*",
         "screen_key_backup_disable_.*",
         "screen_recovery_key_.*",
-        "screen_create_new_recovery_key_.*"
+        "screen_create_new_recovery_key_.*",
+        "screen_encryption_reset.*",
+        "screen_reset_encryption.*",
+        "screen\\.reset_encryption.*"
       ]
     },
     {


Test patch:
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt
index aa0fe9cbf12..8134c091f61 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt
@@ -21,9 +21,6 @@ import android.content.Intent
 import android.net.Uri
 import androidx.core.net.toUri
 import com.google.common.truth.Truth.assertThat
-import io.element.android.features.login.api.oidc.OidcAction
-import io.element.android.features.login.impl.oidc.DefaultOidcIntentResolver
-import io.element.android.features.login.impl.oidc.OidcUrlParser
 import io.element.android.libraries.deeplink.DeepLinkCreator
 import io.element.android.libraries.deeplink.DeeplinkData
 import io.element.android.libraries.deeplink.DeeplinkParser
@@ -33,6 +30,9 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
 import io.element.android.libraries.matrix.test.A_SESSION_ID
 import io.element.android.libraries.matrix.test.A_THREAD_ID
 import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
+import io.element.android.libraries.oidc.api.OidcAction
+import io.element.android.libraries.oidc.impl.DefaultOidcIntentResolver
+import io.element.android.libraries.oidc.impl.OidcUrlParser
 import io.element.android.tests.testutils.lambda.lambdaError
 import org.junit.Assert.assertThrows
 import org.junit.Test
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 5a02797bdb3..3875a4b245b 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,10 +20,8 @@ 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.AsyncData
 import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@@ -31,6 +29,8 @@ 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.FakeMatrixAuthenticationService
+import io.element.android.libraries.oidc.api.OidcAction
+import io.element.android.libraries.oidc.impl.customtab.DefaultOidcActionFlow
 import io.element.android.tests.testutils.WarmUpRule
 import io.element.android.tests.testutils.waitForPredicate
 import kotlinx.coroutines.test.runTest
@@ -274,7 +274,7 @@ class ConfirmAccountProviderPresenterTest {
         params = params,
         accountProviderDataSource = accountProviderDataSource,
         authenticationService = matrixAuthenticationService,
-        defaultOidcActionFlow = defaultOidcActionFlow,
+        oidcActionFlow = defaultOidcActionFlow,
         defaultLoginUserStory = defaultLoginUserStory,
     )
 }
diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt
new file mode 100644
index 00000000000..eb33fe5c366
--- /dev/null
+++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset
+
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle
+import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
+import io.element.android.libraries.matrix.test.FakeMatrixClient
+import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
+import io.element.android.libraries.matrix.test.encryption.FakeIdentityPasswordResetHandle
+import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class ResetIdentityFlowManagerTest {
+    @Test
+    fun `getResetHandle - emits a reset handle`() = runTest {
+        val startResetLambda = lambdaRecorder<Result<IdentityResetHandle?>> { Result.success(FakeIdentityPasswordResetHandle()) }
+        val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda)
+        val flowManager = createFlowManager(encryptionService = encryptionService)
+
+        flowManager.getResetHandle().test {
+            assertThat(awaitItem().isLoading()).isTrue()
+            assertThat(awaitItem().isSuccess()).isTrue()
+            startResetLambda.assertions().isCalledOnce()
+        }
+    }
+
+    @Test
+    fun `getResetHandle - om successful handle retrieval returns that same handle`() = runTest {
+        val startResetLambda = lambdaRecorder<Result<IdentityResetHandle?>> { Result.success(FakeIdentityPasswordResetHandle()) }
+        val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda)
+        val flowManager = createFlowManager(encryptionService = encryptionService)
+
+        var result: AsyncData.Success<IdentityResetHandle>? = null
+        flowManager.getResetHandle().test {
+            assertThat(awaitItem().isLoading()).isTrue()
+            result = awaitItem() as? AsyncData.Success<IdentityResetHandle>
+            assertThat(result).isNotNull()
+        }
+
+        flowManager.getResetHandle().test {
+            assertThat(awaitItem()).isSameInstanceAs(result)
+        }
+    }
+
+    @Test
+    fun `getResetHandle - will fail if it receives a null reset handle`() = runTest {
+        val startResetLambda = lambdaRecorder<Result<IdentityResetHandle?>> { Result.success(null) }
+        val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda)
+        val flowManager = createFlowManager(encryptionService = encryptionService)
+
+        flowManager.getResetHandle().test {
+            assertThat(awaitItem().isLoading()).isTrue()
+            assertThat(awaitItem().isFailure()).isTrue()
+            startResetLambda.assertions().isCalledOnce()
+        }
+    }
+
+    @Test
+    fun `getResetHandle - fails gracefully when receiving an exception from the encryption service`() = runTest {
+        val startResetLambda = lambdaRecorder<Result<IdentityResetHandle?>> { Result.failure(IllegalStateException("Failure")) }
+        val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda)
+        val flowManager = createFlowManager(encryptionService = encryptionService)
+
+        flowManager.getResetHandle().test {
+            assertThat(awaitItem().isLoading()).isTrue()
+            assertThat(awaitItem().isFailure()).isTrue()
+            startResetLambda.assertions().isCalledOnce()
+        }
+    }
+
+    @Test
+    fun `cancel - resets the state and calls cancel on the reset handle`() = runTest {
+        val cancelLambda = lambdaRecorder<Unit> { }
+        val resetHandle = FakeIdentityPasswordResetHandle(cancelLambda = cancelLambda)
+        val startResetLambda = lambdaRecorder<Result<IdentityResetHandle?>> { Result.success(resetHandle) }
+        val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda)
+        val flowManager = createFlowManager(encryptionService = encryptionService)
+
+        flowManager.getResetHandle().test {
+            assertThat(awaitItem().isLoading()).isTrue()
+            assertThat(awaitItem().isSuccess()).isTrue()
+
+            flowManager.cancel()
+            cancelLambda.assertions().isCalledOnce()
+            assertThat(awaitItem().isUninitialized()).isTrue()
+        }
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun `whenResetIsDone - will trigger the lambda when verification status is verified`() = runTest {
+        val verificationService = FakeSessionVerificationService()
+        val flowManager = createFlowManager(sessionVerificationService = verificationService)
+        var isDone = false
+
+        flowManager.whenResetIsDone {
+            isDone = true
+        }
+
+        assertThat(isDone).isFalse()
+
+        verificationService.emitVerifiedStatus(SessionVerifiedStatus.Unknown)
+        advanceUntilIdle()
+        assertThat(isDone).isFalse()
+
+        verificationService.emitVerifiedStatus(SessionVerifiedStatus.NotVerified)
+        advanceUntilIdle()
+        assertThat(isDone).isFalse()
+
+        verificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
+        advanceUntilIdle()
+        assertThat(isDone).isTrue()
+    }
+
+    private fun TestScope.createFlowManager(
+        encryptionService: FakeEncryptionService = FakeEncryptionService(),
+        client: FakeMatrixClient = FakeMatrixClient(encryptionService = encryptionService),
+        sessionCoroutineScope: CoroutineScope = this,
+        sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
+    ) = ResetIdentityFlowManager(
+        matrixClient = client,
+        sessionCoroutineScope = sessionCoroutineScope,
+        sessionVerificationService = sessionVerificationService,
+    )
+}
diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenterTest.kt
new file mode 100644
index 00000000000..059983df3d2
--- /dev/null
+++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenterTest.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset.password
+
+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.libraries.matrix.test.encryption.FakeIdentityPasswordResetHandle
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.testCoroutineDispatchers
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class ResetIdentityPasswordPresenterTest {
+    @Test
+    fun `present - initial state`() = runTest {
+        val presenter = createPresenter()
+        moleculeFlow(RecompositionMode.Immediate) {
+            presenter.present()
+        }.test {
+            val initialState = awaitItem()
+            assertThat(initialState.resetAction.isUninitialized()).isTrue()
+        }
+    }
+
+    @Test
+    fun `present - Reset event succeeds`() = runTest {
+        val resetLambda = lambdaRecorder<String, Result<Unit>> { _ -> Result.success(Unit) }
+        val resetHandle = FakeIdentityPasswordResetHandle(resetPasswordLambda = resetLambda)
+        val presenter = createPresenter(identityResetHandle = resetHandle)
+        moleculeFlow(RecompositionMode.Immediate) {
+            presenter.present()
+        }.test {
+            val initialState = awaitItem()
+            initialState.eventSink(ResetIdentityPasswordEvent.Reset("password"))
+            assertThat(awaitItem().resetAction.isLoading()).isTrue()
+            assertThat(awaitItem().resetAction.isSuccess()).isTrue()
+        }
+    }
+
+    @Test
+    fun `present - Reset event can fail gracefully`() = runTest {
+        val resetLambda = lambdaRecorder<String, Result<Unit>> { _ -> Result.failure(IllegalStateException("Failed")) }
+        val resetHandle = FakeIdentityPasswordResetHandle(resetPasswordLambda = resetLambda)
+        val presenter = createPresenter(identityResetHandle = resetHandle)
+        moleculeFlow(RecompositionMode.Immediate) {
+            presenter.present()
+        }.test {
+            val initialState = awaitItem()
+            initialState.eventSink(ResetIdentityPasswordEvent.Reset("password"))
+            assertThat(awaitItem().resetAction.isLoading()).isTrue()
+            assertThat(awaitItem().resetAction.isFailure()).isTrue()
+        }
+    }
+
+    @Test
+    fun `present - DismissError event resets the state`() = runTest {
+        val resetLambda = lambdaRecorder<String, Result<Unit>> { _ -> Result.failure(IllegalStateException("Failed")) }
+        val resetHandle = FakeIdentityPasswordResetHandle(resetPasswordLambda = resetLambda)
+        val presenter = createPresenter(identityResetHandle = resetHandle)
+        moleculeFlow(RecompositionMode.Immediate) {
+            presenter.present()
+        }.test {
+            val initialState = awaitItem()
+            initialState.eventSink(ResetIdentityPasswordEvent.Reset("password"))
+            assertThat(awaitItem().resetAction.isLoading()).isTrue()
+            assertThat(awaitItem().resetAction.isFailure()).isTrue()
+
+            initialState.eventSink(ResetIdentityPasswordEvent.DismissError)
+            assertThat(awaitItem().resetAction.isUninitialized()).isTrue()
+        }
+    }
+
+    private fun TestScope.createPresenter(
+        identityResetHandle: FakeIdentityPasswordResetHandle = FakeIdentityPasswordResetHandle(),
+    ) = ResetIdentityPasswordPresenter(
+        identityPasswordResetHandle = identityResetHandle,
+        dispatchers = testCoroutineDispatchers(),
+    )
+}
diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt
new file mode 100644
index 00000000000..2449146399c
--- /dev/null
+++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset.password
+
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performTextInput
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.tests.testutils.EnsureNeverCalled
+import io.element.android.tests.testutils.EventsRecorder
+import io.element.android.tests.testutils.clickOn
+import io.element.android.tests.testutils.ensureCalledOnce
+import io.element.android.tests.testutils.pressBack
+import io.element.android.tests.testutils.pressBackKey
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ResetIdentityPasswordViewTest {
+    @get:Rule
+    val rule = createAndroidComposeRule<ComponentActivity>()
+
+    @Test
+    fun `pressing the back HW button invokes the expected callback`() {
+        ensureCalledOnce {
+            rule.setResetPasswordView(
+                ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}),
+                onBack = it,
+            )
+            rule.pressBackKey()
+        }
+    }
+
+    @Test
+    fun `clicking on the back navigation button invokes the expected callback`() {
+        ensureCalledOnce {
+            rule.setResetPasswordView(
+                ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}),
+                onBack = it,
+            )
+            rule.pressBack()
+        }
+    }
+
+    @Test
+    fun `clicking 'Reset identity' confirms the reset`() {
+        val eventsRecorder = EventsRecorder<ResetIdentityPasswordEvent>()
+        rule.setResetPasswordView(
+            ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = eventsRecorder),
+        )
+        rule.onNodeWithText("Password").performTextInput("A password")
+
+        rule.clickOn(CommonStrings.action_reset_identity)
+
+        eventsRecorder.assertSingle(ResetIdentityPasswordEvent.Reset("A password"))
+    }
+
+    @Test
+    fun `modifying the password dismisses the error state`() {
+        val eventsRecorder = EventsRecorder<ResetIdentityPasswordEvent>()
+        rule.setResetPasswordView(
+            ResetIdentityPasswordState(resetAction = AsyncAction.Failure(IllegalStateException("A failure")), eventSink = eventsRecorder),
+        )
+        rule.onNodeWithText("Password").performTextInput("A password")
+
+        eventsRecorder.assertSingle(ResetIdentityPasswordEvent.DismissError)
+    }
+}
+
+private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setResetPasswordView(
+    state: ResetIdentityPasswordState,
+    onBack: () -> Unit = EnsureNeverCalled(),
+) {
+    setContent {
+        ResetIdentityPasswordView(state = state, onBack = onBack)
+    }
+}
diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTest.kt
new file mode 100644
index 00000000000..feb00bf4de5
--- /dev/null
+++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset.root
+
+import app.cash.molecule.RecompositionMode
+import app.cash.molecule.moleculeFlow
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class ResetIdentityRootPresenterTest {
+    @Test
+    fun `present - initial state`() = runTest {
+        val presenter = ResetIdentityRootPresenter()
+        moleculeFlow(RecompositionMode.Immediate) {
+            presenter.present()
+        }.test {
+            val initialState = awaitItem()
+            assertThat(initialState.displayConfirmationDialog).isFalse()
+        }
+    }
+
+    @Test
+    fun `present - Continue event displays the confirmation dialog`() = runTest {
+        val presenter = ResetIdentityRootPresenter()
+        moleculeFlow(RecompositionMode.Immediate) {
+            presenter.present()
+        }.test {
+            val initialState = awaitItem()
+            initialState.eventSink(ResetIdentityRootEvent.Continue)
+
+            assertThat(awaitItem().displayConfirmationDialog).isTrue()
+        }
+    }
+
+    @Test
+    fun `present - DismissDialog event hides the confirmation dialog`() = runTest {
+        val presenter = ResetIdentityRootPresenter()
+        moleculeFlow(RecompositionMode.Immediate) {
+            presenter.present()
+        }.test {
+            val initialState = awaitItem()
+            initialState.eventSink(ResetIdentityRootEvent.Continue)
+            assertThat(awaitItem().displayConfirmationDialog).isTrue()
+
+            initialState.eventSink(ResetIdentityRootEvent.DismissDialog)
+            assertThat(awaitItem().displayConfirmationDialog).isFalse()
+        }
+    }
+}
diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt
new file mode 100644
index 00000000000..da7f11ba42e
--- /dev/null
+++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.features.securebackup.impl.reset.root
+
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.element.android.features.securebackup.impl.R
+import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.tests.testutils.EnsureNeverCalled
+import io.element.android.tests.testutils.EventsRecorder
+import io.element.android.tests.testutils.clickOn
+import io.element.android.tests.testutils.ensureCalledOnce
+import io.element.android.tests.testutils.pressBack
+import io.element.android.tests.testutils.pressBackKey
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class ResetIdentityRootViewTest {
+    @get:Rule
+    val rule = createAndroidComposeRule<ComponentActivity>()
+
+    @Test
+    fun `pressing the back HW button invokes the expected callback`() {
+        ensureCalledOnce {
+            rule.setResetRootView(
+                ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}),
+                onBack = it,
+            )
+            rule.pressBackKey()
+        }
+    }
+
+    @Test
+    fun `clicking on the back navigation button invokes the expected callback`() {
+        ensureCalledOnce {
+            rule.setResetRootView(
+                ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}),
+                onBack = it,
+            )
+            rule.pressBack()
+        }
+    }
+
+    @Test
+    @Config(qualifiers = "h720dp")
+    fun `clicking Continue displays the confirmation dialog`() {
+        val eventsRecorder = EventsRecorder<ResetIdentityRootEvent>()
+        rule.setResetRootView(
+            ResetIdentityRootState(displayConfirmationDialog = false, eventSink = eventsRecorder),
+        )
+
+        rule.clickOn(CommonStrings.action_continue)
+
+        eventsRecorder.assertSingle(ResetIdentityRootEvent.Continue)
+    }
+
+    @Test
+    fun `clicking 'Yes, reset now' confirms the reset`() {
+        ensureCalledOnce {
+            rule.setResetRootView(
+                ResetIdentityRootState(displayConfirmationDialog = true, eventSink = {}),
+                onContinue = it,
+            )
+            rule.clickOn(R.string.screen_reset_encryption_confirmation_alert_action)
+        }
+    }
+
+    @Test
+    fun `clicking Cancel dismisses the dialog`() {
+        val eventsRecorder = EventsRecorder<ResetIdentityRootEvent>()
+        rule.setResetRootView(
+            ResetIdentityRootState(displayConfirmationDialog = true, eventSink = eventsRecorder),
+        )
+
+        rule.clickOn(CommonStrings.action_cancel)
+        eventsRecorder.assertSingle(ResetIdentityRootEvent.DismissDialog)
+    }
+}
+
+private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setResetRootView(
+    state: ResetIdentityRootState,
+    onBack: () -> Unit = EnsureNeverCalled(),
+    onContinue: () -> Unit = EnsureNeverCalled(),
+) {
+    setContent {
+        ResetIdentityRootView(state = state, onContinue = onContinue, onBack = onBack)
+    }
+}
diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt
index f19f3e1f298..7e5bf928e27 100644
--- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt
+++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt
@@ -217,12 +217,14 @@ class VerifySelfSessionViewTest {
         state: VerifySelfSessionState,
         onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(),
         onFinished: () -> Unit = EnsureNeverCalled(),
+        onResetKey: () -> Unit = EnsureNeverCalled(),
     ) {
         setContent {
             VerifySelfSessionView(
                 state = state,
                 onEnterRecoveryKey = onEnterRecoveryKey,
                 onFinish = onFinished,
+                onResetKey = onResetKey,
             )
         }
     }
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt
index b864c69b0b2..82ab5e251f9 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt
@@ -20,13 +20,17 @@ import io.element.android.libraries.matrix.api.encryption.BackupState
 import io.element.android.libraries.matrix.api.encryption.BackupUploadState
 import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
 import io.element.android.libraries.matrix.api.encryption.EncryptionService
+import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle
 import io.element.android.libraries.matrix.api.encryption.RecoveryState
+import io.element.android.tests.testutils.lambda.lambdaError
 import io.element.android.tests.testutils.simulateLongTask
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
 
-class FakeEncryptionService : EncryptionService {
+class FakeEncryptionService(
+    var startIdentityResetLambda: () -> Result<IdentityResetHandle?> = { lambdaError() },
+) : EncryptionService {
     private var disableRecoveryFailure: Exception? = null
     override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(BackupState.UNKNOWN)
     override val recoveryStateStateFlow: MutableStateFlow<RecoveryState> = MutableStateFlow(RecoveryState.UNKNOWN)
@@ -118,6 +122,10 @@ class FakeEncryptionService : EncryptionService {
         enableRecoveryProgressStateFlow.emit(state)
     }
 
+    override suspend fun startIdentityReset(): Result<IdentityResetHandle?> {
+        return startIdentityResetLambda()
+    }
+
     companion object {
         const val FAKE_RECOVERY_KEY = "fake"
     }
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt
new file mode 100644
index 00000000000..69087163d28
--- /dev/null
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2024 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
+ *
+ *     https://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.test.encryption
+
+import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle
+import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
+
+class FakeIdentityOidcResetHandle(
+    override val url: String = "",
+    var resetOidcLambda: () -> Result<Unit> = { error("Not implemented") },
+    var cancelLambda: () -> Unit = { error("Not implemented") },
+) : IdentityOidcResetHandle {
+    override suspend fun resetOidc(): Result<Unit> {
+        return resetOidcLambda()
+    }
+
+    override suspend fun cancel() {
+        cancelLambda()
+    }
+}
+
+class FakeIdentityPasswordResetHandle(
+    var resetPasswordLambda: (String) -> Result<Unit> = { _ -> error("Not implemented") },
+    var cancelLambda: () -> Unit = { error("Not implemented") },
+) : IdentityPasswordResetHandle {
+    override suspend fun resetPassword(password: String): Result<Unit> {
+        return resetPasswordLambda(password)
+    }
+
+    override suspend fun cancel() {
+        cancelLambda()
+    }
+}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParserTest.kt
similarity index 94%
rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt
rename to libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParserTest.kt
index 6f63673b234..895e416faa0 100644
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt
+++ b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParserTest.kt
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package io.element.android.features.login.impl.oidc
+package io.element.android.libraries.oidc.impl
 
 import com.google.common.truth.Truth.assertThat
-import io.element.android.features.login.api.oidc.OidcAction
 import io.element.android.libraries.matrix.api.auth.OidcConfig
+import io.element.android.libraries.oidc.api.OidcAction
 import org.junit.Assert
 import org.junit.Test
 
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenterTest.kt
similarity index 97%
rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt
rename to libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenterTest.kt
index 38d0506dd8d..994334d2e9a 100644
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt
+++ b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenterTest.kt
@@ -16,17 +16,17 @@
 
 @file:OptIn(ExperimentalCoroutinesApi::class)
 
-package io.element.android.features.login.impl.oidc.webview
+package io.element.android.libraries.oidc.impl.webview
 
 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.libraries.architecture.AsyncAction
 import io.element.android.libraries.matrix.test.A_THROWABLE
 import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA
 import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
+import io.element.android.libraries.oidc.api.OidcAction
 import io.element.android.tests.testutils.WarmUpRule
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runTest
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_0_en.png
deleted file mode 100644
index 1022157a43f..00000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_0_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:38b4c852a4bb957a10e24a72b70d7edd89a921983a9426592dc1fc53c1d54d99
-size 5450
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_1_en.png
deleted file mode 100644
index 39071712f4e..00000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_1_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:9cdc07cc74bf427e7ae0244b97543f65aecb6b2f8fee7958d18906d333b868b8
-size 8848
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_0_en.png
deleted file mode 100644
index 15ff876cf4a..00000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_0_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:a0c39aabac135aa3954ac881a43cc2bd79a2e357ec76f9aa46351b2985c17d73
-size 5437
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_1_en.png
deleted file mode 100644
index f99c8e006d8..00000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_1_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:c773553943c9728f1e80b6ca6491c9c8c50f9394f3bd1cb430abb24029a3f926
-size 7791
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png
new file mode 100644
index 00000000000..a49af920d90
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e09769c04ba21aba09eb0de5865f659ad092ba2e46a8b3933f95b1170b09d303
+size 28540
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en.png
new file mode 100644
index 00000000000..4c6ae5f1496
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3702ebe296d7a14e20b3d683fb79f6b2455064a8986ce48c870b5cfd68dc5933
+size 27407
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en.png
new file mode 100644
index 00000000000..4c6ae5f1496
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3702ebe296d7a14e20b3d683fb79f6b2455064a8986ce48c870b5cfd68dc5933
+size 27407
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en.png
new file mode 100644
index 00000000000..1d5e048087b
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b683ed9986e3fd139ee735e51a369322ba3652b2b8a578c05a61f26ee27898e0
+size 39996
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png
new file mode 100644
index 00000000000..96ec36ca5ef
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:14d4ed02bb2f949c4cbba84933bbbfb0c550ad261695a371ef12e74a2dbd8812
+size 27600
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en.png
new file mode 100644
index 00000000000..c4e2dae54bb
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:53f3dc9352250ba7495b6af312f3f370463291d29b76c40d4b3c340b77aa5712
+size 25455
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en.png
new file mode 100644
index 00000000000..c4e2dae54bb
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:53f3dc9352250ba7495b6af312f3f370463291d29b76c40d4b3c340b77aa5712
+size 25455
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en.png
new file mode 100644
index 00000000000..0fa0e6a5196
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b380335d847fa37e5319541f46f341c5fe8016f3af48765cfe7537c59dd1f58a
+size 38379
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png
new file mode 100644
index 00000000000..499713a15dd
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e9b7a1e2d20171a78da4ce4f590372e1d84f9624c4d56a011e74f91105c09f36
+size 79989
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png
new file mode 100644
index 00000000000..6bc0eec9c01
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:43f2eb2cb1f1b986b58d2614c892f2f7f15f1b696a656165ba1f8644f7759476
+size 62679
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png
new file mode 100644
index 00000000000..8685aa5e425
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c3c38d62929774fe6e6e003f656efbeec1f675b5dbb9994a917c4978f64d61a9
+size 78280
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png
new file mode 100644
index 00000000000..e79557dae61
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:08e5a9d4e1bc45e9411b74c7277f34fa0ba8d46fdd1556b65b9c50cc78f1468e
+size 60595
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_0_en.png
index b5f11147ba5..cf48a305b74 100644
--- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_0_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:45548cd4afaac73a72ab4738c099506eb9dead138e8e0cf5668a75c4948b81f6
-size 26027
+oid sha256:ee9c90a91ef8703c4878ea9314121ad8343dd3392c051e7e3e3079198a32ef99
+size 30555
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png
index a989b3b4748..bfef3aada5e 100644
--- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:88f3de0736a207057ce0e71fea632e5197874d93045e87443a957ef0da64b8bd
-size 22760
+oid sha256:a4b44b23029dd1ff76a082a29c0b39584a1e20c92795eb4815d632608abd2858
+size 22702
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_7_en.png
index 4a5ee01fb81..a2d07ed6a45 100644
--- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_7_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:146a2a2439de11035332340ce27e622ff20b661d600fc2977d1f82deb81d0f21
-size 24828
+oid sha256:ce48c81c355317abd6b9de4600e86492c3acde2807ca3572e064b61abc06239c
+size 29426
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_8_en.png
index a4cde41390f..69eb60569f7 100644
--- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_8_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:281e08f29d84da5c1e7c01d701bf5d8f415b6d80fb8a1298742d6e629314a7d8
-size 21186
+oid sha256:4d9810f3b326a05492c870ef22dbceec19112dba8beaa000066504cc96d1387a
+size 23913
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_0_en.png
index 6ec093bbc87..31ba47d4a41 100644
--- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_0_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:bf610f5f758e65ae67836a2b40924e966a8add94fd00605c89ef75ddbd3bf22c
-size 25324
+oid sha256:453cab91aa056f3536c899354a2fb5de21519e7dce3d5675b1cbca702000e2c1
+size 29602
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png
index fddd555a334..89dcb3654d6 100644
--- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:d2ed85817f1d95d1259e07aa566ede6d40360a688e29c0c463378770dd091540
-size 22003
+oid sha256:f468a4a9140c4dc23ab3d394049f59b86f0ca2f332bee50abf8eaa0acce166b5
+size 22030
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_7_en.png
index 130cf67cf45..39ce0ef07f8 100644
--- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_7_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:354c1daceebd169b35a6db0173c954a3425c534156fdc662edfba4d710f51a49
-size 24155
+oid sha256:c21120a08c871c7f64952c20be2c90b28590901f8e52893702775b6b2f176892
+size 28524
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_8_en.png
index 4c87611adad..7d1bfb8eab2 100644
--- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_8_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:5d6ce5c3b44127d5d661ac450ea2cf48617e81e4ae40dfb6d9a5cf89f3591800
-size 20598
+oid sha256:1e46ffc3c3679eb5490d01c34d02314bae5611a7b10fb3c758331a28ae78f065
+size 23267
diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_0_en.png
new file mode 100644
index 00000000000..785f56af7f5
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:154a56d848cc735efc25274052ce92b1a20bc8f80f75e91d0d4cfc7d488cd246
+size 5874
diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_1_en.png
new file mode 100644
index 00000000000..ae8924e66eb
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0915d4781155524bece1db737050ceb78ef76128da39b339fd67dd4a2a4dd79d
+size 9229
diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_0_en.png
new file mode 100644
index 00000000000..c41c1c79218
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2dd3462935091cfe022f0e6fc71b082eed7dab4769def13398a81a90f871b61a
+size 5807
diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_1_en.png
new file mode 100644
index 00000000000..98b81898ef9
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b7cd23334c15b9039cbf0924a82703035b4498e67489418e5244cf2df576af9c
+size 8111